@dmitryvim/form-builder 0.2.32 → 0.3.0
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/browser/formbuilder.min.js +493 -230
- package/dist/browser/formbuilder.v0.3.0.min.js +1411 -0
- package/dist/cjs/index.cjs +1563 -871
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/index.js +1551 -868
- package/dist/esm/index.js.map +1 -1
- package/dist/form-builder.js +493 -230
- package/dist/types/components/boolean.d.ts +4 -0
- package/dist/types/components/colour.d.ts +1 -0
- package/dist/types/components/file/upload.d.ts +4 -0
- package/dist/types/components/file.d.ts +2 -2
- package/dist/types/components/text.d.ts +12 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/instance/FormBuilderInstance.d.ts +14 -1
- package/dist/types/styles/theme.d.ts +47 -0
- package/dist/types/types/component-operations.d.ts +7 -0
- package/dist/types/types/config.d.ts +1 -0
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/types/schema.d.ts +48 -1
- package/dist/types/utils/styles.d.ts +81 -0
- package/package.json +1 -1
- package/dist/browser/formbuilder.v0.2.32.min.js +0 -1148
package/dist/esm/index.js
CHANGED
|
@@ -398,47 +398,331 @@ function deepEqual(a, b) {
|
|
|
398
398
|
return a === b;
|
|
399
399
|
}
|
|
400
400
|
|
|
401
|
+
// src/utils/styles.ts
|
|
402
|
+
function clearFieldError(input) {
|
|
403
|
+
const name = input.getAttribute("name");
|
|
404
|
+
if (!name) return;
|
|
405
|
+
const doc = input.ownerDocument || document;
|
|
406
|
+
const errorNode = doc.getElementById(`error-${name}`);
|
|
407
|
+
if (errorNode) errorNode.remove();
|
|
408
|
+
}
|
|
409
|
+
var BIN_ICON_SVG = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>';
|
|
410
|
+
function ensureThemingHooks(doc) {
|
|
411
|
+
if (doc.head.querySelector("[data-fb-theming-hooks]")) return;
|
|
412
|
+
const style = doc.createElement("style");
|
|
413
|
+
style.setAttribute("data-fb-theming-hooks", "");
|
|
414
|
+
style.textContent = `
|
|
415
|
+
[data-fb-slide-card] {
|
|
416
|
+
background: var(--fb-slide-card-bg);
|
|
417
|
+
box-shadow: var(--fb-slide-card-shadow);
|
|
418
|
+
border-radius: var(--fb-slide-card-radius);
|
|
419
|
+
min-height: var(--fb-slide-card-min-height);
|
|
420
|
+
padding: var(--fb-slide-card-padding);
|
|
421
|
+
}
|
|
422
|
+
[data-fb-label-row] > label {
|
|
423
|
+
font-size: var(--fb-label-section-font-size);
|
|
424
|
+
letter-spacing: var(--fb-label-section-letter-spacing);
|
|
425
|
+
text-transform: var(--fb-label-section-text-transform);
|
|
426
|
+
}
|
|
427
|
+
/* Per-item remove (trash) button used by multi-container items. Shares the
|
|
428
|
+
same faint-on-rest, error-on-hover palette as .fb-chip-remove. */
|
|
429
|
+
.fb-item-remove {
|
|
430
|
+
color: var(--fb-text-faint-color, #94a3b8);
|
|
431
|
+
background-color: transparent;
|
|
432
|
+
transition: color var(--fb-transition-duration), background-color var(--fb-transition-duration);
|
|
433
|
+
}
|
|
434
|
+
.fb-item-remove:hover {
|
|
435
|
+
color: var(--fb-error-color);
|
|
436
|
+
background-color: var(--fb-background-hover-color);
|
|
437
|
+
}
|
|
438
|
+
`;
|
|
439
|
+
doc.head.appendChild(style);
|
|
440
|
+
}
|
|
441
|
+
function applyAutoExpand(textarea) {
|
|
442
|
+
textarea.style.overflow = "hidden";
|
|
443
|
+
textarea.style.resize = "none";
|
|
444
|
+
const lineCount = (textarea.value.match(/\n/g) || []).length + 1;
|
|
445
|
+
textarea.rows = Math.max(1, lineCount);
|
|
446
|
+
const resize = () => {
|
|
447
|
+
if (!textarea.isConnected) return;
|
|
448
|
+
textarea.style.height = "0";
|
|
449
|
+
textarea.style.height = `${textarea.scrollHeight}px`;
|
|
450
|
+
};
|
|
451
|
+
textarea.addEventListener("input", resize);
|
|
452
|
+
setTimeout(() => {
|
|
453
|
+
if (textarea.isConnected) resize();
|
|
454
|
+
}, 0);
|
|
455
|
+
}
|
|
456
|
+
function applySingleLineMode(textarea) {
|
|
457
|
+
textarea.addEventListener("keydown", (e) => {
|
|
458
|
+
if (e.key === "Enter") {
|
|
459
|
+
e.preventDefault();
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
textarea.addEventListener("paste", (e) => {
|
|
463
|
+
const pasted = e.clipboardData?.getData("text") ?? "";
|
|
464
|
+
if (!/[\r\n]/.test(pasted)) return;
|
|
465
|
+
e.preventDefault();
|
|
466
|
+
const cleaned = pasted.replace(/[\r\n]+/g, " ");
|
|
467
|
+
const start = textarea.selectionStart ?? textarea.value.length;
|
|
468
|
+
const end = textarea.selectionEnd ?? textarea.value.length;
|
|
469
|
+
const before = textarea.value.slice(0, start);
|
|
470
|
+
const after = textarea.value.slice(end);
|
|
471
|
+
textarea.value = before + cleaned + after;
|
|
472
|
+
const pos = start + cleaned.length;
|
|
473
|
+
textarea.setSelectionRange(pos, pos);
|
|
474
|
+
textarea.dispatchEvent(new Event("input", { bubbles: true }));
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
function mountCounterInLabel(wrapper, counter) {
|
|
478
|
+
const labelRow = wrapper.querySelector(
|
|
479
|
+
":scope > [data-fb-label-row]"
|
|
480
|
+
);
|
|
481
|
+
if (labelRow) labelRow.appendChild(counter);
|
|
482
|
+
}
|
|
483
|
+
function createAddItemRow(classNameSuffix, onClick, options = {}) {
|
|
484
|
+
const label = options.label ?? "";
|
|
485
|
+
const showCounter = options.showCounter !== false;
|
|
486
|
+
const row = document.createElement("div");
|
|
487
|
+
row.className = "fb-add-row mt-2";
|
|
488
|
+
row.style.cssText = "display:flex;align-items:stretch;width:100%;";
|
|
489
|
+
const button = document.createElement("button");
|
|
490
|
+
button.type = "button";
|
|
491
|
+
button.className = `add-${classNameSuffix}-btn`;
|
|
492
|
+
button.style.cssText = `
|
|
493
|
+
flex: 1 1 auto;
|
|
494
|
+
display: inline-flex;
|
|
495
|
+
align-items: center;
|
|
496
|
+
justify-content: center;
|
|
497
|
+
gap: 6px;
|
|
498
|
+
padding: 6px 10px;
|
|
499
|
+
border: 1px dashed var(--fb-primary-color);
|
|
500
|
+
border-radius: var(--fb-border-radius);
|
|
501
|
+
background: transparent;
|
|
502
|
+
color: var(--fb-primary-color);
|
|
503
|
+
font-size: var(--fb-font-size-small, var(--fb-font-size));
|
|
504
|
+
font-weight: 500;
|
|
505
|
+
font-family: var(--fb-font-family);
|
|
506
|
+
cursor: pointer;
|
|
507
|
+
transition: border-color var(--fb-transition-duration), color var(--fb-transition-duration), background-color var(--fb-transition-duration);
|
|
508
|
+
`;
|
|
509
|
+
button.textContent = label ? `+ ${label}` : "+";
|
|
510
|
+
button.addEventListener("mouseenter", () => {
|
|
511
|
+
if (button.disabled) return;
|
|
512
|
+
button.style.borderStyle = "solid";
|
|
513
|
+
button.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
514
|
+
});
|
|
515
|
+
button.addEventListener("mouseleave", () => {
|
|
516
|
+
button.style.borderStyle = "dashed";
|
|
517
|
+
button.style.backgroundColor = "transparent";
|
|
518
|
+
});
|
|
519
|
+
button.onclick = onClick;
|
|
520
|
+
const counter = document.createElement("span");
|
|
521
|
+
counter.className = "fb-add-counter";
|
|
522
|
+
counter.style.cssText = `
|
|
523
|
+
margin-left: auto;
|
|
524
|
+
font-size: var(--fb-font-size-small, 0.875rem);
|
|
525
|
+
color: var(--fb-text-secondary-color);
|
|
526
|
+
font-weight: 400;
|
|
527
|
+
`;
|
|
528
|
+
if (!showCounter) counter.style.display = "none";
|
|
529
|
+
row.appendChild(button);
|
|
530
|
+
const update = (current, max) => {
|
|
531
|
+
const reached = current >= max;
|
|
532
|
+
row.style.display = reached ? "none" : "flex";
|
|
533
|
+
button.style.display = reached ? "none" : "inline-flex";
|
|
534
|
+
button.disabled = reached;
|
|
535
|
+
if (showCounter) {
|
|
536
|
+
counter.textContent = `${current}/${max === Infinity ? "\u221E" : max}`;
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
return { row, button, counter, update };
|
|
540
|
+
}
|
|
541
|
+
function createSlideAddTile(onClick, options = {}) {
|
|
542
|
+
const label = options.label ?? "";
|
|
543
|
+
const tile = document.createElement("button");
|
|
544
|
+
tile.type = "button";
|
|
545
|
+
tile.className = "add-container-btn fb-slide-add";
|
|
546
|
+
tile.style.cssText = `
|
|
547
|
+
display: flex;
|
|
548
|
+
flex-direction: column;
|
|
549
|
+
align-items: center;
|
|
550
|
+
justify-content: center;
|
|
551
|
+
gap: 12px;
|
|
552
|
+
width: 100%;
|
|
553
|
+
min-height: 180px;
|
|
554
|
+
align-self: stretch;
|
|
555
|
+
padding: 24px 16px;
|
|
556
|
+
border: 1.5px dashed var(--fb-primary-color);
|
|
557
|
+
border-radius: var(--fb-border-radius);
|
|
558
|
+
background: transparent;
|
|
559
|
+
color: var(--fb-primary-color);
|
|
560
|
+
font-size: var(--fb-font-size-small, var(--fb-font-size));
|
|
561
|
+
font-weight: 500;
|
|
562
|
+
font-family: var(--fb-font-family);
|
|
563
|
+
cursor: pointer;
|
|
564
|
+
transition: border-color var(--fb-transition-duration), color var(--fb-transition-duration), background-color var(--fb-transition-duration);
|
|
565
|
+
`;
|
|
566
|
+
const circle = document.createElement("span");
|
|
567
|
+
circle.className = "fb-slide-add-circle";
|
|
568
|
+
circle.style.cssText = `
|
|
569
|
+
display: inline-flex;
|
|
570
|
+
align-items: center;
|
|
571
|
+
justify-content: center;
|
|
572
|
+
width: 36px;
|
|
573
|
+
height: 36px;
|
|
574
|
+
border: 1px solid var(--fb-primary-color);
|
|
575
|
+
border-radius: 50%;
|
|
576
|
+
background: var(--fb-background-color);
|
|
577
|
+
font-size: 20px;
|
|
578
|
+
line-height: 1;
|
|
579
|
+
color: inherit;
|
|
580
|
+
transition: inherit;
|
|
581
|
+
`;
|
|
582
|
+
circle.textContent = "+";
|
|
583
|
+
tile.appendChild(circle);
|
|
584
|
+
if (label) {
|
|
585
|
+
const text = document.createElement("span");
|
|
586
|
+
text.textContent = label;
|
|
587
|
+
tile.appendChild(text);
|
|
588
|
+
}
|
|
589
|
+
tile.addEventListener("mouseenter", () => {
|
|
590
|
+
if (tile.disabled) return;
|
|
591
|
+
tile.style.borderStyle = "solid";
|
|
592
|
+
tile.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
593
|
+
});
|
|
594
|
+
tile.addEventListener("mouseleave", () => {
|
|
595
|
+
tile.style.borderStyle = "dashed";
|
|
596
|
+
tile.style.backgroundColor = "transparent";
|
|
597
|
+
});
|
|
598
|
+
tile.onclick = onClick;
|
|
599
|
+
const counter = document.createElement("span");
|
|
600
|
+
counter.className = "fb-add-counter";
|
|
601
|
+
counter.style.cssText = `
|
|
602
|
+
margin-left: auto;
|
|
603
|
+
font-size: var(--fb-font-size-small, 0.875rem);
|
|
604
|
+
color: var(--fb-text-secondary-color);
|
|
605
|
+
font-weight: 400;
|
|
606
|
+
`;
|
|
607
|
+
const update = (current, max) => {
|
|
608
|
+
const reached = current >= max;
|
|
609
|
+
tile.style.display = reached ? "none" : "flex";
|
|
610
|
+
tile.disabled = reached;
|
|
611
|
+
counter.textContent = `${current}/${max === Infinity ? "\u221E" : max}`;
|
|
612
|
+
};
|
|
613
|
+
return { tile, counter, update };
|
|
614
|
+
}
|
|
615
|
+
function applyActionButtonStyles(button, isFormLevel = false) {
|
|
616
|
+
button.style.cssText = `
|
|
617
|
+
background-color: var(--fb-action-bg-color);
|
|
618
|
+
color: var(--fb-action-text-color);
|
|
619
|
+
border: var(--fb-border-width) solid var(--fb-action-border-color);
|
|
620
|
+
padding: ${isFormLevel ? "0.5rem 1rem" : "0.5rem 0.75rem"};
|
|
621
|
+
font-size: var(--fb-font-size);
|
|
622
|
+
font-weight: var(--fb-font-weight-medium);
|
|
623
|
+
border-radius: var(--fb-border-radius);
|
|
624
|
+
transition: all var(--fb-transition-duration);
|
|
625
|
+
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
|
626
|
+
`;
|
|
627
|
+
button.addEventListener("mouseenter", () => {
|
|
628
|
+
button.style.backgroundColor = "var(--fb-action-hover-bg-color)";
|
|
629
|
+
button.style.borderColor = "var(--fb-action-hover-border-color)";
|
|
630
|
+
});
|
|
631
|
+
button.addEventListener("mouseleave", () => {
|
|
632
|
+
button.style.backgroundColor = "var(--fb-action-bg-color)";
|
|
633
|
+
button.style.borderColor = "var(--fb-action-border-color)";
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
|
|
401
637
|
// src/components/text.ts
|
|
402
|
-
function
|
|
638
|
+
function ensureChipStyles(doc) {
|
|
639
|
+
if (doc.head.querySelector("[data-fb-chip-styles]")) return;
|
|
640
|
+
const style = doc.createElement("style");
|
|
641
|
+
style.setAttribute("data-fb-chip-styles", "");
|
|
642
|
+
style.textContent = `
|
|
643
|
+
.fb-chip-list { display: flex; flex-direction: column; gap: 4px; }
|
|
644
|
+
.fb-chip {
|
|
645
|
+
display: flex;
|
|
646
|
+
align-items: center;
|
|
647
|
+
gap: 8px;
|
|
648
|
+
padding: 5px 6px 5px 10px;
|
|
649
|
+
background: var(--fb-chip-bg, var(--fb-background-color, #fff));
|
|
650
|
+
border: 1px solid var(--fb-chip-border, var(--fb-border-color, #e2e8f0));
|
|
651
|
+
border-radius: 6px;
|
|
652
|
+
position: relative;
|
|
653
|
+
transition: border-color var(--fb-transition-duration, 0.15s);
|
|
654
|
+
}
|
|
655
|
+
.fb-chip:hover { border-color: var(--fb-border-hover-color, var(--fb-border-color, #cbd5e1)); }
|
|
656
|
+
.fb-chip:focus-within { border-color: var(--fb-border-focus-color, var(--fb-primary-color, #2f5bea)); }
|
|
657
|
+
.fb-chip-dot {
|
|
658
|
+
flex: 0 0 6px;
|
|
659
|
+
width: 6px;
|
|
660
|
+
height: 6px;
|
|
661
|
+
border-radius: 50%;
|
|
662
|
+
background: var(--fb-chip-dot, var(--fb-primary-color, #2f5bea));
|
|
663
|
+
}
|
|
664
|
+
.fb-chip-input {
|
|
665
|
+
flex: 1;
|
|
666
|
+
min-width: 0;
|
|
667
|
+
padding: 2px 0;
|
|
668
|
+
border: 0;
|
|
669
|
+
outline: none;
|
|
670
|
+
background: transparent;
|
|
671
|
+
color: var(--fb-chip-text, var(--fb-text-color, inherit));
|
|
672
|
+
font-size: var(--fb-font-size, 14px);
|
|
673
|
+
font-family: var(--fb-font-family, inherit);
|
|
674
|
+
line-height: 1.4;
|
|
675
|
+
}
|
|
676
|
+
.fb-chip-input::placeholder { color: var(--fb-text-placeholder-color, #94a3b8); }
|
|
677
|
+
.fb-chip-input:read-only { color: var(--fb-text-secondary-color, #475569); }
|
|
678
|
+
.fb-chip-remove {
|
|
679
|
+
flex: 0 0 auto;
|
|
680
|
+
width: 22px;
|
|
681
|
+
height: 22px;
|
|
682
|
+
display: inline-flex;
|
|
683
|
+
align-items: center;
|
|
684
|
+
justify-content: center;
|
|
685
|
+
padding: 0;
|
|
686
|
+
border: 0;
|
|
687
|
+
border-radius: 4px;
|
|
688
|
+
background: transparent;
|
|
689
|
+
color: var(--fb-text-faint-color, #94a3b8);
|
|
690
|
+
cursor: pointer;
|
|
691
|
+
opacity: 0;
|
|
692
|
+
transition: opacity 0.12s, color 0.12s, background-color 0.12s;
|
|
693
|
+
}
|
|
694
|
+
.fb-chip:hover .fb-chip-remove,
|
|
695
|
+
.fb-chip-remove:focus-visible { opacity: 1; }
|
|
696
|
+
.fb-chip-remove:hover {
|
|
697
|
+
color: var(--fb-error-color, #dc2626);
|
|
698
|
+
background: var(--fb-background-hover-color, #f1f5f9);
|
|
699
|
+
}
|
|
700
|
+
.fb-chip-remove:disabled { opacity: 0 !important; pointer-events: none; }
|
|
701
|
+
`;
|
|
702
|
+
doc.head.appendChild(style);
|
|
703
|
+
}
|
|
704
|
+
function createCharCounter(element, input) {
|
|
403
705
|
const counter = document.createElement("span");
|
|
404
706
|
counter.className = "char-counter";
|
|
405
707
|
counter.style.cssText = `
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
708
|
+
margin-top: 4px;
|
|
709
|
+
padding-right: 12px;
|
|
710
|
+
text-align: right;
|
|
409
711
|
font-size: var(--fb-font-size-small);
|
|
410
|
-
|
|
712
|
+
line-height: 1;
|
|
713
|
+
color: var(--fb-error-color);
|
|
411
714
|
pointer-events: none;
|
|
412
|
-
|
|
413
|
-
padding: 0 4px;
|
|
715
|
+
display: none;
|
|
414
716
|
`;
|
|
415
717
|
const updateCounter = () => {
|
|
416
718
|
const len = input.value.length;
|
|
417
|
-
const min = element.minLength;
|
|
418
719
|
const max = element.maxLength;
|
|
419
|
-
if (
|
|
420
|
-
counter.textContent = "";
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
if (len === 0 || min != null && len < min) {
|
|
424
|
-
if (min != null && max != null) {
|
|
425
|
-
counter.textContent = `${min}-${max}`;
|
|
426
|
-
} else if (max != null) {
|
|
427
|
-
counter.textContent = `\u2264${max}`;
|
|
428
|
-
} else if (min != null) {
|
|
429
|
-
counter.textContent = `\u2265${min}`;
|
|
430
|
-
}
|
|
431
|
-
counter.style.color = "var(--fb-text-secondary-color)";
|
|
432
|
-
} else if (max != null && len > max) {
|
|
720
|
+
if (max != null && len > max) {
|
|
433
721
|
counter.textContent = `${len}/${max}`;
|
|
434
|
-
counter.style.
|
|
722
|
+
counter.style.display = "block";
|
|
435
723
|
} else {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
} else {
|
|
439
|
-
counter.textContent = `${len}`;
|
|
440
|
-
}
|
|
441
|
-
counter.style.color = "var(--fb-text-secondary-color)";
|
|
724
|
+
counter.textContent = "";
|
|
725
|
+
counter.style.display = "none";
|
|
442
726
|
}
|
|
443
727
|
};
|
|
444
728
|
input.addEventListener("input", updateCounter);
|
|
@@ -450,26 +734,33 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
|
|
|
450
734
|
const readonly = isElementReadonly(element, state, ctx);
|
|
451
735
|
const inputWrapper = document.createElement("div");
|
|
452
736
|
inputWrapper.style.cssText = "position: relative;";
|
|
453
|
-
const
|
|
454
|
-
textInput
|
|
737
|
+
const hasCharCounter = !readonly && (element.minLength != null || element.maxLength != null);
|
|
738
|
+
const textInput = document.createElement("textarea");
|
|
739
|
+
textInput.rows = 1;
|
|
455
740
|
textInput.className = "w-full rounded-lg";
|
|
456
741
|
textInput.style.cssText = `
|
|
457
742
|
padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
|
|
458
|
-
padding-right: 60px;
|
|
459
743
|
border: var(--fb-border-width) solid var(--fb-border-color);
|
|
460
744
|
border-radius: var(--fb-border-radius);
|
|
461
745
|
background-color: ${readonly ? "var(--fb-background-readonly-color)" : "var(--fb-background-color)"};
|
|
462
746
|
color: var(--fb-text-color);
|
|
463
747
|
font-size: var(--fb-font-size);
|
|
464
748
|
font-family: var(--fb-font-family);
|
|
749
|
+
line-height: var(--fb-line-height, 1.5);
|
|
465
750
|
transition: all var(--fb-transition-duration) ease-in-out;
|
|
466
751
|
width: 100%;
|
|
467
752
|
box-sizing: border-box;
|
|
753
|
+
resize: none;
|
|
754
|
+
overflow: hidden;
|
|
755
|
+
word-break: break-word;
|
|
756
|
+
overflow-wrap: anywhere;
|
|
468
757
|
`;
|
|
469
758
|
textInput.name = pathKey;
|
|
470
759
|
textInput.placeholder = element.placeholder || "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442";
|
|
471
760
|
textInput.value = ctx.prefill[element.key] || element.default || "";
|
|
472
761
|
textInput.readOnly = readonly;
|
|
762
|
+
applySingleLineMode(textInput);
|
|
763
|
+
applyAutoExpand(textInput);
|
|
473
764
|
if (!readonly) {
|
|
474
765
|
textInput.addEventListener("focus", () => {
|
|
475
766
|
textInput.style.borderColor = "var(--fb-border-focus-color)";
|
|
@@ -500,8 +791,8 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
|
|
|
500
791
|
textInput.addEventListener("input", handleChange);
|
|
501
792
|
}
|
|
502
793
|
inputWrapper.appendChild(textInput);
|
|
503
|
-
if (
|
|
504
|
-
const counter = createCharCounter(element, textInput
|
|
794
|
+
if (hasCharCounter) {
|
|
795
|
+
const counter = createCharCounter(element, textInput);
|
|
505
796
|
inputWrapper.appendChild(counter);
|
|
506
797
|
}
|
|
507
798
|
wrapper.appendChild(inputWrapper);
|
|
@@ -516,174 +807,100 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
516
807
|
while (values.length < minCount) {
|
|
517
808
|
values.push(element.default || "");
|
|
518
809
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
810
|
+
ensureChipStyles(document);
|
|
811
|
+
const list = document.createElement("div");
|
|
812
|
+
list.className = "fb-chip-list";
|
|
813
|
+
wrapper.appendChild(list);
|
|
522
814
|
function updateIndices() {
|
|
523
|
-
const items =
|
|
524
|
-
items.forEach((
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
815
|
+
const items = list.querySelectorAll(".fb-chip-input");
|
|
816
|
+
items.forEach((input, index) => {
|
|
817
|
+
input.name = `${pathKey}[${index}]`;
|
|
818
|
+
const chip = input.closest(".fb-chip");
|
|
819
|
+
const sib = chip?.nextElementSibling;
|
|
820
|
+
if (sib && sib.classList.contains("error-message")) {
|
|
821
|
+
sib.id = `error-${input.name}`;
|
|
528
822
|
}
|
|
529
823
|
});
|
|
530
824
|
}
|
|
531
|
-
function
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
font-size: var(--fb-font-size);
|
|
546
|
-
font-family: var(--fb-font-family);
|
|
547
|
-
transition: all var(--fb-transition-duration) ease-in-out;
|
|
548
|
-
width: 100%;
|
|
549
|
-
box-sizing: border-box;
|
|
550
|
-
`;
|
|
551
|
-
textInput.placeholder = element.placeholder || t("placeholderText", state);
|
|
552
|
-
textInput.value = value;
|
|
553
|
-
textInput.readOnly = readonly;
|
|
554
|
-
if (!readonly) {
|
|
555
|
-
textInput.addEventListener("focus", () => {
|
|
556
|
-
textInput.style.borderColor = "var(--fb-border-focus-color)";
|
|
557
|
-
textInput.style.outline = `var(--fb-focus-ring-width) solid var(--fb-focus-ring-color)`;
|
|
558
|
-
textInput.style.outlineOffset = "0";
|
|
559
|
-
});
|
|
560
|
-
textInput.addEventListener("blur", () => {
|
|
561
|
-
textInput.style.borderColor = "var(--fb-border-color)";
|
|
562
|
-
textInput.style.outline = "none";
|
|
563
|
-
});
|
|
564
|
-
textInput.addEventListener("mouseenter", () => {
|
|
565
|
-
if (document.activeElement !== textInput) {
|
|
566
|
-
textInput.style.borderColor = "var(--fb-border-hover-color)";
|
|
567
|
-
}
|
|
568
|
-
});
|
|
569
|
-
textInput.addEventListener("mouseleave", () => {
|
|
570
|
-
if (document.activeElement !== textInput) {
|
|
571
|
-
textInput.style.borderColor = "var(--fb-border-color)";
|
|
572
|
-
}
|
|
573
|
-
});
|
|
574
|
-
}
|
|
825
|
+
function addChip(value = "") {
|
|
826
|
+
const chip = document.createElement("div");
|
|
827
|
+
chip.className = "fb-chip";
|
|
828
|
+
const dot = document.createElement("span");
|
|
829
|
+
dot.className = "fb-chip-dot";
|
|
830
|
+
dot.setAttribute("aria-hidden", "true");
|
|
831
|
+
chip.appendChild(dot);
|
|
832
|
+
const input = document.createElement("input");
|
|
833
|
+
input.type = "text";
|
|
834
|
+
input.className = "fb-chip-input";
|
|
835
|
+
input.value = value;
|
|
836
|
+
input.placeholder = element.placeholder || t("placeholderText", state);
|
|
837
|
+
input.readOnly = readonly;
|
|
838
|
+
chip.appendChild(input);
|
|
575
839
|
if (!readonly && ctx.instance) {
|
|
576
840
|
const handleChange = () => {
|
|
577
|
-
|
|
578
|
-
|
|
841
|
+
ctx.instance.triggerOnChange(
|
|
842
|
+
input.name,
|
|
843
|
+
input.value === "" ? null : input.value
|
|
844
|
+
);
|
|
579
845
|
};
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
}
|
|
583
|
-
inputContainer.appendChild(textInput);
|
|
584
|
-
if (!readonly && (element.minLength != null || element.maxLength != null)) {
|
|
585
|
-
const counter = createCharCounter(element, textInput, false);
|
|
586
|
-
inputContainer.appendChild(counter);
|
|
846
|
+
input.addEventListener("blur", handleChange);
|
|
847
|
+
input.addEventListener("input", handleChange);
|
|
587
848
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
849
|
+
if (!readonly) {
|
|
850
|
+
const rem = document.createElement("button");
|
|
851
|
+
rem.type = "button";
|
|
852
|
+
rem.className = "fb-chip-remove";
|
|
853
|
+
rem.setAttribute("aria-label", t("removeElement", state));
|
|
854
|
+
rem.innerHTML = BIN_ICON_SVG;
|
|
855
|
+
rem.onclick = () => {
|
|
856
|
+
const chips = list.querySelectorAll(".fb-chip");
|
|
857
|
+
const idx = Array.prototype.indexOf.call(chips, chip);
|
|
858
|
+
if (idx < 0) return;
|
|
859
|
+
if (chips.length <= minCount) return;
|
|
860
|
+
values.splice(idx, 1);
|
|
861
|
+
const trailingError = chip.nextElementSibling;
|
|
862
|
+
if (trailingError && trailingError.classList.contains("error-message")) {
|
|
863
|
+
trailingError.remove();
|
|
864
|
+
}
|
|
865
|
+
chip.remove();
|
|
866
|
+
updateIndices();
|
|
867
|
+
updateAddButton();
|
|
868
|
+
updateRemoveButtons();
|
|
869
|
+
};
|
|
870
|
+
chip.appendChild(rem);
|
|
593
871
|
}
|
|
872
|
+
list.appendChild(chip);
|
|
594
873
|
updateIndices();
|
|
595
|
-
return
|
|
874
|
+
return chip;
|
|
596
875
|
}
|
|
597
876
|
function updateRemoveButtons() {
|
|
598
877
|
if (readonly) return;
|
|
599
|
-
const
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
".remove-item-btn"
|
|
604
|
-
);
|
|
605
|
-
if (!removeBtn) {
|
|
606
|
-
removeBtn = document.createElement("button");
|
|
607
|
-
removeBtn.type = "button";
|
|
608
|
-
removeBtn.className = "remove-item-btn px-2 py-1 rounded";
|
|
609
|
-
removeBtn.style.cssText = `
|
|
610
|
-
color: var(--fb-error-color);
|
|
611
|
-
background-color: transparent;
|
|
612
|
-
transition: background-color var(--fb-transition-duration);
|
|
613
|
-
`;
|
|
614
|
-
removeBtn.innerHTML = "\u2715";
|
|
615
|
-
removeBtn.addEventListener("mouseenter", () => {
|
|
616
|
-
removeBtn.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
617
|
-
});
|
|
618
|
-
removeBtn.addEventListener("mouseleave", () => {
|
|
619
|
-
removeBtn.style.backgroundColor = "transparent";
|
|
620
|
-
});
|
|
621
|
-
removeBtn.onclick = () => {
|
|
622
|
-
const currentIndex = Array.from(container.children).indexOf(
|
|
623
|
-
item
|
|
624
|
-
);
|
|
625
|
-
if (container.children.length > minCount) {
|
|
626
|
-
values.splice(currentIndex, 1);
|
|
627
|
-
item.remove();
|
|
628
|
-
updateIndices();
|
|
629
|
-
updateAddButton();
|
|
630
|
-
updateRemoveButtons();
|
|
631
|
-
}
|
|
632
|
-
};
|
|
633
|
-
item.appendChild(removeBtn);
|
|
634
|
-
}
|
|
635
|
-
const disabled = currentCount <= minCount;
|
|
636
|
-
removeBtn.disabled = disabled;
|
|
637
|
-
removeBtn.style.opacity = disabled ? "0.5" : "1";
|
|
638
|
-
removeBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
878
|
+
const chipCount = list.querySelectorAll(".fb-chip").length;
|
|
879
|
+
const disabled = chipCount <= minCount;
|
|
880
|
+
list.querySelectorAll(".fb-chip-remove").forEach((btn) => {
|
|
881
|
+
btn.disabled = disabled;
|
|
639
882
|
});
|
|
640
883
|
}
|
|
641
|
-
let
|
|
642
|
-
let countDisplay = null;
|
|
884
|
+
let addUpdate = null;
|
|
643
885
|
if (!readonly) {
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
addBtn.addEventListener("mouseenter", () => {
|
|
658
|
-
addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
659
|
-
});
|
|
660
|
-
addBtn.addEventListener("mouseleave", () => {
|
|
661
|
-
addBtn.style.backgroundColor = "transparent";
|
|
662
|
-
});
|
|
663
|
-
addBtn.onclick = () => {
|
|
664
|
-
values.push(element.default || "");
|
|
665
|
-
addTextItem(element.default || "");
|
|
666
|
-
updateAddButton();
|
|
667
|
-
updateRemoveButtons();
|
|
668
|
-
};
|
|
669
|
-
countDisplay = document.createElement("span");
|
|
670
|
-
countDisplay.className = "text-sm text-gray-500";
|
|
671
|
-
addRow.appendChild(addBtn);
|
|
672
|
-
addRow.appendChild(countDisplay);
|
|
673
|
-
wrapper.appendChild(addRow);
|
|
886
|
+
const handle = createAddItemRow(
|
|
887
|
+
"text",
|
|
888
|
+
() => {
|
|
889
|
+
values.push(element.default || "");
|
|
890
|
+
addChip(element.default || "");
|
|
891
|
+
updateAddButton();
|
|
892
|
+
updateRemoveButtons();
|
|
893
|
+
},
|
|
894
|
+
{ label: element.addLabel }
|
|
895
|
+
);
|
|
896
|
+
addUpdate = handle.update;
|
|
897
|
+
mountCounterInLabel(wrapper, handle.counter);
|
|
898
|
+
wrapper.appendChild(handle.row);
|
|
674
899
|
}
|
|
675
900
|
function updateAddButton() {
|
|
676
|
-
if (
|
|
677
|
-
const addBtn = addRow.querySelector(".add-text-btn");
|
|
678
|
-
if (addBtn) {
|
|
679
|
-
const disabled = values.length >= maxCount;
|
|
680
|
-
addBtn.disabled = disabled;
|
|
681
|
-
addBtn.style.opacity = disabled ? "0.5" : "1";
|
|
682
|
-
addBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
683
|
-
}
|
|
684
|
-
countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
|
|
901
|
+
if (addUpdate) addUpdate(values.length, maxCount);
|
|
685
902
|
}
|
|
686
|
-
values.forEach((value) =>
|
|
903
|
+
values.forEach((value) => addChip(value));
|
|
687
904
|
updateAddButton();
|
|
688
905
|
updateRemoveButtons();
|
|
689
906
|
}
|
|
@@ -706,10 +923,12 @@ function validateTextElement(element, key, context) {
|
|
|
706
923
|
font-size: var(--fb-font-size-small);
|
|
707
924
|
margin-top: 0.25rem;
|
|
708
925
|
`;
|
|
709
|
-
|
|
710
|
-
|
|
926
|
+
const chipAncestor = input.closest?.(".fb-chip");
|
|
927
|
+
const anchor = chipAncestor || input;
|
|
928
|
+
if (anchor.nextSibling) {
|
|
929
|
+
anchor.parentNode?.insertBefore(errorElement, anchor.nextSibling);
|
|
711
930
|
} else {
|
|
712
|
-
|
|
931
|
+
anchor.parentNode?.appendChild(errorElement);
|
|
713
932
|
}
|
|
714
933
|
}
|
|
715
934
|
errorElement.textContent = errorMessage;
|
|
@@ -758,7 +977,7 @@ function validateTextElement(element, key, context) {
|
|
|
758
977
|
}
|
|
759
978
|
};
|
|
760
979
|
if (element.multiple) {
|
|
761
|
-
const inputs = scopeRoot.querySelectorAll(`[name^="${key}["]`);
|
|
980
|
+
const inputs = scopeRoot.querySelectorAll(`[name^="${key}\\["]`);
|
|
762
981
|
const values = [];
|
|
763
982
|
const rawValues = [];
|
|
764
983
|
inputs.forEach((input, index) => {
|
|
@@ -807,12 +1026,14 @@ function updateTextField(element, fieldPath, value, context) {
|
|
|
807
1026
|
);
|
|
808
1027
|
return;
|
|
809
1028
|
}
|
|
810
|
-
const inputs = scopeRoot.querySelectorAll(`[name^="${fieldPath}["]`);
|
|
1029
|
+
const inputs = scopeRoot.querySelectorAll(`[name^="${fieldPath}\\["]`);
|
|
811
1030
|
inputs.forEach((input, index) => {
|
|
812
1031
|
if (index < value.length) {
|
|
813
1032
|
input.value = value[index] != null ? String(value[index]) : "";
|
|
814
1033
|
input.classList.remove("invalid");
|
|
815
1034
|
input.title = "";
|
|
1035
|
+
clearFieldError(input);
|
|
1036
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
816
1037
|
}
|
|
817
1038
|
});
|
|
818
1039
|
if (value.length !== inputs.length) {
|
|
@@ -826,26 +1047,15 @@ function updateTextField(element, fieldPath, value, context) {
|
|
|
826
1047
|
input.value = value != null ? String(value) : "";
|
|
827
1048
|
input.classList.remove("invalid");
|
|
828
1049
|
input.title = "";
|
|
1050
|
+
clearFieldError(input);
|
|
1051
|
+
if (input instanceof HTMLTextAreaElement) {
|
|
1052
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1053
|
+
}
|
|
829
1054
|
}
|
|
830
1055
|
}
|
|
831
1056
|
}
|
|
832
1057
|
|
|
833
1058
|
// src/components/textarea.ts
|
|
834
|
-
function applyAutoExpand(textarea) {
|
|
835
|
-
textarea.style.overflow = "hidden";
|
|
836
|
-
textarea.style.resize = "none";
|
|
837
|
-
const lineCount = (textarea.value.match(/\n/g) || []).length + 1;
|
|
838
|
-
textarea.rows = Math.max(1, lineCount);
|
|
839
|
-
const resize = () => {
|
|
840
|
-
if (!textarea.isConnected) return;
|
|
841
|
-
textarea.style.height = "0";
|
|
842
|
-
textarea.style.height = `${textarea.scrollHeight}px`;
|
|
843
|
-
};
|
|
844
|
-
textarea.addEventListener("input", resize);
|
|
845
|
-
setTimeout(() => {
|
|
846
|
-
if (textarea.isConnected) resize();
|
|
847
|
-
}, 0);
|
|
848
|
-
}
|
|
849
1059
|
function renderTextareaElement(element, ctx, wrapper, pathKey) {
|
|
850
1060
|
const state = ctx.state;
|
|
851
1061
|
const readonly = isElementReadonly(element, state, ctx);
|
|
@@ -876,7 +1086,7 @@ function renderTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
876
1086
|
}
|
|
877
1087
|
textareaWrapper.appendChild(textareaInput);
|
|
878
1088
|
if (!readonly && (element.minLength != null || element.maxLength != null)) {
|
|
879
|
-
const counter = createCharCounter(element, textareaInput
|
|
1089
|
+
const counter = createCharCounter(element, textareaInput);
|
|
880
1090
|
textareaWrapper.appendChild(counter);
|
|
881
1091
|
}
|
|
882
1092
|
wrapper.appendChild(textareaWrapper);
|
|
@@ -932,7 +1142,7 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
932
1142
|
}
|
|
933
1143
|
textareaContainer.appendChild(textareaInput);
|
|
934
1144
|
if (!readonly && (element.minLength != null || element.maxLength != null)) {
|
|
935
|
-
const counter = createCharCounter(element, textareaInput
|
|
1145
|
+
const counter = createCharCounter(element, textareaInput);
|
|
936
1146
|
textareaContainer.appendChild(counter);
|
|
937
1147
|
}
|
|
938
1148
|
itemWrapper.appendChild(textareaContainer);
|
|
@@ -977,52 +1187,24 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
977
1187
|
removeBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
978
1188
|
});
|
|
979
1189
|
}
|
|
980
|
-
let
|
|
981
|
-
let countDisplay = null;
|
|
1190
|
+
let addUpdate = null;
|
|
982
1191
|
if (!readonly) {
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
addBtn.addEventListener("mouseenter", () => {
|
|
997
|
-
addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
998
|
-
});
|
|
999
|
-
addBtn.addEventListener("mouseleave", () => {
|
|
1000
|
-
addBtn.style.backgroundColor = "transparent";
|
|
1001
|
-
});
|
|
1002
|
-
addBtn.onclick = () => {
|
|
1003
|
-
values.push(element.default || "");
|
|
1004
|
-
addTextareaItem(element.default || "");
|
|
1005
|
-
updateAddButton();
|
|
1006
|
-
updateRemoveButtons();
|
|
1007
|
-
};
|
|
1008
|
-
countDisplay = document.createElement("span");
|
|
1009
|
-
countDisplay.className = "text-sm text-gray-500";
|
|
1010
|
-
addRow.appendChild(addBtn);
|
|
1011
|
-
addRow.appendChild(countDisplay);
|
|
1012
|
-
wrapper.appendChild(addRow);
|
|
1192
|
+
const handle = createAddItemRow(
|
|
1193
|
+
"textarea",
|
|
1194
|
+
() => {
|
|
1195
|
+
values.push(element.default || "");
|
|
1196
|
+
addTextareaItem(element.default || "");
|
|
1197
|
+
updateAddButton();
|
|
1198
|
+
updateRemoveButtons();
|
|
1199
|
+
},
|
|
1200
|
+
{ label: element.addLabel }
|
|
1201
|
+
);
|
|
1202
|
+
addUpdate = handle.update;
|
|
1203
|
+
mountCounterInLabel(wrapper, handle.counter);
|
|
1204
|
+
wrapper.appendChild(handle.row);
|
|
1013
1205
|
}
|
|
1014
1206
|
function updateAddButton() {
|
|
1015
|
-
if (
|
|
1016
|
-
const addBtn = addRow.querySelector(
|
|
1017
|
-
".add-textarea-btn"
|
|
1018
|
-
);
|
|
1019
|
-
if (addBtn) {
|
|
1020
|
-
const disabled = values.length >= maxCount;
|
|
1021
|
-
addBtn.disabled = disabled;
|
|
1022
|
-
addBtn.style.opacity = disabled ? "0.5" : "1";
|
|
1023
|
-
addBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
1024
|
-
}
|
|
1025
|
-
countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
|
|
1207
|
+
if (addUpdate) addUpdate(values.length, maxCount);
|
|
1026
1208
|
}
|
|
1027
1209
|
values.forEach((value) => addTextareaItem(value));
|
|
1028
1210
|
updateAddButton();
|
|
@@ -1054,6 +1236,89 @@ function updateTextareaField(element, fieldPath, value, context) {
|
|
|
1054
1236
|
}
|
|
1055
1237
|
|
|
1056
1238
|
// src/components/number.ts
|
|
1239
|
+
function ensureStepperStyles(doc) {
|
|
1240
|
+
const ID = "fb-number-stepper-styles";
|
|
1241
|
+
if (doc.getElementById(ID)) return;
|
|
1242
|
+
const style = doc.createElement("style");
|
|
1243
|
+
style.id = ID;
|
|
1244
|
+
style.textContent = `
|
|
1245
|
+
.fb-stepper-input::-webkit-outer-spin-button,
|
|
1246
|
+
.fb-stepper-input::-webkit-inner-spin-button {
|
|
1247
|
+
-webkit-appearance: none;
|
|
1248
|
+
margin: 0;
|
|
1249
|
+
}
|
|
1250
|
+
.fb-stepper-input { -moz-appearance: textfield; }
|
|
1251
|
+
`;
|
|
1252
|
+
doc.head.appendChild(style);
|
|
1253
|
+
}
|
|
1254
|
+
function buildStepper(input, element, readonly) {
|
|
1255
|
+
ensureStepperStyles(input.ownerDocument);
|
|
1256
|
+
const step = element.step ?? 1;
|
|
1257
|
+
const min = element.min;
|
|
1258
|
+
const max = element.max;
|
|
1259
|
+
const wrap = document.createElement("div");
|
|
1260
|
+
wrap.className = "fb-stepper";
|
|
1261
|
+
wrap.style.cssText = `
|
|
1262
|
+
display: inline-flex;
|
|
1263
|
+
align-items: stretch;
|
|
1264
|
+
border: var(--fb-border-width) solid var(--fb-border-color);
|
|
1265
|
+
border-radius: var(--fb-border-radius);
|
|
1266
|
+
overflow: hidden;
|
|
1267
|
+
background: var(--fb-background-color);
|
|
1268
|
+
`;
|
|
1269
|
+
const makeBtn = (label, delta) => {
|
|
1270
|
+
const b = document.createElement("button");
|
|
1271
|
+
b.type = "button";
|
|
1272
|
+
b.textContent = label;
|
|
1273
|
+
b.tabIndex = -1;
|
|
1274
|
+
b.style.cssText = `
|
|
1275
|
+
width: 32px;
|
|
1276
|
+
border: none;
|
|
1277
|
+
background: transparent;
|
|
1278
|
+
color: var(--fb-text-color);
|
|
1279
|
+
font-size: var(--fb-font-size);
|
|
1280
|
+
font-family: var(--fb-font-family);
|
|
1281
|
+
cursor: ${readonly ? "default" : "pointer"};
|
|
1282
|
+
user-select: none;
|
|
1283
|
+
`;
|
|
1284
|
+
if (readonly) {
|
|
1285
|
+
b.disabled = true;
|
|
1286
|
+
b.style.opacity = "0.5";
|
|
1287
|
+
} else {
|
|
1288
|
+
b.addEventListener("click", (e) => {
|
|
1289
|
+
e.preventDefault();
|
|
1290
|
+
const current = parseFloat(input.value);
|
|
1291
|
+
const base = Number.isFinite(current) ? current : min ?? element.default ?? 0;
|
|
1292
|
+
let next = parseFloat((base + delta * step).toPrecision(12));
|
|
1293
|
+
if (min != null) next = Math.max(min, next);
|
|
1294
|
+
if (max != null) next = Math.min(max, next);
|
|
1295
|
+
input.value = String(next);
|
|
1296
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1297
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
return b;
|
|
1301
|
+
};
|
|
1302
|
+
input.classList.add("fb-stepper-input");
|
|
1303
|
+
input.style.cssText = `
|
|
1304
|
+
width: 56px;
|
|
1305
|
+
border: none;
|
|
1306
|
+
border-left: var(--fb-border-width) solid var(--fb-border-color);
|
|
1307
|
+
border-right: var(--fb-border-width) solid var(--fb-border-color);
|
|
1308
|
+
padding: var(--fb-input-padding-y) 0;
|
|
1309
|
+
font-size: var(--fb-font-size);
|
|
1310
|
+
font-family: var(--fb-font-family);
|
|
1311
|
+
text-align: center;
|
|
1312
|
+
background: transparent;
|
|
1313
|
+
color: var(--fb-text-color);
|
|
1314
|
+
-moz-appearance: textfield;
|
|
1315
|
+
box-sizing: border-box;
|
|
1316
|
+
`;
|
|
1317
|
+
wrap.appendChild(makeBtn("\u2212", -1));
|
|
1318
|
+
wrap.appendChild(input);
|
|
1319
|
+
wrap.appendChild(makeBtn("+", 1));
|
|
1320
|
+
return wrap;
|
|
1321
|
+
}
|
|
1057
1322
|
function createNumberRangeHint(element, input) {
|
|
1058
1323
|
const hint = document.createElement("span");
|
|
1059
1324
|
hint.className = "number-range-hint";
|
|
@@ -1100,14 +1365,6 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1100
1365
|
inputWrapper.style.cssText = "position: relative;";
|
|
1101
1366
|
const numberInput = document.createElement("input");
|
|
1102
1367
|
numberInput.type = "number";
|
|
1103
|
-
numberInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
|
|
1104
|
-
numberInput.style.cssText = `
|
|
1105
|
-
padding: var(--fb-input-padding-y) 60px var(--fb-input-padding-y) var(--fb-input-padding-x);
|
|
1106
|
-
font-size: var(--fb-font-size);
|
|
1107
|
-
font-family: var(--fb-font-family);
|
|
1108
|
-
width: 100%;
|
|
1109
|
-
box-sizing: border-box;
|
|
1110
|
-
`;
|
|
1111
1368
|
numberInput.name = pathKey;
|
|
1112
1369
|
numberInput.placeholder = element.placeholder || "0";
|
|
1113
1370
|
if (element.min !== void 0) numberInput.min = element.min.toString();
|
|
@@ -1115,6 +1372,16 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1115
1372
|
if (element.step !== void 0) numberInput.step = element.step.toString();
|
|
1116
1373
|
numberInput.value = ctx.prefill[element.key] || element.default || "";
|
|
1117
1374
|
numberInput.readOnly = readonly;
|
|
1375
|
+
if (!element.stepper) {
|
|
1376
|
+
numberInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
|
|
1377
|
+
numberInput.style.cssText = `
|
|
1378
|
+
padding: var(--fb-input-padding-y) 60px var(--fb-input-padding-y) var(--fb-input-padding-x);
|
|
1379
|
+
font-size: var(--fb-font-size);
|
|
1380
|
+
font-family: var(--fb-font-family);
|
|
1381
|
+
width: 100%;
|
|
1382
|
+
box-sizing: border-box;
|
|
1383
|
+
`;
|
|
1384
|
+
}
|
|
1118
1385
|
if (!readonly && ctx.instance) {
|
|
1119
1386
|
const handleChange = () => {
|
|
1120
1387
|
const value = numberInput.value ? parseFloat(numberInput.value) : null;
|
|
@@ -1123,10 +1390,14 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1123
1390
|
numberInput.addEventListener("blur", handleChange);
|
|
1124
1391
|
numberInput.addEventListener("input", handleChange);
|
|
1125
1392
|
}
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
inputWrapper.appendChild(
|
|
1393
|
+
if (element.stepper) {
|
|
1394
|
+
inputWrapper.appendChild(buildStepper(numberInput, element, readonly));
|
|
1395
|
+
} else {
|
|
1396
|
+
inputWrapper.appendChild(numberInput);
|
|
1397
|
+
if (!readonly && (element.min != null || element.max != null)) {
|
|
1398
|
+
const counter = createNumberRangeHint(element, numberInput);
|
|
1399
|
+
inputWrapper.appendChild(counter);
|
|
1400
|
+
}
|
|
1130
1401
|
}
|
|
1131
1402
|
wrapper.appendChild(inputWrapper);
|
|
1132
1403
|
}
|
|
@@ -1228,50 +1499,24 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1228
1499
|
removeBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
1229
1500
|
});
|
|
1230
1501
|
}
|
|
1231
|
-
let
|
|
1232
|
-
let countDisplay = null;
|
|
1502
|
+
let addUpdate = null;
|
|
1233
1503
|
if (!readonly) {
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
addBtn.addEventListener("mouseenter", () => {
|
|
1248
|
-
addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
1249
|
-
});
|
|
1250
|
-
addBtn.addEventListener("mouseleave", () => {
|
|
1251
|
-
addBtn.style.backgroundColor = "transparent";
|
|
1252
|
-
});
|
|
1253
|
-
addBtn.onclick = () => {
|
|
1254
|
-
values.push(element.default || "");
|
|
1255
|
-
addNumberItem(element.default || "");
|
|
1256
|
-
updateAddButton();
|
|
1257
|
-
updateRemoveButtons();
|
|
1258
|
-
};
|
|
1259
|
-
countDisplay = document.createElement("span");
|
|
1260
|
-
countDisplay.className = "text-sm text-gray-500";
|
|
1261
|
-
addRow.appendChild(addBtn);
|
|
1262
|
-
addRow.appendChild(countDisplay);
|
|
1263
|
-
wrapper.appendChild(addRow);
|
|
1504
|
+
const handle = createAddItemRow(
|
|
1505
|
+
"number",
|
|
1506
|
+
() => {
|
|
1507
|
+
values.push(element.default || "");
|
|
1508
|
+
addNumberItem(element.default || "");
|
|
1509
|
+
updateAddButton();
|
|
1510
|
+
updateRemoveButtons();
|
|
1511
|
+
},
|
|
1512
|
+
{ label: element.addLabel }
|
|
1513
|
+
);
|
|
1514
|
+
addUpdate = handle.update;
|
|
1515
|
+
mountCounterInLabel(wrapper, handle.counter);
|
|
1516
|
+
wrapper.appendChild(handle.row);
|
|
1264
1517
|
}
|
|
1265
1518
|
function updateAddButton() {
|
|
1266
|
-
if (
|
|
1267
|
-
const addBtn = addRow.querySelector(".add-number-btn");
|
|
1268
|
-
if (addBtn) {
|
|
1269
|
-
const disabled = values.length >= maxCount;
|
|
1270
|
-
addBtn.disabled = disabled;
|
|
1271
|
-
addBtn.style.opacity = disabled ? "0.5" : "1";
|
|
1272
|
-
addBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
1273
|
-
}
|
|
1274
|
-
countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
|
|
1519
|
+
if (addUpdate) addUpdate(values.length, maxCount);
|
|
1275
1520
|
}
|
|
1276
1521
|
values.forEach((value) => addNumberItem(value));
|
|
1277
1522
|
updateAddButton();
|
|
@@ -1406,13 +1651,14 @@ function updateNumberField(element, fieldPath, value, context) {
|
|
|
1406
1651
|
return;
|
|
1407
1652
|
}
|
|
1408
1653
|
const inputs = scopeRoot.querySelectorAll(
|
|
1409
|
-
`[name^="${fieldPath}["]`
|
|
1654
|
+
`[name^="${fieldPath}\\["]`
|
|
1410
1655
|
);
|
|
1411
1656
|
inputs.forEach((input, index) => {
|
|
1412
1657
|
if (index < value.length) {
|
|
1413
1658
|
input.value = value[index] != null ? String(value[index]) : "";
|
|
1414
1659
|
input.classList.remove("invalid");
|
|
1415
1660
|
input.title = "";
|
|
1661
|
+
clearFieldError(input);
|
|
1416
1662
|
}
|
|
1417
1663
|
});
|
|
1418
1664
|
if (value.length !== inputs.length) {
|
|
@@ -1428,6 +1674,7 @@ function updateNumberField(element, fieldPath, value, context) {
|
|
|
1428
1674
|
input.value = value != null ? String(value) : "";
|
|
1429
1675
|
input.classList.remove("invalid");
|
|
1430
1676
|
input.title = "";
|
|
1677
|
+
clearFieldError(input);
|
|
1431
1678
|
}
|
|
1432
1679
|
}
|
|
1433
1680
|
}
|
|
@@ -1556,51 +1803,25 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
1556
1803
|
removeBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
1557
1804
|
});
|
|
1558
1805
|
}
|
|
1559
|
-
let
|
|
1560
|
-
let countDisplay = null;
|
|
1806
|
+
let addUpdate = null;
|
|
1561
1807
|
if (!readonly) {
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
1577
|
-
});
|
|
1578
|
-
addBtn.addEventListener("mouseleave", () => {
|
|
1579
|
-
addBtn.style.backgroundColor = "transparent";
|
|
1580
|
-
});
|
|
1581
|
-
addBtn.onclick = () => {
|
|
1582
|
-
const defaultValue = element.default || element.options?.[0]?.value || "";
|
|
1583
|
-
values.push(defaultValue);
|
|
1584
|
-
addSelectItem(defaultValue);
|
|
1585
|
-
updateAddButton();
|
|
1586
|
-
updateRemoveButtons();
|
|
1587
|
-
};
|
|
1588
|
-
countDisplay = document.createElement("span");
|
|
1589
|
-
countDisplay.className = "text-sm text-gray-500";
|
|
1590
|
-
addRow.appendChild(addBtn);
|
|
1591
|
-
addRow.appendChild(countDisplay);
|
|
1592
|
-
wrapper.appendChild(addRow);
|
|
1808
|
+
const handle = createAddItemRow(
|
|
1809
|
+
"select",
|
|
1810
|
+
() => {
|
|
1811
|
+
const defaultValue = element.default || element.options?.[0]?.value || "";
|
|
1812
|
+
values.push(defaultValue);
|
|
1813
|
+
addSelectItem(defaultValue);
|
|
1814
|
+
updateAddButton();
|
|
1815
|
+
updateRemoveButtons();
|
|
1816
|
+
},
|
|
1817
|
+
{ label: element.addLabel }
|
|
1818
|
+
);
|
|
1819
|
+
addUpdate = handle.update;
|
|
1820
|
+
mountCounterInLabel(wrapper, handle.counter);
|
|
1821
|
+
wrapper.appendChild(handle.row);
|
|
1593
1822
|
}
|
|
1594
1823
|
function updateAddButton() {
|
|
1595
|
-
if (
|
|
1596
|
-
const addBtn = addRow.querySelector(".add-select-btn");
|
|
1597
|
-
if (addBtn) {
|
|
1598
|
-
const disabled = values.length >= maxCount;
|
|
1599
|
-
addBtn.disabled = disabled;
|
|
1600
|
-
addBtn.style.opacity = disabled ? "0.5" : "1";
|
|
1601
|
-
addBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
1602
|
-
}
|
|
1603
|
-
countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
|
|
1824
|
+
if (addUpdate) addUpdate(values.length, maxCount);
|
|
1604
1825
|
}
|
|
1605
1826
|
values.forEach((value) => addSelectItem(value));
|
|
1606
1827
|
updateAddButton();
|
|
@@ -1665,7 +1886,7 @@ function validateSelectElement(element, key, context) {
|
|
|
1665
1886
|
};
|
|
1666
1887
|
if ("multiple" in element && element.multiple) {
|
|
1667
1888
|
const inputs = scopeRoot.querySelectorAll(
|
|
1668
|
-
`[name^="${key}["]`
|
|
1889
|
+
`[name^="${key}\\["]`
|
|
1669
1890
|
);
|
|
1670
1891
|
const values = [];
|
|
1671
1892
|
inputs.forEach((input) => {
|
|
@@ -1701,7 +1922,7 @@ function updateSelectField(element, fieldPath, value, context) {
|
|
|
1701
1922
|
return;
|
|
1702
1923
|
}
|
|
1703
1924
|
const selects = scopeRoot.querySelectorAll(
|
|
1704
|
-
`[name^="${fieldPath}["]`
|
|
1925
|
+
`[name^="${fieldPath}\\["]`
|
|
1705
1926
|
);
|
|
1706
1927
|
selects.forEach((select, index) => {
|
|
1707
1928
|
if (index < value.length) {
|
|
@@ -1712,6 +1933,7 @@ function updateSelectField(element, fieldPath, value, context) {
|
|
|
1712
1933
|
});
|
|
1713
1934
|
select.classList.remove("invalid");
|
|
1714
1935
|
select.title = "";
|
|
1936
|
+
clearFieldError(select);
|
|
1715
1937
|
}
|
|
1716
1938
|
});
|
|
1717
1939
|
if (value.length !== selects.length) {
|
|
@@ -1731,72 +1953,147 @@ function updateSelectField(element, fieldPath, value, context) {
|
|
|
1731
1953
|
});
|
|
1732
1954
|
select.classList.remove("invalid");
|
|
1733
1955
|
select.title = "";
|
|
1956
|
+
clearFieldError(select);
|
|
1734
1957
|
}
|
|
1735
1958
|
}
|
|
1736
1959
|
}
|
|
1737
1960
|
|
|
1738
1961
|
// src/components/switcher.ts
|
|
1739
|
-
function applySelectedStyle(btn) {
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1962
|
+
function applySelectedStyle(btn, isPreset) {
|
|
1963
|
+
if (isPreset) {
|
|
1964
|
+
btn.style.backgroundColor = "var(--fb-primary-soft-color)";
|
|
1965
|
+
btn.style.color = "var(--fb-primary-color)";
|
|
1966
|
+
btn.style.borderColor = "var(--fb-primary-color)";
|
|
1967
|
+
} else {
|
|
1968
|
+
btn.style.backgroundColor = "var(--fb-primary-color)";
|
|
1969
|
+
btn.style.color = "#ffffff";
|
|
1970
|
+
btn.style.borderColor = "var(--fb-primary-color)";
|
|
1971
|
+
}
|
|
1743
1972
|
}
|
|
1744
|
-
function applyUnselectedStyle(btn) {
|
|
1745
|
-
btn.style.backgroundColor = "transparent";
|
|
1973
|
+
function applyUnselectedStyle(btn, isPreset) {
|
|
1974
|
+
btn.style.backgroundColor = isPreset ? "var(--fb-background-color)" : "transparent";
|
|
1746
1975
|
btn.style.color = "var(--fb-text-color)";
|
|
1747
1976
|
btn.style.borderColor = "var(--fb-border-color)";
|
|
1748
1977
|
}
|
|
1978
|
+
function isPresetButton(btn) {
|
|
1979
|
+
return btn.classList.contains("fb-switcher-preset");
|
|
1980
|
+
}
|
|
1981
|
+
function buildPresetCard(option, readonly) {
|
|
1982
|
+
const btn = document.createElement("button");
|
|
1983
|
+
btn.type = "button";
|
|
1984
|
+
btn.className = "fb-switcher-btn fb-switcher-preset";
|
|
1985
|
+
btn.dataset.value = option.value;
|
|
1986
|
+
btn.style.cssText = `
|
|
1987
|
+
display: inline-flex;
|
|
1988
|
+
align-items: center;
|
|
1989
|
+
gap: 8px;
|
|
1990
|
+
padding: 7px 12px 7px 10px;
|
|
1991
|
+
border-width: var(--fb-border-width);
|
|
1992
|
+
border-style: solid;
|
|
1993
|
+
border-radius: 999px;
|
|
1994
|
+
background: var(--fb-background-color);
|
|
1995
|
+
font-size: var(--fb-font-size);
|
|
1996
|
+
font-family: var(--fb-font-family);
|
|
1997
|
+
line-height: 1.25;
|
|
1998
|
+
cursor: ${readonly ? "default" : "pointer"};
|
|
1999
|
+
transition: background-color var(--fb-transition-duration), color var(--fb-transition-duration), border-color var(--fb-transition-duration);
|
|
2000
|
+
outline: none;
|
|
2001
|
+
`;
|
|
2002
|
+
if (option.iconUrl) {
|
|
2003
|
+
const icon = document.createElement("img");
|
|
2004
|
+
icon.className = "fb-switcher-icon";
|
|
2005
|
+
icon.src = option.iconUrl;
|
|
2006
|
+
icon.alt = "";
|
|
2007
|
+
icon.setAttribute("aria-hidden", "true");
|
|
2008
|
+
icon.style.cssText = `
|
|
2009
|
+
display: block;
|
|
2010
|
+
flex: 0 0 auto;
|
|
2011
|
+
width: 20px;
|
|
2012
|
+
height: 20px;
|
|
2013
|
+
object-fit: contain;
|
|
2014
|
+
`;
|
|
2015
|
+
btn.appendChild(icon);
|
|
2016
|
+
}
|
|
2017
|
+
const name = document.createElement("span");
|
|
2018
|
+
name.className = "fb-switcher-name";
|
|
2019
|
+
name.textContent = option.label;
|
|
2020
|
+
name.style.cssText = "font-weight: 600;";
|
|
2021
|
+
btn.appendChild(name);
|
|
2022
|
+
if (option.subtitle) {
|
|
2023
|
+
const sub = document.createElement("span");
|
|
2024
|
+
sub.className = "fb-switcher-subtitle";
|
|
2025
|
+
sub.textContent = option.subtitle;
|
|
2026
|
+
sub.style.cssText = `
|
|
2027
|
+
font-size: var(--fb-font-size-small);
|
|
2028
|
+
opacity: 0.7;
|
|
2029
|
+
font-variant-numeric: tabular-nums;
|
|
2030
|
+
`;
|
|
2031
|
+
btn.appendChild(sub);
|
|
2032
|
+
}
|
|
2033
|
+
return btn;
|
|
2034
|
+
}
|
|
1749
2035
|
function buildSegmentedGroup(element, currentValue, hiddenInput, readonly, onChange) {
|
|
1750
2036
|
const options = element.options || [];
|
|
2037
|
+
const isPresetMode = options.some((o) => o.subtitle || o.iconUrl);
|
|
1751
2038
|
const group = document.createElement("div");
|
|
1752
2039
|
group.className = "fb-switcher-group";
|
|
1753
|
-
group.style.cssText = `
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
2040
|
+
group.style.cssText = isPresetMode ? `
|
|
2041
|
+
display: flex;
|
|
2042
|
+
flex-direction: row;
|
|
2043
|
+
flex-wrap: wrap;
|
|
2044
|
+
gap: 6px;
|
|
2045
|
+
` : `
|
|
2046
|
+
display: inline-flex;
|
|
2047
|
+
flex-direction: row;
|
|
2048
|
+
flex-wrap: nowrap;
|
|
2049
|
+
`;
|
|
1758
2050
|
const buttons = [];
|
|
1759
2051
|
options.forEach((option, index) => {
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
btn.dataset.value = option.value;
|
|
1764
|
-
btn.textContent = option.label;
|
|
1765
|
-
btn.style.cssText = `
|
|
1766
|
-
padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
|
|
1767
|
-
font-size: var(--fb-font-size);
|
|
1768
|
-
border-width: var(--fb-border-width);
|
|
1769
|
-
border-style: solid;
|
|
1770
|
-
cursor: ${readonly ? "default" : "pointer"};
|
|
1771
|
-
transition: background-color var(--fb-transition-duration), color var(--fb-transition-duration), border-color var(--fb-transition-duration);
|
|
1772
|
-
white-space: nowrap;
|
|
1773
|
-
line-height: 1.25;
|
|
1774
|
-
outline: none;
|
|
1775
|
-
`;
|
|
1776
|
-
if (options.length === 1) {
|
|
1777
|
-
btn.style.borderRadius = "var(--fb-border-radius)";
|
|
1778
|
-
} else if (index === 0) {
|
|
1779
|
-
btn.style.borderRadius = "var(--fb-border-radius) 0 0 var(--fb-border-radius)";
|
|
1780
|
-
btn.style.borderRightWidth = "0";
|
|
1781
|
-
} else if (index === options.length - 1) {
|
|
1782
|
-
btn.style.borderRadius = "0 var(--fb-border-radius) var(--fb-border-radius) 0";
|
|
2052
|
+
let btn;
|
|
2053
|
+
if (isPresetMode) {
|
|
2054
|
+
btn = buildPresetCard(option, readonly);
|
|
1783
2055
|
} else {
|
|
1784
|
-
btn
|
|
1785
|
-
btn.
|
|
2056
|
+
btn = document.createElement("button");
|
|
2057
|
+
btn.type = "button";
|
|
2058
|
+
btn.className = "fb-switcher-btn";
|
|
2059
|
+
btn.dataset.value = option.value;
|
|
2060
|
+
btn.textContent = option.label;
|
|
2061
|
+
btn.style.cssText = `
|
|
2062
|
+
padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
|
|
2063
|
+
font-size: var(--fb-font-size);
|
|
2064
|
+
border-width: var(--fb-border-width);
|
|
2065
|
+
border-style: solid;
|
|
2066
|
+
cursor: ${readonly ? "default" : "pointer"};
|
|
2067
|
+
transition: background-color var(--fb-transition-duration), color var(--fb-transition-duration), border-color var(--fb-transition-duration);
|
|
2068
|
+
white-space: nowrap;
|
|
2069
|
+
line-height: 1.25;
|
|
2070
|
+
outline: none;
|
|
2071
|
+
`;
|
|
2072
|
+
if (options.length === 1) {
|
|
2073
|
+
btn.style.borderRadius = "var(--fb-border-radius)";
|
|
2074
|
+
} else if (index === 0) {
|
|
2075
|
+
btn.style.borderRadius = "var(--fb-border-radius) 0 0 var(--fb-border-radius)";
|
|
2076
|
+
btn.style.borderRightWidth = "0";
|
|
2077
|
+
} else if (index === options.length - 1) {
|
|
2078
|
+
btn.style.borderRadius = "0 var(--fb-border-radius) var(--fb-border-radius) 0";
|
|
2079
|
+
} else {
|
|
2080
|
+
btn.style.borderRadius = "0";
|
|
2081
|
+
btn.style.borderRightWidth = "0";
|
|
2082
|
+
}
|
|
1786
2083
|
}
|
|
1787
2084
|
if (option.value === currentValue) {
|
|
1788
|
-
applySelectedStyle(btn);
|
|
2085
|
+
applySelectedStyle(btn, isPresetMode);
|
|
1789
2086
|
} else {
|
|
1790
|
-
applyUnselectedStyle(btn);
|
|
2087
|
+
applyUnselectedStyle(btn, isPresetMode);
|
|
1791
2088
|
}
|
|
1792
2089
|
if (!readonly) {
|
|
1793
2090
|
btn.addEventListener("click", () => {
|
|
1794
2091
|
hiddenInput.value = option.value;
|
|
1795
2092
|
buttons.forEach((b) => {
|
|
1796
2093
|
if (b.dataset.value === option.value) {
|
|
1797
|
-
applySelectedStyle(b);
|
|
2094
|
+
applySelectedStyle(b, isPresetMode);
|
|
1798
2095
|
} else {
|
|
1799
|
-
applyUnselectedStyle(b);
|
|
2096
|
+
applyUnselectedStyle(b, isPresetMode);
|
|
1800
2097
|
}
|
|
1801
2098
|
});
|
|
1802
2099
|
if (onChange) {
|
|
@@ -1810,7 +2107,7 @@ function buildSegmentedGroup(element, currentValue, hiddenInput, readonly, onCha
|
|
|
1810
2107
|
});
|
|
1811
2108
|
btn.addEventListener("mouseleave", () => {
|
|
1812
2109
|
if (hiddenInput.value !== option.value) {
|
|
1813
|
-
btn.style.backgroundColor = "transparent";
|
|
2110
|
+
btn.style.backgroundColor = isPresetMode ? "var(--fb-background-color)" : "transparent";
|
|
1814
2111
|
}
|
|
1815
2112
|
});
|
|
1816
2113
|
}
|
|
@@ -1943,53 +2240,25 @@ function renderMultipleSwitcherElement(element, ctx, wrapper, pathKey) {
|
|
|
1943
2240
|
removeBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
1944
2241
|
});
|
|
1945
2242
|
}
|
|
1946
|
-
let
|
|
1947
|
-
let countDisplay = null;
|
|
2243
|
+
let addUpdate = null;
|
|
1948
2244
|
if (!readonly) {
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
1964
|
-
});
|
|
1965
|
-
addBtn.addEventListener("mouseleave", () => {
|
|
1966
|
-
addBtn.style.backgroundColor = "transparent";
|
|
1967
|
-
});
|
|
1968
|
-
addBtn.onclick = () => {
|
|
1969
|
-
const defaultValue = element.default || element.options?.[0]?.value || "";
|
|
1970
|
-
values.push(defaultValue);
|
|
1971
|
-
addSwitcherItem(defaultValue);
|
|
1972
|
-
updateAddButton();
|
|
1973
|
-
updateRemoveButtons();
|
|
1974
|
-
};
|
|
1975
|
-
countDisplay = document.createElement("span");
|
|
1976
|
-
countDisplay.className = "text-sm text-gray-500";
|
|
1977
|
-
addRow.appendChild(addBtn);
|
|
1978
|
-
addRow.appendChild(countDisplay);
|
|
1979
|
-
wrapper.appendChild(addRow);
|
|
2245
|
+
const handle = createAddItemRow(
|
|
2246
|
+
"switcher",
|
|
2247
|
+
() => {
|
|
2248
|
+
const defaultValue = element.default || element.options?.[0]?.value || "";
|
|
2249
|
+
values.push(defaultValue);
|
|
2250
|
+
addSwitcherItem(defaultValue);
|
|
2251
|
+
updateAddButton();
|
|
2252
|
+
updateRemoveButtons();
|
|
2253
|
+
},
|
|
2254
|
+
{ label: element.addLabel }
|
|
2255
|
+
);
|
|
2256
|
+
addUpdate = handle.update;
|
|
2257
|
+
mountCounterInLabel(wrapper, handle.counter);
|
|
2258
|
+
wrapper.appendChild(handle.row);
|
|
1980
2259
|
}
|
|
1981
2260
|
function updateAddButton() {
|
|
1982
|
-
if (
|
|
1983
|
-
const addBtn = addRow.querySelector(
|
|
1984
|
-
".add-switcher-btn"
|
|
1985
|
-
);
|
|
1986
|
-
if (addBtn) {
|
|
1987
|
-
const disabled = values.length >= maxCount;
|
|
1988
|
-
addBtn.disabled = disabled;
|
|
1989
|
-
addBtn.style.opacity = disabled ? "0.5" : "1";
|
|
1990
|
-
addBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
1991
|
-
}
|
|
1992
|
-
countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
|
|
2261
|
+
if (addUpdate) addUpdate(values.length, maxCount);
|
|
1993
2262
|
}
|
|
1994
2263
|
values.forEach((value) => addSwitcherItem(value));
|
|
1995
2264
|
updateAddButton();
|
|
@@ -2057,7 +2326,7 @@ function validateSwitcherElement(element, key, context) {
|
|
|
2057
2326
|
);
|
|
2058
2327
|
if ("multiple" in element && element.multiple) {
|
|
2059
2328
|
const inputs = scopeRoot.querySelectorAll(
|
|
2060
|
-
`input[type="hidden"][name^="${key}["]`
|
|
2329
|
+
`input[type="hidden"][name^="${key}\\["]`
|
|
2061
2330
|
);
|
|
2062
2331
|
const values = [];
|
|
2063
2332
|
inputs.forEach((input) => {
|
|
@@ -2104,7 +2373,7 @@ function updateSwitcherField(element, fieldPath, value, context) {
|
|
|
2104
2373
|
return;
|
|
2105
2374
|
}
|
|
2106
2375
|
const inputs = scopeRoot.querySelectorAll(
|
|
2107
|
-
`input[type="hidden"][name^="${fieldPath}["]`
|
|
2376
|
+
`input[type="hidden"][name^="${fieldPath}\\["]`
|
|
2108
2377
|
);
|
|
2109
2378
|
inputs.forEach((input, index) => {
|
|
2110
2379
|
if (index < value.length) {
|
|
@@ -2113,15 +2382,17 @@ function updateSwitcherField(element, fieldPath, value, context) {
|
|
|
2113
2382
|
const group = input.parentElement?.querySelector(".fb-switcher-group");
|
|
2114
2383
|
if (group) {
|
|
2115
2384
|
group.querySelectorAll(".fb-switcher-btn").forEach((btn) => {
|
|
2385
|
+
const isPreset = isPresetButton(btn);
|
|
2116
2386
|
if (btn.dataset.value === newVal) {
|
|
2117
|
-
applySelectedStyle(btn);
|
|
2387
|
+
applySelectedStyle(btn, isPreset);
|
|
2118
2388
|
} else {
|
|
2119
|
-
applyUnselectedStyle(btn);
|
|
2389
|
+
applyUnselectedStyle(btn, isPreset);
|
|
2120
2390
|
}
|
|
2121
2391
|
});
|
|
2122
2392
|
}
|
|
2123
2393
|
input.classList.remove("invalid");
|
|
2124
2394
|
input.title = "";
|
|
2395
|
+
clearFieldError(input);
|
|
2125
2396
|
}
|
|
2126
2397
|
});
|
|
2127
2398
|
if (value.length !== inputs.length) {
|
|
@@ -2139,16 +2410,208 @@ function updateSwitcherField(element, fieldPath, value, context) {
|
|
|
2139
2410
|
const group = input.parentElement?.querySelector(".fb-switcher-group");
|
|
2140
2411
|
if (group) {
|
|
2141
2412
|
group.querySelectorAll(".fb-switcher-btn").forEach((btn) => {
|
|
2413
|
+
const isPreset = isPresetButton(btn);
|
|
2142
2414
|
if (btn.dataset.value === newVal) {
|
|
2143
|
-
applySelectedStyle(btn);
|
|
2415
|
+
applySelectedStyle(btn, isPreset);
|
|
2144
2416
|
} else {
|
|
2145
|
-
applyUnselectedStyle(btn);
|
|
2417
|
+
applyUnselectedStyle(btn, isPreset);
|
|
2146
2418
|
}
|
|
2147
2419
|
});
|
|
2148
2420
|
}
|
|
2149
2421
|
input.classList.remove("invalid");
|
|
2150
2422
|
input.title = "";
|
|
2423
|
+
clearFieldError(input);
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
// src/components/boolean.ts
|
|
2429
|
+
var TOGGLE_W = 36;
|
|
2430
|
+
var TOGGLE_H = 20;
|
|
2431
|
+
var KNOB = 16;
|
|
2432
|
+
function ensureStyles(doc) {
|
|
2433
|
+
const ID = "fb-boolean-styles";
|
|
2434
|
+
if (doc.getElementById(ID)) return;
|
|
2435
|
+
const style = doc.createElement("style");
|
|
2436
|
+
style.id = ID;
|
|
2437
|
+
style.textContent = `
|
|
2438
|
+
.fb-toggle {
|
|
2439
|
+
position: relative;
|
|
2440
|
+
display: inline-block;
|
|
2441
|
+
width: ${TOGGLE_W}px;
|
|
2442
|
+
height: ${TOGGLE_H}px;
|
|
2443
|
+
border-radius: ${TOGGLE_H}px;
|
|
2444
|
+
background: var(--fb-border-color);
|
|
2445
|
+
transition: background-color var(--fb-transition-duration);
|
|
2446
|
+
flex-shrink: 0;
|
|
2447
|
+
}
|
|
2448
|
+
.fb-toggle::after {
|
|
2449
|
+
content: "";
|
|
2450
|
+
position: absolute;
|
|
2451
|
+
top: ${(TOGGLE_H - KNOB) / 2}px;
|
|
2452
|
+
left: ${(TOGGLE_H - KNOB) / 2}px;
|
|
2453
|
+
width: ${KNOB}px;
|
|
2454
|
+
height: ${KNOB}px;
|
|
2455
|
+
border-radius: 50%;
|
|
2456
|
+
background: #ffffff;
|
|
2457
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
|
2458
|
+
transition: transform var(--fb-transition-duration);
|
|
2459
|
+
}
|
|
2460
|
+
.fb-toggle.fb-on {
|
|
2461
|
+
background: var(--fb-primary-color);
|
|
2462
|
+
}
|
|
2463
|
+
.fb-toggle.fb-on::after {
|
|
2464
|
+
transform: translateX(${TOGGLE_W - KNOB - (TOGGLE_H - KNOB)}px);
|
|
2465
|
+
}
|
|
2466
|
+
.fb-toggle-row {
|
|
2467
|
+
display: flex;
|
|
2468
|
+
align-items: center;
|
|
2469
|
+
gap: 12px;
|
|
2470
|
+
padding: 12px 14px;
|
|
2471
|
+
background: var(--fb-surface-soft-color);
|
|
2472
|
+
border: var(--fb-border-width) solid var(--fb-border-color);
|
|
2473
|
+
border-radius: var(--fb-border-radius);
|
|
2474
|
+
cursor: pointer;
|
|
2475
|
+
user-select: none;
|
|
2476
|
+
}
|
|
2477
|
+
.fb-toggle-row[aria-disabled="true"] {
|
|
2478
|
+
cursor: default;
|
|
2479
|
+
opacity: 0.7;
|
|
2480
|
+
}
|
|
2481
|
+
.fb-toggle-row:focus-visible {
|
|
2482
|
+
outline: var(--fb-focus-ring-width) solid var(--fb-focus-ring-color);
|
|
2483
|
+
outline-offset: var(--fb-focus-ring-offset);
|
|
2484
|
+
}
|
|
2485
|
+
.fb-toggle-text { flex: 1; min-width: 0; }
|
|
2486
|
+
.fb-toggle-title {
|
|
2487
|
+
display: flex;
|
|
2488
|
+
align-items: center;
|
|
2489
|
+
gap: 4px;
|
|
2490
|
+
font-size: var(--fb-font-size);
|
|
2491
|
+
font-weight: 500;
|
|
2492
|
+
color: var(--fb-text-color);
|
|
2493
|
+
line-height: 1.3;
|
|
2494
|
+
}
|
|
2495
|
+
.fb-toggle-subtitle {
|
|
2496
|
+
font-size: var(--fb-font-size-small);
|
|
2497
|
+
color: var(--fb-text-secondary-color);
|
|
2498
|
+
margin-top: 2px;
|
|
2499
|
+
line-height: 1.35;
|
|
2500
|
+
}
|
|
2501
|
+
.fb-toggle-info {
|
|
2502
|
+
flex: 0 0 14px;
|
|
2503
|
+
display: inline-flex;
|
|
2504
|
+
align-items: center;
|
|
2505
|
+
justify-content: center;
|
|
2506
|
+
width: 14px;
|
|
2507
|
+
height: 14px;
|
|
2508
|
+
border-radius: 50%;
|
|
2509
|
+
background: var(--fb-border-color);
|
|
2510
|
+
color: #fff;
|
|
2511
|
+
font-size: 10px;
|
|
2512
|
+
font-weight: 700;
|
|
2513
|
+
font-style: italic;
|
|
2514
|
+
font-family: serif;
|
|
2515
|
+
cursor: help;
|
|
2151
2516
|
}
|
|
2517
|
+
`;
|
|
2518
|
+
doc.head.appendChild(style);
|
|
2519
|
+
}
|
|
2520
|
+
function parseBool(v) {
|
|
2521
|
+
if (typeof v === "boolean") return v;
|
|
2522
|
+
if (typeof v === "string") return v === "true" || v === "on" || v === "1";
|
|
2523
|
+
return false;
|
|
2524
|
+
}
|
|
2525
|
+
function renderBooleanElement(element, ctx, wrapper, pathKey) {
|
|
2526
|
+
ensureStyles(document);
|
|
2527
|
+
const state = ctx.state;
|
|
2528
|
+
const readonly = isElementReadonly(element, state, ctx);
|
|
2529
|
+
const prefillRaw = ctx.prefill[element.key];
|
|
2530
|
+
const initial = prefillRaw !== void 0 ? parseBool(prefillRaw) : parseBool(element.default);
|
|
2531
|
+
const hiddenInput = document.createElement("input");
|
|
2532
|
+
hiddenInput.type = "hidden";
|
|
2533
|
+
hiddenInput.name = pathKey;
|
|
2534
|
+
hiddenInput.value = initial ? "true" : "false";
|
|
2535
|
+
const row = document.createElement("div");
|
|
2536
|
+
row.className = "fb-toggle-row";
|
|
2537
|
+
row.setAttribute("role", "switch");
|
|
2538
|
+
row.setAttribute("aria-checked", initial ? "true" : "false");
|
|
2539
|
+
if (readonly) {
|
|
2540
|
+
row.setAttribute("aria-disabled", "true");
|
|
2541
|
+
} else {
|
|
2542
|
+
row.tabIndex = 0;
|
|
2543
|
+
}
|
|
2544
|
+
const pill = document.createElement("span");
|
|
2545
|
+
pill.className = "fb-toggle" + (initial ? " fb-on" : "");
|
|
2546
|
+
pill.setAttribute("aria-hidden", "true");
|
|
2547
|
+
row.appendChild(pill);
|
|
2548
|
+
const textBlock = document.createElement("div");
|
|
2549
|
+
textBlock.className = "fb-toggle-text";
|
|
2550
|
+
const titleEl = document.createElement("div");
|
|
2551
|
+
titleEl.className = "fb-toggle-title";
|
|
2552
|
+
titleEl.appendChild(document.createTextNode(element.label ?? ""));
|
|
2553
|
+
if (element.description) {
|
|
2554
|
+
const info = document.createElement("span");
|
|
2555
|
+
info.className = "fb-toggle-info";
|
|
2556
|
+
info.textContent = "i";
|
|
2557
|
+
info.title = element.description;
|
|
2558
|
+
titleEl.appendChild(info);
|
|
2559
|
+
}
|
|
2560
|
+
textBlock.appendChild(titleEl);
|
|
2561
|
+
if (element.hint) {
|
|
2562
|
+
const subtitle = document.createElement("div");
|
|
2563
|
+
subtitle.className = "fb-toggle-subtitle";
|
|
2564
|
+
subtitle.textContent = element.hint;
|
|
2565
|
+
textBlock.appendChild(subtitle);
|
|
2566
|
+
}
|
|
2567
|
+
row.appendChild(textBlock);
|
|
2568
|
+
if (!readonly) {
|
|
2569
|
+
const toggle = () => {
|
|
2570
|
+
const next = hiddenInput.value !== "true";
|
|
2571
|
+
hiddenInput.value = next ? "true" : "false";
|
|
2572
|
+
pill.classList.toggle("fb-on", next);
|
|
2573
|
+
row.setAttribute("aria-checked", next ? "true" : "false");
|
|
2574
|
+
if (ctx.instance) ctx.instance.triggerOnChange(pathKey, next);
|
|
2575
|
+
};
|
|
2576
|
+
row.addEventListener("click", (e) => {
|
|
2577
|
+
if (e.target?.classList.contains("fb-toggle-info")) {
|
|
2578
|
+
return;
|
|
2579
|
+
}
|
|
2580
|
+
toggle();
|
|
2581
|
+
});
|
|
2582
|
+
row.addEventListener("keydown", (e) => {
|
|
2583
|
+
if (e.key === " " || e.key === "Enter") {
|
|
2584
|
+
e.preventDefault();
|
|
2585
|
+
toggle();
|
|
2586
|
+
}
|
|
2587
|
+
});
|
|
2588
|
+
}
|
|
2589
|
+
wrapper.appendChild(hiddenInput);
|
|
2590
|
+
wrapper.appendChild(row);
|
|
2591
|
+
}
|
|
2592
|
+
function validateBooleanElement(element, key, context) {
|
|
2593
|
+
const { scopeRoot } = context;
|
|
2594
|
+
const input = scopeRoot.querySelector(
|
|
2595
|
+
`input[type="hidden"][name="${key}"]`
|
|
2596
|
+
);
|
|
2597
|
+
const raw = input?.value ?? "";
|
|
2598
|
+
const value = parseBool(raw);
|
|
2599
|
+
const errors = [];
|
|
2600
|
+
return { value, errors };
|
|
2601
|
+
}
|
|
2602
|
+
function updateBooleanField(_element, fieldPath, value, context) {
|
|
2603
|
+
const { scopeRoot } = context;
|
|
2604
|
+
const input = scopeRoot.querySelector(
|
|
2605
|
+
`input[type="hidden"][name="${fieldPath}"]`
|
|
2606
|
+
);
|
|
2607
|
+
if (!input) return;
|
|
2608
|
+
const bool = parseBool(value);
|
|
2609
|
+
input.value = bool ? "true" : "false";
|
|
2610
|
+
const row = input.parentElement?.querySelector(".fb-toggle-row");
|
|
2611
|
+
if (row) {
|
|
2612
|
+
row.setAttribute("aria-checked", bool ? "true" : "false");
|
|
2613
|
+
const pill = row.querySelector(".fb-toggle");
|
|
2614
|
+
if (pill) pill.classList.toggle("fb-on", bool);
|
|
2152
2615
|
}
|
|
2153
2616
|
}
|
|
2154
2617
|
|
|
@@ -2252,14 +2715,20 @@ function ensureFileStyles() {
|
|
|
2252
2715
|
}
|
|
2253
2716
|
|
|
2254
2717
|
/* \u2500\u2500\u2500 Wide single-file add tile (empty state) \u2500\u2500\u2500 */
|
|
2718
|
+
/* Flex-wraps: side-by-side when wide enough, stacks upload/library
|
|
2719
|
+
vertically when narrow (e.g. inside a 50/50 container column). */
|
|
2255
2720
|
.fb-wide-tile {
|
|
2256
2721
|
width: 100%;
|
|
2722
|
+
box-sizing: border-box;
|
|
2257
2723
|
border-radius: 0.75rem;
|
|
2258
2724
|
border: 1px dashed #60a5fa;
|
|
2259
2725
|
background: rgba(239,246,255,0.5);
|
|
2260
2726
|
display: flex;
|
|
2727
|
+
flex-wrap: wrap;
|
|
2728
|
+
align-items: stretch;
|
|
2729
|
+
gap: 0;
|
|
2261
2730
|
overflow: hidden;
|
|
2262
|
-
height: 180px;
|
|
2731
|
+
min-height: 180px;
|
|
2263
2732
|
transition: border-color 150ms, background 150ms, box-shadow 150ms;
|
|
2264
2733
|
cursor: pointer;
|
|
2265
2734
|
}
|
|
@@ -2273,9 +2742,12 @@ function ensureFileStyles() {
|
|
|
2273
2742
|
box-shadow: 0 0 0 4px rgba(191,219,254,0.7);
|
|
2274
2743
|
}
|
|
2275
2744
|
|
|
2276
|
-
/* Upload zone inside wide tile
|
|
2745
|
+
/* Upload zone inside wide tile.
|
|
2746
|
+
flex: 1 1 220px \u2014 wants at least 220px; if the container can't fit
|
|
2747
|
+
upload + library on one row (~220 + 176), library wraps below. */
|
|
2277
2748
|
.fb-wide-tile-upload {
|
|
2278
|
-
flex: 1;
|
|
2749
|
+
flex: 1 1 220px;
|
|
2750
|
+
min-height: 140px;
|
|
2279
2751
|
display: flex;
|
|
2280
2752
|
flex-direction: column;
|
|
2281
2753
|
align-items: center;
|
|
@@ -2288,24 +2760,21 @@ function ensureFileStyles() {
|
|
|
2288
2760
|
background: transparent;
|
|
2289
2761
|
border: none;
|
|
2290
2762
|
font-family: inherit;
|
|
2763
|
+
/* Dashed separator from library: right side when in a row, bottom when
|
|
2764
|
+
wrapped (the line then sits between the two stacked cards). */
|
|
2765
|
+
border-right: 1px dashed rgba(96,165,250,0.5);
|
|
2291
2766
|
}
|
|
2292
2767
|
.fb-wide-tile-upload:hover {
|
|
2293
2768
|
background: rgba(191,219,254,0.25);
|
|
2294
2769
|
}
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
.
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
border-left: 1px dashed rgba(96,165,250,0.5);
|
|
2301
|
-
background: transparent;
|
|
2302
|
-
flex-shrink: 0;
|
|
2303
|
-
}
|
|
2304
|
-
|
|
2305
|
-
/* Library zone inside wide tile */
|
|
2770
|
+
/* Library zone inside wide tile.
|
|
2771
|
+
flex: 0 0 176px \u2014 fixed 176px, never grows. Upload fills the rest in
|
|
2772
|
+
row layout. When the tile wraps to two rows on narrow containers,
|
|
2773
|
+
library stays 176px wide on its own row (left-aligned), preserving the
|
|
2774
|
+
visual hierarchy "upload > library" in both layouts. */
|
|
2306
2775
|
.fb-wide-tile-library {
|
|
2307
|
-
|
|
2308
|
-
|
|
2776
|
+
flex: 0 0 176px;
|
|
2777
|
+
min-height: 120px;
|
|
2309
2778
|
display: flex;
|
|
2310
2779
|
flex-direction: column;
|
|
2311
2780
|
align-items: center;
|
|
@@ -2322,6 +2791,10 @@ function ensureFileStyles() {
|
|
|
2322
2791
|
.fb-wide-tile-library:hover {
|
|
2323
2792
|
background: rgba(191,219,254,0.25);
|
|
2324
2793
|
}
|
|
2794
|
+
/* Narrow-tile mode lives in a separate <style> tag (see below) \u2014 the
|
|
2795
|
+
@container rule is appended only when the runtime actually supports
|
|
2796
|
+
container queries, so jsdom (which doesn't) never sees it and stays
|
|
2797
|
+
quiet in test logs. */
|
|
2325
2798
|
|
|
2326
2799
|
/* \u2500\u2500\u2500 Multi-file outer grid container \u2500\u2500\u2500 */
|
|
2327
2800
|
.fb-multi-outer {
|
|
@@ -2700,6 +3173,39 @@ function ensureFileStyles() {
|
|
|
2700
3173
|
}
|
|
2701
3174
|
`;
|
|
2702
3175
|
document.head.appendChild(style);
|
|
3176
|
+
if (typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("container-type", "inline-size")) {
|
|
3177
|
+
const cq = document.createElement("style");
|
|
3178
|
+
cq.setAttribute("data-fb-file-styles-cq", "true");
|
|
3179
|
+
cq.textContent = `
|
|
3180
|
+
.fb-wide-tile { container-type: inline-size; }
|
|
3181
|
+
@container (max-width: 408px) {
|
|
3182
|
+
.fb-wide-tile-upload {
|
|
3183
|
+
border-right: none;
|
|
3184
|
+
border-bottom: 1px dashed rgba(96,165,250,0.5);
|
|
3185
|
+
}
|
|
3186
|
+
.fb-wide-tile-library {
|
|
3187
|
+
flex: 1 0 100%;
|
|
3188
|
+
min-height: 0;
|
|
3189
|
+
flex-direction: row;
|
|
3190
|
+
gap: 6px;
|
|
3191
|
+
padding: 8px 12px;
|
|
3192
|
+
font-size: 12px;
|
|
3193
|
+
}
|
|
3194
|
+
.fb-wide-tile-library .fb-wide-tile-library-icon {
|
|
3195
|
+
width: 16px;
|
|
3196
|
+
height: 16px;
|
|
3197
|
+
}
|
|
3198
|
+
.fb-wide-tile-library .fb-wide-tile-library-label {
|
|
3199
|
+
font-size: 12px;
|
|
3200
|
+
font-weight: 500;
|
|
3201
|
+
}
|
|
3202
|
+
.fb-wide-tile-library .fb-wide-tile-library-hint {
|
|
3203
|
+
display: none;
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
`;
|
|
3207
|
+
document.head.appendChild(cq);
|
|
3208
|
+
}
|
|
2703
3209
|
}
|
|
2704
3210
|
|
|
2705
3211
|
// src/components/file/dom.ts
|
|
@@ -2857,24 +3363,40 @@ function createTileActions(options) {
|
|
|
2857
3363
|
return btn;
|
|
2858
3364
|
};
|
|
2859
3365
|
if (replaceHandler) {
|
|
2860
|
-
const replaceBtn = makeBtn(
|
|
3366
|
+
const replaceBtn = makeBtn(
|
|
3367
|
+
ICON_REPLACE,
|
|
3368
|
+
t("replaceFile", state),
|
|
3369
|
+
"fb-tile-action-replace"
|
|
3370
|
+
);
|
|
2861
3371
|
replaceBtn.addEventListener("click", () => replaceHandler());
|
|
2862
3372
|
group.appendChild(replaceBtn);
|
|
2863
3373
|
}
|
|
2864
3374
|
if (libraryHandler) {
|
|
2865
|
-
const libBtn = makeBtn(
|
|
3375
|
+
const libBtn = makeBtn(
|
|
3376
|
+
ICON_LIBRARY,
|
|
3377
|
+
t("fromLibrary", state),
|
|
3378
|
+
"fb-tile-action-library"
|
|
3379
|
+
);
|
|
2866
3380
|
libBtn.addEventListener("click", () => libraryHandler());
|
|
2867
3381
|
group.appendChild(libBtn);
|
|
2868
3382
|
}
|
|
2869
3383
|
if (canDownload(state, meta)) {
|
|
2870
|
-
const dlBtn = makeBtn(
|
|
3384
|
+
const dlBtn = makeBtn(
|
|
3385
|
+
ICON_DOWNLOAD,
|
|
3386
|
+
t("downloadFile", state),
|
|
3387
|
+
"fb-tile-action-download"
|
|
3388
|
+
);
|
|
2871
3389
|
dlBtn.addEventListener("click", () => {
|
|
2872
3390
|
triggerTileDownload(resourceId, fileName, state, meta);
|
|
2873
3391
|
});
|
|
2874
3392
|
group.appendChild(dlBtn);
|
|
2875
3393
|
}
|
|
2876
3394
|
if (canOpenInTab(state, meta)) {
|
|
2877
|
-
const openBtn = makeBtn(
|
|
3395
|
+
const openBtn = makeBtn(
|
|
3396
|
+
ICON_OPEN,
|
|
3397
|
+
t("openInNewTab", state),
|
|
3398
|
+
"fb-tile-action-open"
|
|
3399
|
+
);
|
|
2878
3400
|
openBtn.addEventListener("click", () => {
|
|
2879
3401
|
triggerTileOpen(resourceId, state, meta).catch((err) => {
|
|
2880
3402
|
console.error("Open failed:", err);
|
|
@@ -2883,7 +3405,11 @@ function createTileActions(options) {
|
|
|
2883
3405
|
group.appendChild(openBtn);
|
|
2884
3406
|
}
|
|
2885
3407
|
if (canRemove && removeHandler) {
|
|
2886
|
-
const rmBtn = makeBtn(
|
|
3408
|
+
const rmBtn = makeBtn(
|
|
3409
|
+
ICON_REMOVE,
|
|
3410
|
+
t("removeElement", state),
|
|
3411
|
+
"fb-tile-action-remove"
|
|
3412
|
+
);
|
|
2887
3413
|
rmBtn.addEventListener("click", () => {
|
|
2888
3414
|
removeHandler();
|
|
2889
3415
|
});
|
|
@@ -2961,11 +3487,17 @@ function positionZoomPopup(popup, tile) {
|
|
|
2961
3487
|
} else if (tileRect.bottom + margin + popupSize + padding <= window.innerHeight) {
|
|
2962
3488
|
top = tileRect.bottom + margin;
|
|
2963
3489
|
} else {
|
|
2964
|
-
top = Math.max(
|
|
3490
|
+
top = Math.max(
|
|
3491
|
+
padding,
|
|
3492
|
+
Math.min(window.innerHeight - popupSize - padding, tileRect.top)
|
|
3493
|
+
);
|
|
2965
3494
|
}
|
|
2966
3495
|
const tileCenterX = tileRect.left + tileRect.width / 2;
|
|
2967
3496
|
let left = tileCenterX - popupSize / 2;
|
|
2968
|
-
left = Math.max(
|
|
3497
|
+
left = Math.max(
|
|
3498
|
+
padding,
|
|
3499
|
+
Math.min(window.innerWidth - popupSize - padding, left)
|
|
3500
|
+
);
|
|
2969
3501
|
popup.style.top = `${top}px`;
|
|
2970
3502
|
popup.style.left = `${left}px`;
|
|
2971
3503
|
}
|
|
@@ -3009,7 +3541,9 @@ function attachZoomHover(tile, src, alt, actionsEl) {
|
|
|
3009
3541
|
const popup = getOrCreateZoomPopup();
|
|
3010
3542
|
const existingActions = popup.querySelector(".fb-tile-actions");
|
|
3011
3543
|
if (existingActions) existingActions.remove();
|
|
3012
|
-
const img = popup.querySelector(
|
|
3544
|
+
const img = popup.querySelector(
|
|
3545
|
+
".fb-tile-zoom-preview-img"
|
|
3546
|
+
);
|
|
3013
3547
|
img.src = src;
|
|
3014
3548
|
img.alt = alt;
|
|
3015
3549
|
if (actionsEl) {
|
|
@@ -3037,7 +3571,9 @@ function attachZoomHover(tile, src, alt, actionsEl) {
|
|
|
3037
3571
|
});
|
|
3038
3572
|
}
|
|
3039
3573
|
function attachClonedActionListeners(cloned, original) {
|
|
3040
|
-
const originalBtns = Array.from(
|
|
3574
|
+
const originalBtns = Array.from(
|
|
3575
|
+
original.querySelectorAll(".fb-tile-action-btn")
|
|
3576
|
+
);
|
|
3041
3577
|
const clonedBtns = Array.from(cloned.querySelectorAll(".fb-tile-action-btn"));
|
|
3042
3578
|
clonedBtns.forEach((clonedBtn, i) => {
|
|
3043
3579
|
const origBtn = originalBtns[i];
|
|
@@ -3090,14 +3626,18 @@ function renderLocalVideoPreview(container, file, videoType, resourceId, state,
|
|
|
3090
3626
|
return newContainer;
|
|
3091
3627
|
}
|
|
3092
3628
|
function attachVideoButtonHandlers(container, resourceId, state, deps) {
|
|
3093
|
-
const changeBtn = container.querySelector(
|
|
3629
|
+
const changeBtn = container.querySelector(
|
|
3630
|
+
".change-file-btn"
|
|
3631
|
+
);
|
|
3094
3632
|
if (changeBtn) {
|
|
3095
3633
|
changeBtn.onclick = (e) => {
|
|
3096
3634
|
e.stopPropagation();
|
|
3097
3635
|
deps?.picker?.click();
|
|
3098
3636
|
};
|
|
3099
3637
|
}
|
|
3100
|
-
const deleteBtn = container.querySelector(
|
|
3638
|
+
const deleteBtn = container.querySelector(
|
|
3639
|
+
".delete-file-btn"
|
|
3640
|
+
);
|
|
3101
3641
|
if (deleteBtn) {
|
|
3102
3642
|
deleteBtn.onclick = (e) => {
|
|
3103
3643
|
e.stopPropagation();
|
|
@@ -3230,7 +3770,13 @@ async function renderFilePreview(container, resourceId, state, options = {}) {
|
|
|
3230
3770
|
deps
|
|
3231
3771
|
);
|
|
3232
3772
|
} else {
|
|
3233
|
-
await renderUploadedFilePreview(
|
|
3773
|
+
await renderUploadedFilePreview(
|
|
3774
|
+
container,
|
|
3775
|
+
resourceId,
|
|
3776
|
+
fileName,
|
|
3777
|
+
meta,
|
|
3778
|
+
state
|
|
3779
|
+
);
|
|
3234
3780
|
const isVideo = meta?.type?.startsWith("video/");
|
|
3235
3781
|
if (!isReadonly && !isVideo) {
|
|
3236
3782
|
renderDeleteButton(container, resourceId, state);
|
|
@@ -3257,7 +3803,8 @@ async function renderFilePreviewReadonly(resourceId, state, fileName, options =
|
|
|
3257
3803
|
}
|
|
3258
3804
|
const localFileUrl = meta?.file instanceof File ? getLocalFileUrl(meta.file) : null;
|
|
3259
3805
|
const resolveOpenUrl = async () => {
|
|
3260
|
-
if (state.config.getDownloadUrl)
|
|
3806
|
+
if (state.config.getDownloadUrl)
|
|
3807
|
+
return state.config.getDownloadUrl(resourceId);
|
|
3261
3808
|
if (state.config.getThumbnail) return state.config.getThumbnail(resourceId);
|
|
3262
3809
|
return localFileUrl;
|
|
3263
3810
|
};
|
|
@@ -3397,7 +3944,8 @@ async function fillTileContent(tile, rid, meta, state, actionsEl) {
|
|
|
3397
3944
|
}
|
|
3398
3945
|
} catch (error) {
|
|
3399
3946
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
3400
|
-
if (state.config.onThumbnailError)
|
|
3947
|
+
if (state.config.onThumbnailError)
|
|
3948
|
+
state.config.onThumbnailError(err, rid);
|
|
3401
3949
|
tile.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:16px;color:var(--fb-error-color,#ef4444);">\u2715</div>`;
|
|
3402
3950
|
}
|
|
3403
3951
|
} else {
|
|
@@ -3430,7 +3978,8 @@ async function fillTileContent(tile, rid, meta, state, actionsEl) {
|
|
|
3430
3978
|
}
|
|
3431
3979
|
} catch (error) {
|
|
3432
3980
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
3433
|
-
if (state.config.onThumbnailError)
|
|
3981
|
+
if (state.config.onThumbnailError)
|
|
3982
|
+
state.config.onThumbnailError(err, rid);
|
|
3434
3983
|
tile.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:16px;color:var(--fb-error-color,#ef4444);">\u2715</div>`;
|
|
3435
3984
|
}
|
|
3436
3985
|
} else {
|
|
@@ -3463,7 +4012,8 @@ async function forceDownload(resourceId, fileName, state) {
|
|
|
3463
4012
|
if (fileUrl) {
|
|
3464
4013
|
const finalUrl = fileUrl.startsWith("http") ? fileUrl : new URL(fileUrl, window.location.href).href;
|
|
3465
4014
|
const response = await fetch(finalUrl);
|
|
3466
|
-
if (!response.ok)
|
|
4015
|
+
if (!response.ok)
|
|
4016
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
3467
4017
|
const blob = await response.blob();
|
|
3468
4018
|
downloadBlob(blob, fileName);
|
|
3469
4019
|
} else {
|
|
@@ -3512,7 +4062,9 @@ async function uploadSingleFile(file, state) {
|
|
|
3512
4062
|
} catch (error) {
|
|
3513
4063
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
3514
4064
|
if (state.config.onUploadError) state.config.onUploadError(err, file);
|
|
3515
|
-
|
|
4065
|
+
const wrapped = new Error(`File upload failed: ${err.message}`);
|
|
4066
|
+
wrapped.cause = err;
|
|
4067
|
+
throw wrapped;
|
|
3516
4068
|
}
|
|
3517
4069
|
}
|
|
3518
4070
|
async function handleFileSelect(opts) {
|
|
@@ -3624,7 +4176,9 @@ function filterAndSlice(allFiles, currentCount, constraints, state) {
|
|
|
3624
4176
|
const rejectedBySize = afterMime.filter(
|
|
3625
4177
|
(f) => !isFileSizeAllowed(f, constraints.maxSize)
|
|
3626
4178
|
);
|
|
3627
|
-
const valid = afterMime.filter(
|
|
4179
|
+
const valid = afterMime.filter(
|
|
4180
|
+
(f) => isFileSizeAllowed(f, constraints.maxSize)
|
|
4181
|
+
);
|
|
3628
4182
|
const remaining = constraints.maxCount === Infinity ? valid.length : Math.max(0, constraints.maxCount - currentCount);
|
|
3629
4183
|
const accepted = valid.slice(0, remaining);
|
|
3630
4184
|
const skippedByCount = valid.length - accepted.length;
|
|
@@ -3637,7 +4191,13 @@ function filterAndSlice(allFiles, currentCount, constraints, state) {
|
|
|
3637
4191
|
if (rejectedByMime.length > 0) {
|
|
3638
4192
|
const mimes = constraints.allowedMimes.join(", ");
|
|
3639
4193
|
const names = rejectedByMime.map((f) => f.name).join(", ");
|
|
3640
|
-
errorParts.push(
|
|
4194
|
+
errorParts.push(
|
|
4195
|
+
t("invalidFileMime", state, {
|
|
4196
|
+
name: names,
|
|
4197
|
+
type: rejectedByMime.map((f) => f.type).join(", "),
|
|
4198
|
+
mimes
|
|
4199
|
+
})
|
|
4200
|
+
);
|
|
3641
4201
|
}
|
|
3642
4202
|
if (rejectedBySize.length > 0) {
|
|
3643
4203
|
const names = rejectedBySize.map((f) => f.name).join(", ");
|
|
@@ -3661,7 +4221,8 @@ async function uploadBatch(accepted, resourceIds, listEl, state) {
|
|
|
3661
4221
|
const addTile = tilesWrap.querySelector(".fb-multi-add-tile-js") ?? tilesWrap.querySelector(".fb-tile-add");
|
|
3662
4222
|
if (addTile) addTile.style.display = "none";
|
|
3663
4223
|
}
|
|
3664
|
-
|
|
4224
|
+
const failures = [];
|
|
4225
|
+
await Promise.allSettled(
|
|
3665
4226
|
accepted.map(async (file) => {
|
|
3666
4227
|
const placeholder = createUploadingTile(file.name, state);
|
|
3667
4228
|
if (listEl) {
|
|
@@ -3678,11 +4239,27 @@ async function uploadBatch(accepted, resourceIds, listEl, state) {
|
|
|
3678
4239
|
file: void 0
|
|
3679
4240
|
});
|
|
3680
4241
|
resourceIds.push(rid);
|
|
4242
|
+
} catch (err) {
|
|
4243
|
+
const wrapped = err instanceof Error ? err : new Error(String(err));
|
|
4244
|
+
const cause = wrapped.cause;
|
|
4245
|
+
const root = cause instanceof Error ? cause : cause !== void 0 ? new Error(String(cause)) : wrapped;
|
|
4246
|
+
failures.push({ file, error: root });
|
|
3681
4247
|
} finally {
|
|
3682
4248
|
placeholder.remove();
|
|
3683
4249
|
}
|
|
3684
4250
|
})
|
|
3685
4251
|
);
|
|
4252
|
+
return { failures };
|
|
4253
|
+
}
|
|
4254
|
+
function buildBatchErrorMessage(filterError, failures, state) {
|
|
4255
|
+
if (failures.length === 0) return filterError;
|
|
4256
|
+
const uploadMsg = failures.map(
|
|
4257
|
+
(f) => t("uploadFailed", state, {
|
|
4258
|
+
name: f.file.name,
|
|
4259
|
+
error: f.error.message
|
|
4260
|
+
})
|
|
4261
|
+
).join(" \u2022 ");
|
|
4262
|
+
return filterError ? `${filterError} \u2022 ${uploadMsg}` : uploadMsg;
|
|
3686
4263
|
}
|
|
3687
4264
|
function setupFilesDropHandler(filesContainer, resourceIds, state, updateCallback, constraints, pathKey, instance) {
|
|
3688
4265
|
setupDragAndDrop(filesContainer, async (files) => {
|
|
@@ -3698,7 +4275,13 @@ function setupFilesDropHandler(filesContainer, resourceIds, state, updateCallbac
|
|
|
3698
4275
|
clearFileError(filesContainer);
|
|
3699
4276
|
}
|
|
3700
4277
|
const list = filesContainer.querySelector(".files-list") ?? filesContainer;
|
|
3701
|
-
await uploadBatch(accepted, resourceIds, list, state);
|
|
4278
|
+
const { failures } = await uploadBatch(accepted, resourceIds, list, state);
|
|
4279
|
+
const combined = buildBatchErrorMessage(errorMessage, failures, state);
|
|
4280
|
+
if (combined) {
|
|
4281
|
+
showFileError(filesContainer, combined);
|
|
4282
|
+
} else {
|
|
4283
|
+
clearFileError(filesContainer);
|
|
4284
|
+
}
|
|
3702
4285
|
updateCallback();
|
|
3703
4286
|
if (instance && pathKey && !state.config.readonly) {
|
|
3704
4287
|
instance.triggerOnChange(pathKey, resourceIds);
|
|
@@ -3721,7 +4304,20 @@ function setupFilesPickerHandler(filesPicker, resourceIds, state, updateCallback
|
|
|
3721
4304
|
clearFileError(wrapperEl);
|
|
3722
4305
|
}
|
|
3723
4306
|
const listEl = wrapperEl?.querySelector(".files-list");
|
|
3724
|
-
|
|
4307
|
+
const { failures } = await uploadBatch(
|
|
4308
|
+
accepted,
|
|
4309
|
+
resourceIds,
|
|
4310
|
+
listEl ?? null,
|
|
4311
|
+
state
|
|
4312
|
+
);
|
|
4313
|
+
if (wrapperEl) {
|
|
4314
|
+
const combined = buildBatchErrorMessage(errorMessage, failures, state);
|
|
4315
|
+
if (combined) {
|
|
4316
|
+
showFileError(wrapperEl, combined);
|
|
4317
|
+
} else {
|
|
4318
|
+
clearFileError(wrapperEl);
|
|
4319
|
+
}
|
|
4320
|
+
}
|
|
3725
4321
|
updateCallback();
|
|
3726
4322
|
filesPicker.value = "";
|
|
3727
4323
|
if (instance && pathKey && !state.config.readonly) {
|
|
@@ -3753,10 +4349,17 @@ function validatePickedResource(resource, allowedExtensions, allowedMimes, maxSi
|
|
|
3753
4349
|
}
|
|
3754
4350
|
if (!isMimeAllowed(resource.type, allowedMimes)) {
|
|
3755
4351
|
const mimes = allowedMimes.join(", ");
|
|
3756
|
-
return t("invalidFileMime", state, {
|
|
4352
|
+
return t("invalidFileMime", state, {
|
|
4353
|
+
name: resource.name,
|
|
4354
|
+
type: resource.type,
|
|
4355
|
+
mimes
|
|
4356
|
+
});
|
|
3757
4357
|
}
|
|
3758
4358
|
if (!isSizeWithinLimit(resource.size, maxSizeMB)) {
|
|
3759
|
-
return t("fileTooLarge", state, {
|
|
4359
|
+
return t("fileTooLarge", state, {
|
|
4360
|
+
name: resource.name,
|
|
4361
|
+
maxSize: maxSizeMB
|
|
4362
|
+
});
|
|
3760
4363
|
}
|
|
3761
4364
|
return null;
|
|
3762
4365
|
}
|
|
@@ -3815,7 +4418,13 @@ async function handleLibraryPickMulti(state, element, wrapper, fieldPath, resour
|
|
|
3815
4418
|
return true;
|
|
3816
4419
|
});
|
|
3817
4420
|
const validItems = deduped.filter((r) => {
|
|
3818
|
-
const err = validatePickedResource(
|
|
4421
|
+
const err = validatePickedResource(
|
|
4422
|
+
r,
|
|
4423
|
+
allowedExtensions,
|
|
4424
|
+
allowedMimes,
|
|
4425
|
+
maxSizeMB,
|
|
4426
|
+
state
|
|
4427
|
+
);
|
|
3819
4428
|
return err === null;
|
|
3820
4429
|
});
|
|
3821
4430
|
const freshRemaining = maxCount === Infinity ? validItems.length : Math.max(0, maxCount - resourceIds.length);
|
|
@@ -3859,14 +4468,22 @@ async function handleLibraryPickSingle(state, element, container, fileWrapper, p
|
|
|
3859
4468
|
}
|
|
3860
4469
|
if (picked.length === 0) return;
|
|
3861
4470
|
const first = picked[0];
|
|
3862
|
-
const validationError = validatePickedResource(
|
|
4471
|
+
const validationError = validatePickedResource(
|
|
4472
|
+
first,
|
|
4473
|
+
allowedExtensions,
|
|
4474
|
+
allowedMimes,
|
|
4475
|
+
maxSizeMB,
|
|
4476
|
+
state
|
|
4477
|
+
);
|
|
3863
4478
|
if (validationError !== null) {
|
|
3864
4479
|
showFileError(container, validationError);
|
|
3865
4480
|
return;
|
|
3866
4481
|
}
|
|
3867
4482
|
clearFileError(container);
|
|
3868
4483
|
registerPickedResource(first, state);
|
|
3869
|
-
let hiddenInput = fileWrapper.querySelector(
|
|
4484
|
+
let hiddenInput = fileWrapper.querySelector(
|
|
4485
|
+
'input[type="hidden"]'
|
|
4486
|
+
);
|
|
3870
4487
|
if (!hiddenInput) {
|
|
3871
4488
|
hiddenInput = document.createElement("input");
|
|
3872
4489
|
hiddenInput.type = "hidden";
|
|
@@ -3934,21 +4551,21 @@ function buildWideTile(state, hasLibrary, onUploadClick, onLibraryClick, isDragO
|
|
|
3934
4551
|
};
|
|
3935
4552
|
outer.appendChild(uploadBtn);
|
|
3936
4553
|
if (hasLibrary && onLibraryClick) {
|
|
3937
|
-
const divider = document.createElement("div");
|
|
3938
|
-
divider.className = "fb-wide-tile-divider";
|
|
3939
|
-
outer.appendChild(divider);
|
|
3940
4554
|
const libBtn = document.createElement("button");
|
|
3941
4555
|
libBtn.type = "button";
|
|
3942
4556
|
libBtn.className = "fb-wide-tile-library fb-file-library-card";
|
|
3943
4557
|
const libIcon = document.createElement("span");
|
|
4558
|
+
libIcon.className = "fb-wide-tile-library-icon";
|
|
3944
4559
|
libIcon.style.cssText = "width:28px;height:28px;display:block;flex-shrink:0;";
|
|
3945
4560
|
libIcon.innerHTML = ICON_LIBRARY2;
|
|
3946
4561
|
libBtn.appendChild(libIcon);
|
|
3947
4562
|
const libLabel = document.createElement("div");
|
|
4563
|
+
libLabel.className = "fb-wide-tile-library-label";
|
|
3948
4564
|
libLabel.style.cssText = "font-size:13px;font-weight:600;text-align:center;";
|
|
3949
4565
|
libLabel.textContent = t("fromLibrary", state);
|
|
3950
4566
|
libBtn.appendChild(libLabel);
|
|
3951
4567
|
const libHint = document.createElement("div");
|
|
4568
|
+
libHint.className = "fb-wide-tile-library-hint";
|
|
3952
4569
|
libHint.style.cssText = "font-size:11px;opacity:0.75;text-align:center;";
|
|
3953
4570
|
libHint.textContent = t("libraryHint", state);
|
|
3954
4571
|
libBtn.appendChild(libHint);
|
|
@@ -4025,7 +4642,8 @@ function renderSingleFileFilled(fileContainer, resourceId, state, deps, extras)
|
|
|
4025
4642
|
grid.appendChild(tile);
|
|
4026
4643
|
fileContainer.className = "file-preview-container";
|
|
4027
4644
|
fileContainer.removeAttribute("style");
|
|
4028
|
-
while (fileContainer.firstChild)
|
|
4645
|
+
while (fileContainer.firstChild)
|
|
4646
|
+
fileContainer.removeChild(fileContainer.firstChild);
|
|
4029
4647
|
fileContainer.appendChild(outer);
|
|
4030
4648
|
}
|
|
4031
4649
|
function buildMultiAddTile(state, hasLibrary, onUploadClick, onLibraryClick, isDragOver = false) {
|
|
@@ -4106,7 +4724,9 @@ function buildMetaLine(state, element, ridCount, maxCount, canClearAll, onClearA
|
|
|
4106
4724
|
metaText.appendChild(sizeSpan);
|
|
4107
4725
|
metaText.appendChild(buildMetaDot());
|
|
4108
4726
|
}
|
|
4109
|
-
const exts = getAllowedExtensions(
|
|
4727
|
+
const exts = getAllowedExtensions(
|
|
4728
|
+
element.accept
|
|
4729
|
+
);
|
|
4110
4730
|
if (exts.length > 0) {
|
|
4111
4731
|
const fmtSpan = document.createElement("span");
|
|
4112
4732
|
fmtSpan.className = "fb-meta-mono";
|
|
@@ -4352,7 +4972,9 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
|
4352
4972
|
setupDragAndDrop(container, handlers.dragHandler);
|
|
4353
4973
|
},
|
|
4354
4974
|
onRemove() {
|
|
4355
|
-
const hiddenInput = fileWrapper.querySelector(
|
|
4975
|
+
const hiddenInput = fileWrapper.querySelector(
|
|
4976
|
+
'input[type="hidden"]'
|
|
4977
|
+
);
|
|
4356
4978
|
const currentRid = hiddenInput?.value;
|
|
4357
4979
|
if (currentRid) {
|
|
4358
4980
|
releaseLocalFileUrl(state.resourceIndex.get(currentRid)?.file);
|
|
@@ -4362,7 +4984,9 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
|
4362
4984
|
}
|
|
4363
4985
|
};
|
|
4364
4986
|
const buildSingleExtras = () => {
|
|
4365
|
-
const hasLibrary = Boolean(
|
|
4987
|
+
const hasLibrary = Boolean(
|
|
4988
|
+
state.config.pickExistingFiles && !element.disableLibrary
|
|
4989
|
+
);
|
|
4366
4990
|
return {
|
|
4367
4991
|
replaceHandler: state.config.uploadFile ? () => picker.click() : null,
|
|
4368
4992
|
libraryHandler: hasLibrary ? () => {
|
|
@@ -4374,7 +4998,13 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
|
4374
4998
|
pathKey,
|
|
4375
4999
|
pathKey,
|
|
4376
5000
|
async (rid) => {
|
|
4377
|
-
renderSingleFileFilled(
|
|
5001
|
+
renderSingleFileFilled(
|
|
5002
|
+
fileContainer,
|
|
5003
|
+
rid,
|
|
5004
|
+
state,
|
|
5005
|
+
buildDeps(),
|
|
5006
|
+
buildSingleExtras()
|
|
5007
|
+
);
|
|
4378
5008
|
},
|
|
4379
5009
|
ctx.instance
|
|
4380
5010
|
).catch((err) => {
|
|
@@ -4390,14 +5020,21 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
|
4390
5020
|
setupDrop: handlers.setupDrop,
|
|
4391
5021
|
onRemove: handlers.onRemove,
|
|
4392
5022
|
onAfterUpload: (container, rid) => {
|
|
4393
|
-
renderSingleFileFilled(
|
|
5023
|
+
renderSingleFileFilled(
|
|
5024
|
+
container,
|
|
5025
|
+
rid,
|
|
5026
|
+
state,
|
|
5027
|
+
buildDeps(),
|
|
5028
|
+
buildSingleExtras()
|
|
5029
|
+
);
|
|
4394
5030
|
}
|
|
4395
5031
|
});
|
|
4396
5032
|
const renderEmptySingleState = () => {
|
|
4397
5033
|
ensureFileStyles();
|
|
4398
5034
|
fileContainer.className = "file-preview-container";
|
|
4399
5035
|
fileContainer.removeAttribute("style");
|
|
4400
|
-
while (fileContainer.firstChild)
|
|
5036
|
+
while (fileContainer.firstChild)
|
|
5037
|
+
fileContainer.removeChild(fileContainer.firstChild);
|
|
4401
5038
|
const onLibraryClick = buildSingleExtras().libraryHandler;
|
|
4402
5039
|
const wideTile = buildWideTile(
|
|
4403
5040
|
state,
|
|
@@ -4538,7 +5175,13 @@ function renderFilesElementEdit(element, ctx, wrapper, pathKey) {
|
|
|
4538
5175
|
setupMultiFileEditMode(element, ctx, wrapper, pathKey, Infinity);
|
|
4539
5176
|
}
|
|
4540
5177
|
function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
4541
|
-
setupMultiFileEditMode(
|
|
5178
|
+
setupMultiFileEditMode(
|
|
5179
|
+
element,
|
|
5180
|
+
ctx,
|
|
5181
|
+
wrapper,
|
|
5182
|
+
pathKey,
|
|
5183
|
+
element.maxCount ?? Infinity
|
|
5184
|
+
);
|
|
4542
5185
|
}
|
|
4543
5186
|
|
|
4544
5187
|
// src/components/file/validate.ts
|
|
@@ -4717,7 +5360,11 @@ function renderMultiFileReadonly(rids, state, wrapper, pathKey, _marginTop) {
|
|
|
4717
5360
|
const placeholder = placeholders[i];
|
|
4718
5361
|
const meta = state.resourceIndex.get(resourceId);
|
|
4719
5362
|
renderFilePreviewReadonly(resourceId, state, meta?.name).then((tile) => {
|
|
4720
|
-
tile.classList.add(
|
|
5363
|
+
tile.classList.add(
|
|
5364
|
+
"fb-readonly-tile",
|
|
5365
|
+
"fb-checker",
|
|
5366
|
+
"fb-tile-resource"
|
|
5367
|
+
);
|
|
4721
5368
|
tile.dataset.resourceId = resourceId;
|
|
4722
5369
|
placeholder.replaceWith(tile);
|
|
4723
5370
|
}).catch(() => {
|
|
@@ -5067,51 +5714,25 @@ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
|
|
|
5067
5714
|
removeBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
5068
5715
|
});
|
|
5069
5716
|
}
|
|
5070
|
-
let
|
|
5071
|
-
let countDisplay = null;
|
|
5717
|
+
let addUpdate = null;
|
|
5072
5718
|
if (!readonly) {
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
5088
|
-
});
|
|
5089
|
-
addBtn.addEventListener("mouseleave", () => {
|
|
5090
|
-
addBtn.style.backgroundColor = "transparent";
|
|
5091
|
-
});
|
|
5092
|
-
addBtn.onclick = () => {
|
|
5093
|
-
const defaultColour = element.default || "#000000";
|
|
5094
|
-
values.push(defaultColour);
|
|
5095
|
-
addColourItem(defaultColour);
|
|
5096
|
-
updateAddButton();
|
|
5097
|
-
updateRemoveButtons();
|
|
5098
|
-
};
|
|
5099
|
-
countDisplay = document.createElement("span");
|
|
5100
|
-
countDisplay.className = "text-sm text-gray-500";
|
|
5101
|
-
addRow.appendChild(addBtn);
|
|
5102
|
-
addRow.appendChild(countDisplay);
|
|
5103
|
-
wrapper.appendChild(addRow);
|
|
5719
|
+
const handle = createAddItemRow(
|
|
5720
|
+
"colour",
|
|
5721
|
+
() => {
|
|
5722
|
+
const defaultColour = element.default || "#000000";
|
|
5723
|
+
values.push(defaultColour);
|
|
5724
|
+
addColourItem(defaultColour);
|
|
5725
|
+
updateAddButton();
|
|
5726
|
+
updateRemoveButtons();
|
|
5727
|
+
},
|
|
5728
|
+
{ label: element.addLabel }
|
|
5729
|
+
);
|
|
5730
|
+
addUpdate = handle.update;
|
|
5731
|
+
mountCounterInLabel(wrapper, handle.counter);
|
|
5732
|
+
wrapper.appendChild(handle.row);
|
|
5104
5733
|
}
|
|
5105
5734
|
function updateAddButton() {
|
|
5106
|
-
if (
|
|
5107
|
-
const addBtn = addRow.querySelector(".add-colour-btn");
|
|
5108
|
-
if (addBtn) {
|
|
5109
|
-
const disabled = values.length >= maxCount;
|
|
5110
|
-
addBtn.disabled = disabled;
|
|
5111
|
-
addBtn.style.opacity = disabled ? "0.5" : "1";
|
|
5112
|
-
addBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
5113
|
-
}
|
|
5114
|
-
countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
|
|
5735
|
+
if (addUpdate) addUpdate(values.length, maxCount);
|
|
5115
5736
|
}
|
|
5116
5737
|
values.forEach((value) => addColourItem(value));
|
|
5117
5738
|
updateAddButton();
|
|
@@ -5186,7 +5807,7 @@ function validateColourElement(element, key, context) {
|
|
|
5186
5807
|
};
|
|
5187
5808
|
if (element.multiple) {
|
|
5188
5809
|
const hexInputs = scopeRoot.querySelectorAll(
|
|
5189
|
-
`[name^="${key}["].colour-hex-input`
|
|
5810
|
+
`[name^="${key}\\["].colour-hex-input`
|
|
5190
5811
|
);
|
|
5191
5812
|
const values = [];
|
|
5192
5813
|
hexInputs.forEach((input, index) => {
|
|
@@ -5235,7 +5856,7 @@ function updateColourField(element, fieldPath, value, context) {
|
|
|
5235
5856
|
return;
|
|
5236
5857
|
}
|
|
5237
5858
|
const hexInputs = scopeRoot.querySelectorAll(
|
|
5238
|
-
`[name^="${fieldPath}["].colour-hex-input`
|
|
5859
|
+
`[name^="${fieldPath}\\["].colour-hex-input`
|
|
5239
5860
|
);
|
|
5240
5861
|
hexInputs.forEach((hexInput, index) => {
|
|
5241
5862
|
if (index < value.length) {
|
|
@@ -5243,6 +5864,7 @@ function updateColourField(element, fieldPath, value, context) {
|
|
|
5243
5864
|
hexInput.value = normalized;
|
|
5244
5865
|
hexInput.classList.remove("invalid");
|
|
5245
5866
|
hexInput.title = "";
|
|
5867
|
+
clearFieldError(hexInput);
|
|
5246
5868
|
const wrapper = hexInput.closest(".colour-picker-wrapper");
|
|
5247
5869
|
if (wrapper) {
|
|
5248
5870
|
const swatch = wrapper.querySelector(".colour-swatch");
|
|
@@ -5272,6 +5894,7 @@ function updateColourField(element, fieldPath, value, context) {
|
|
|
5272
5894
|
hexInput.value = normalized;
|
|
5273
5895
|
hexInput.classList.remove("invalid");
|
|
5274
5896
|
hexInput.title = "";
|
|
5897
|
+
clearFieldError(hexInput);
|
|
5275
5898
|
const wrapper = hexInput.closest(".colour-picker-wrapper");
|
|
5276
5899
|
if (wrapper) {
|
|
5277
5900
|
const swatch = wrapper.querySelector(".colour-swatch");
|
|
@@ -5547,50 +6170,24 @@ function renderMultipleSliderElement(element, ctx, wrapper, pathKey) {
|
|
|
5547
6170
|
removeBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
5548
6171
|
});
|
|
5549
6172
|
}
|
|
5550
|
-
let
|
|
5551
|
-
let countDisplay = null;
|
|
6173
|
+
let addUpdate = null;
|
|
5552
6174
|
if (!readonly) {
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
addBtn.addEventListener("mouseenter", () => {
|
|
5567
|
-
addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
5568
|
-
});
|
|
5569
|
-
addBtn.addEventListener("mouseleave", () => {
|
|
5570
|
-
addBtn.style.backgroundColor = "transparent";
|
|
5571
|
-
});
|
|
5572
|
-
addBtn.onclick = () => {
|
|
5573
|
-
values.push(defaultValue);
|
|
5574
|
-
addSliderItem(defaultValue);
|
|
5575
|
-
updateAddButton();
|
|
5576
|
-
updateRemoveButtons();
|
|
5577
|
-
};
|
|
5578
|
-
countDisplay = document.createElement("span");
|
|
5579
|
-
countDisplay.className = "text-sm text-gray-500";
|
|
5580
|
-
addRow.appendChild(addBtn);
|
|
5581
|
-
addRow.appendChild(countDisplay);
|
|
5582
|
-
wrapper.appendChild(addRow);
|
|
6175
|
+
const handle = createAddItemRow(
|
|
6176
|
+
"slider",
|
|
6177
|
+
() => {
|
|
6178
|
+
values.push(defaultValue);
|
|
6179
|
+
addSliderItem(defaultValue);
|
|
6180
|
+
updateAddButton();
|
|
6181
|
+
updateRemoveButtons();
|
|
6182
|
+
},
|
|
6183
|
+
{ label: element.addLabel }
|
|
6184
|
+
);
|
|
6185
|
+
addUpdate = handle.update;
|
|
6186
|
+
mountCounterInLabel(wrapper, handle.counter);
|
|
6187
|
+
wrapper.appendChild(handle.row);
|
|
5583
6188
|
}
|
|
5584
6189
|
function updateAddButton() {
|
|
5585
|
-
if (
|
|
5586
|
-
const addBtn = addRow.querySelector(".add-slider-btn");
|
|
5587
|
-
if (addBtn) {
|
|
5588
|
-
const disabled = values.length >= maxCount;
|
|
5589
|
-
addBtn.disabled = disabled;
|
|
5590
|
-
addBtn.style.opacity = disabled ? "0.5" : "1";
|
|
5591
|
-
addBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
5592
|
-
}
|
|
5593
|
-
countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
|
|
6190
|
+
if (addUpdate) addUpdate(values.length, maxCount);
|
|
5594
6191
|
}
|
|
5595
6192
|
values.forEach((value) => addSliderItem(value));
|
|
5596
6193
|
updateAddButton();
|
|
@@ -5700,7 +6297,7 @@ function validateSliderElement(element, key, context) {
|
|
|
5700
6297
|
};
|
|
5701
6298
|
if (element.multiple) {
|
|
5702
6299
|
const sliders = scopeRoot.querySelectorAll(
|
|
5703
|
-
`input[type="range"][name^="${key}["]`
|
|
6300
|
+
`input[type="range"][name^="${key}\\["]`
|
|
5704
6301
|
);
|
|
5705
6302
|
const values = [];
|
|
5706
6303
|
sliders.forEach((slider, index) => {
|
|
@@ -5751,7 +6348,7 @@ function updateSliderField(element, fieldPath, value, context) {
|
|
|
5751
6348
|
return;
|
|
5752
6349
|
}
|
|
5753
6350
|
const sliders = scopeRoot.querySelectorAll(
|
|
5754
|
-
`input[type="range"][name^="${fieldPath}["]`
|
|
6351
|
+
`input[type="range"][name^="${fieldPath}\\["]`
|
|
5755
6352
|
);
|
|
5756
6353
|
sliders.forEach((slider, index) => {
|
|
5757
6354
|
if (index < value.length && value[index] !== null) {
|
|
@@ -5779,6 +6376,7 @@ function updateSliderField(element, fieldPath, value, context) {
|
|
|
5779
6376
|
}
|
|
5780
6377
|
slider.classList.remove("invalid");
|
|
5781
6378
|
slider.title = "";
|
|
6379
|
+
clearFieldError(slider);
|
|
5782
6380
|
}
|
|
5783
6381
|
});
|
|
5784
6382
|
if (value.length !== sliders.length) {
|
|
@@ -5815,6 +6413,7 @@ function updateSliderField(element, fieldPath, value, context) {
|
|
|
5815
6413
|
}
|
|
5816
6414
|
slider.classList.remove("invalid");
|
|
5817
6415
|
slider.title = "";
|
|
6416
|
+
clearFieldError(slider);
|
|
5818
6417
|
}
|
|
5819
6418
|
}
|
|
5820
6419
|
}
|
|
@@ -5937,21 +6536,50 @@ function getChildWrapperClass(isSlides, columns) {
|
|
|
5937
6536
|
const cols = columns || 1;
|
|
5938
6537
|
return cols === 1 ? "space-y-2" : `grid grid-cols-${cols} gap-2`;
|
|
5939
6538
|
}
|
|
6539
|
+
function mountRemoveButton(item, onRemove, state) {
|
|
6540
|
+
const rem = document.createElement("button");
|
|
6541
|
+
rem.type = "button";
|
|
6542
|
+
rem.className = "fb-item-remove";
|
|
6543
|
+
rem.setAttribute("aria-label", t("removeElement", state));
|
|
6544
|
+
rem.style.cssText = `
|
|
6545
|
+
width: 24px;
|
|
6546
|
+
height: 24px;
|
|
6547
|
+
display: inline-flex;
|
|
6548
|
+
align-items: center;
|
|
6549
|
+
justify-content: center;
|
|
6550
|
+
padding: 0;
|
|
6551
|
+
border: 0;
|
|
6552
|
+
border-radius: 4px;
|
|
6553
|
+
cursor: pointer;
|
|
6554
|
+
flex-shrink: 0;
|
|
6555
|
+
`;
|
|
6556
|
+
rem.innerHTML = BIN_ICON_SVG;
|
|
6557
|
+
rem.onclick = onRemove;
|
|
6558
|
+
const labelRow = item.querySelector("[data-fb-label-row]");
|
|
6559
|
+
if (labelRow) {
|
|
6560
|
+
rem.style.marginLeft = "auto";
|
|
6561
|
+
labelRow.appendChild(rem);
|
|
6562
|
+
return;
|
|
6563
|
+
}
|
|
6564
|
+
rem.style.position = "absolute";
|
|
6565
|
+
rem.style.top = "8px";
|
|
6566
|
+
rem.style.right = "8px";
|
|
6567
|
+
item.style.position = "relative";
|
|
6568
|
+
item.appendChild(rem);
|
|
6569
|
+
}
|
|
5940
6570
|
function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
5941
6571
|
const state = ctx.state;
|
|
5942
6572
|
const containerIsReadonly = isElementReadonly(element, state, ctx);
|
|
5943
6573
|
const childInheritedReadonly = containerIsReadonly || ctx.inheritedReadonly;
|
|
5944
6574
|
const containerWrap = document.createElement("div");
|
|
5945
6575
|
containerWrap.className = "border border-gray-200 rounded-lg p-2 bg-gray-50";
|
|
5946
|
-
const countDisplay = document.createElement("span");
|
|
5947
|
-
countDisplay.className = "text-sm text-gray-500";
|
|
5948
6576
|
const itemsWrap = document.createElement("div");
|
|
5949
6577
|
const isSlides = element.displayMode === "slides";
|
|
5950
6578
|
if (isSlides) {
|
|
5951
6579
|
itemsWrap.className = "fb-container-slides";
|
|
5952
6580
|
const slideCols = element.columns;
|
|
5953
6581
|
const gridTemplateColumns = typeof slideCols === "number" && slideCols > 0 ? `repeat(${slideCols}, 1fr)` : "repeat(auto-fit, minmax(280px, 1fr))";
|
|
5954
|
-
itemsWrap.style.cssText = `display:grid;grid-template-columns:${gridTemplateColumns};gap:
|
|
6582
|
+
itemsWrap.style.cssText = `display:grid;grid-template-columns:${gridTemplateColumns};gap:var(--fb-slides-gap, 14px);align-items:start;`;
|
|
5955
6583
|
} else {
|
|
5956
6584
|
itemsWrap.className = "space-y-2";
|
|
5957
6585
|
}
|
|
@@ -5966,92 +6594,67 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
5966
6594
|
const pre = Array.isArray(ctx.prefill?.[element.key]) ? ctx.prefill[element.key] : null;
|
|
5967
6595
|
const childDefaults = extractChildDefaults(element.elements);
|
|
5968
6596
|
const countItems = () => itemsWrap.querySelectorAll(":scope > .containerItem").length;
|
|
5969
|
-
const
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
add.addEventListener("mouseenter", () => {
|
|
5982
|
-
add.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
5983
|
-
});
|
|
5984
|
-
add.addEventListener("mouseleave", () => {
|
|
5985
|
-
add.style.backgroundColor = "transparent";
|
|
5986
|
-
});
|
|
5987
|
-
add.onclick = () => {
|
|
5988
|
-
if (countItems() < max) {
|
|
5989
|
-
const idx = countItems();
|
|
5990
|
-
const currentFormData = state.formRoot ? extractRootFormData(state.formRoot) : {};
|
|
5991
|
-
const subCtx = {
|
|
5992
|
-
state: ctx.state,
|
|
5993
|
-
path: pathJoin(ctx.path, `${element.key}[${idx}]`),
|
|
5994
|
-
prefill: childDefaults,
|
|
5995
|
-
// Defaults for enableIf evaluation
|
|
5996
|
-
formData: currentFormData,
|
|
5997
|
-
// Current root data from DOM for enableIf
|
|
5998
|
-
inheritedReadonly: childInheritedReadonly
|
|
5999
|
-
};
|
|
6000
|
-
const item = document.createElement("div");
|
|
6001
|
-
item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
|
|
6002
|
-
item.setAttribute("data-container-item", `${element.key}[${idx}]`);
|
|
6003
|
-
const childWrapper = document.createElement("div");
|
|
6004
|
-
childWrapper.className = getChildWrapperClass(isSlides, element.columns);
|
|
6005
|
-
element.elements.forEach((child) => {
|
|
6006
|
-
if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
|
|
6007
|
-
childWrapper.appendChild(
|
|
6008
|
-
createHiddenInput(
|
|
6009
|
-
pathJoin(subCtx.path, child.key),
|
|
6010
|
-
("default" in child ? child.default : null) ?? null
|
|
6011
|
-
)
|
|
6012
|
-
);
|
|
6013
|
-
} else {
|
|
6014
|
-
childWrapper.appendChild(renderElement(child, subCtx));
|
|
6015
|
-
}
|
|
6016
|
-
});
|
|
6017
|
-
item.appendChild(childWrapper);
|
|
6018
|
-
if (!containerIsReadonly) {
|
|
6019
|
-
const rem = document.createElement("button");
|
|
6020
|
-
rem.type = "button";
|
|
6021
|
-
rem.className = "absolute top-2 right-2 px-2 py-1 rounded";
|
|
6022
|
-
rem.style.cssText = `
|
|
6023
|
-
color: var(--fb-error-color);
|
|
6024
|
-
background-color: transparent;
|
|
6025
|
-
transition: background-color var(--fb-transition-duration);
|
|
6026
|
-
`;
|
|
6027
|
-
rem.textContent = "\u2715";
|
|
6028
|
-
rem.addEventListener("mouseenter", () => {
|
|
6029
|
-
rem.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
6030
|
-
});
|
|
6031
|
-
rem.addEventListener("mouseleave", () => {
|
|
6032
|
-
rem.style.backgroundColor = "transparent";
|
|
6033
|
-
});
|
|
6034
|
-
rem.onclick = () => handleRemoveItem(item);
|
|
6035
|
-
item.style.position = "relative";
|
|
6036
|
-
item.appendChild(rem);
|
|
6037
|
-
}
|
|
6038
|
-
itemsWrap.appendChild(item);
|
|
6039
|
-
updateAddButton();
|
|
6040
|
-
}
|
|
6597
|
+
const handleAddItem = () => {
|
|
6598
|
+
if (countItems() >= max) return;
|
|
6599
|
+
const idx = countItems();
|
|
6600
|
+
const currentFormData = state.formRoot ? extractRootFormData(state.formRoot) : {};
|
|
6601
|
+
const subCtx = {
|
|
6602
|
+
state: ctx.state,
|
|
6603
|
+
path: pathJoin(ctx.path, `${element.key}[${idx}]`),
|
|
6604
|
+
prefill: childDefaults,
|
|
6605
|
+
// Defaults for enableIf evaluation
|
|
6606
|
+
formData: currentFormData,
|
|
6607
|
+
// Current root data from DOM for enableIf
|
|
6608
|
+
inheritedReadonly: childInheritedReadonly
|
|
6041
6609
|
};
|
|
6042
|
-
|
|
6610
|
+
const item = document.createElement("div");
|
|
6611
|
+
item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
|
|
6612
|
+
item.setAttribute("data-container-item", `${element.key}[${idx}]`);
|
|
6613
|
+
if (isSlides) {
|
|
6614
|
+
item.setAttribute("data-fb-slide-card", "");
|
|
6615
|
+
}
|
|
6616
|
+
const childWrapper = document.createElement("div");
|
|
6617
|
+
childWrapper.className = getChildWrapperClass(isSlides, element.columns);
|
|
6618
|
+
element.elements.forEach((child) => {
|
|
6619
|
+
if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
|
|
6620
|
+
childWrapper.appendChild(
|
|
6621
|
+
createHiddenInput(
|
|
6622
|
+
pathJoin(subCtx.path, child.key),
|
|
6623
|
+
("default" in child ? child.default : null) ?? null
|
|
6624
|
+
)
|
|
6625
|
+
);
|
|
6626
|
+
} else {
|
|
6627
|
+
childWrapper.appendChild(renderElement(child, subCtx));
|
|
6628
|
+
}
|
|
6629
|
+
});
|
|
6630
|
+
item.appendChild(childWrapper);
|
|
6631
|
+
if (!containerIsReadonly) {
|
|
6632
|
+
mountRemoveButton(item, () => handleRemoveItem(item), state);
|
|
6633
|
+
}
|
|
6634
|
+
if (slideAddTile && slideAddTile.parentElement === itemsWrap) {
|
|
6635
|
+
itemsWrap.insertBefore(item, slideAddTile);
|
|
6636
|
+
} else {
|
|
6637
|
+
itemsWrap.appendChild(item);
|
|
6638
|
+
}
|
|
6639
|
+
updateAddButton();
|
|
6043
6640
|
};
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6641
|
+
let slideAddTile = null;
|
|
6642
|
+
let slideAddUpdate = null;
|
|
6643
|
+
let pillAddUpdate = null;
|
|
6644
|
+
const syncSlideTileSize = () => {
|
|
6645
|
+
if (!slideAddTile) return;
|
|
6646
|
+
const firstSlide = itemsWrap.querySelector(
|
|
6647
|
+
":scope > .containerItem"
|
|
6048
6648
|
);
|
|
6049
|
-
if (
|
|
6050
|
-
|
|
6051
|
-
existingAddBtn.style.opacity = currentCount >= max ? "0.5" : "1";
|
|
6052
|
-
existingAddBtn.style.pointerEvents = currentCount >= max ? "none" : "auto";
|
|
6649
|
+
if (firstSlide && firstSlide.offsetHeight > 0) {
|
|
6650
|
+
slideAddTile.style.minHeight = `${firstSlide.offsetHeight}px`;
|
|
6053
6651
|
}
|
|
6054
|
-
|
|
6652
|
+
};
|
|
6653
|
+
const updateAddButton = () => {
|
|
6654
|
+
const currentCount = countItems();
|
|
6655
|
+
if (slideAddUpdate) slideAddUpdate(currentCount, max);
|
|
6656
|
+
if (pillAddUpdate) pillAddUpdate(currentCount, max);
|
|
6657
|
+
if (slideAddTile) syncSlideTileSize();
|
|
6055
6658
|
};
|
|
6056
6659
|
const handleRemoveItem = (item) => {
|
|
6057
6660
|
item.remove();
|
|
@@ -6072,6 +6675,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
6072
6675
|
const item = document.createElement("div");
|
|
6073
6676
|
item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
|
|
6074
6677
|
item.setAttribute("data-container-item", `${element.key}[${idx}]`);
|
|
6678
|
+
if (isSlides) {
|
|
6679
|
+
item.setAttribute("data-fb-slide-card", "");
|
|
6680
|
+
}
|
|
6075
6681
|
const childWrapper = document.createElement("div");
|
|
6076
6682
|
if (isSlides) {
|
|
6077
6683
|
childWrapper.className = "space-y-2";
|
|
@@ -6095,24 +6701,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
6095
6701
|
});
|
|
6096
6702
|
item.appendChild(childWrapper);
|
|
6097
6703
|
if (!containerIsReadonly) {
|
|
6098
|
-
|
|
6099
|
-
rem.type = "button";
|
|
6100
|
-
rem.className = "absolute top-2 right-2 px-2 py-1 rounded";
|
|
6101
|
-
rem.style.cssText = `
|
|
6102
|
-
color: var(--fb-error-color);
|
|
6103
|
-
background-color: transparent;
|
|
6104
|
-
transition: background-color var(--fb-transition-duration);
|
|
6105
|
-
`;
|
|
6106
|
-
rem.textContent = "\u2715";
|
|
6107
|
-
rem.addEventListener("mouseenter", () => {
|
|
6108
|
-
rem.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
6109
|
-
});
|
|
6110
|
-
rem.addEventListener("mouseleave", () => {
|
|
6111
|
-
rem.style.backgroundColor = "transparent";
|
|
6112
|
-
});
|
|
6113
|
-
rem.onclick = () => handleRemoveItem(item);
|
|
6114
|
-
item.style.position = "relative";
|
|
6115
|
-
item.appendChild(rem);
|
|
6704
|
+
mountRemoveButton(item, () => handleRemoveItem(item), ctx.state);
|
|
6116
6705
|
}
|
|
6117
6706
|
itemsWrap.appendChild(item);
|
|
6118
6707
|
});
|
|
@@ -6132,6 +6721,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
6132
6721
|
const item = document.createElement("div");
|
|
6133
6722
|
item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
|
|
6134
6723
|
item.setAttribute("data-container-item", `${element.key}[${idx}]`);
|
|
6724
|
+
if (isSlides) {
|
|
6725
|
+
item.setAttribute("data-fb-slide-card", "");
|
|
6726
|
+
}
|
|
6135
6727
|
const childWrapper = document.createElement("div");
|
|
6136
6728
|
if (isSlides) {
|
|
6137
6729
|
childWrapper.className = "space-y-2";
|
|
@@ -6156,41 +6748,47 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
6156
6748
|
}
|
|
6157
6749
|
});
|
|
6158
6750
|
item.appendChild(childWrapper);
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6751
|
+
mountRemoveButton(
|
|
6752
|
+
item,
|
|
6753
|
+
() => {
|
|
6754
|
+
if (countItems() > min) {
|
|
6755
|
+
handleRemoveItem(item);
|
|
6756
|
+
}
|
|
6757
|
+
},
|
|
6758
|
+
ctx.state
|
|
6759
|
+
);
|
|
6760
|
+
itemsWrap.appendChild(item);
|
|
6761
|
+
}
|
|
6762
|
+
}
|
|
6763
|
+
containerWrap.appendChild(itemsWrap);
|
|
6764
|
+
if (!containerIsReadonly) {
|
|
6765
|
+
if (isSlides) {
|
|
6766
|
+
itemsWrap.style.alignItems = "stretch";
|
|
6767
|
+
const handle = createSlideAddTile(handleAddItem, {
|
|
6768
|
+
label: element.addLabel
|
|
6170
6769
|
});
|
|
6171
|
-
|
|
6172
|
-
|
|
6770
|
+
slideAddTile = handle.tile;
|
|
6771
|
+
slideAddUpdate = handle.update;
|
|
6772
|
+
mountCounterInLabel(wrapper, handle.counter);
|
|
6773
|
+
itemsWrap.appendChild(handle.tile);
|
|
6774
|
+
} else {
|
|
6775
|
+
const handle = createAddItemRow("container", handleAddItem, {
|
|
6776
|
+
label: element.addLabel
|
|
6173
6777
|
});
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
}
|
|
6178
|
-
};
|
|
6179
|
-
item.style.position = "relative";
|
|
6180
|
-
item.appendChild(rem);
|
|
6181
|
-
itemsWrap.appendChild(item);
|
|
6778
|
+
pillAddUpdate = handle.update;
|
|
6779
|
+
mountCounterInLabel(wrapper, handle.counter);
|
|
6780
|
+
containerWrap.appendChild(handle.row);
|
|
6182
6781
|
}
|
|
6183
6782
|
}
|
|
6184
|
-
containerWrap.appendChild(itemsWrap);
|
|
6185
|
-
if (!containerIsReadonly) {
|
|
6186
|
-
const addRow = document.createElement("div");
|
|
6187
|
-
addRow.className = "flex items-center gap-3 mt-2";
|
|
6188
|
-
addRow.appendChild(createAddButton());
|
|
6189
|
-
addRow.appendChild(countDisplay);
|
|
6190
|
-
containerWrap.appendChild(addRow);
|
|
6191
|
-
}
|
|
6192
6783
|
updateAddButton();
|
|
6193
6784
|
wrapper.appendChild(containerWrap);
|
|
6785
|
+
if (slideAddTile) {
|
|
6786
|
+
if (typeof requestAnimationFrame === "function") {
|
|
6787
|
+
requestAnimationFrame(syncSlideTileSize);
|
|
6788
|
+
} else {
|
|
6789
|
+
syncSlideTileSize();
|
|
6790
|
+
}
|
|
6791
|
+
}
|
|
6194
6792
|
}
|
|
6195
6793
|
var validateElementFunc = null;
|
|
6196
6794
|
function setValidateElement(fn) {
|
|
@@ -8783,7 +9381,7 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
|
|
|
8783
9381
|
if (element.minLength != null || element.maxLength != null) {
|
|
8784
9382
|
const counterRow = document.createElement("div");
|
|
8785
9383
|
counterRow.style.cssText = "position: relative; padding: 2px 10px 4px; text-align: right;";
|
|
8786
|
-
const counter = createCharCounter(element, textarea
|
|
9384
|
+
const counter = createCharCounter(element, textarea);
|
|
8787
9385
|
counter.style.cssText = `
|
|
8788
9386
|
position: static;
|
|
8789
9387
|
display: inline-block;
|
|
@@ -9085,10 +9683,7 @@ var TAGS = {
|
|
|
9085
9683
|
"-": ["<hr />"]
|
|
9086
9684
|
};
|
|
9087
9685
|
function outdent(str) {
|
|
9088
|
-
return str.replace(
|
|
9089
|
-
RegExp("^" + (str.match(/^(\t| )+/) || "")[0], "gm"),
|
|
9090
|
-
""
|
|
9091
|
-
);
|
|
9686
|
+
return str.replace(RegExp("^" + (str.match(/^(\t| )+/) || "")[0], "gm"), "");
|
|
9092
9687
|
}
|
|
9093
9688
|
function encodeAttr(str) {
|
|
9094
9689
|
return (str + "").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -9251,12 +9846,7 @@ function ensureMarkdownStyles() {
|
|
|
9251
9846
|
`;
|
|
9252
9847
|
document.head.appendChild(style);
|
|
9253
9848
|
}
|
|
9254
|
-
var ANCHOR_DANGEROUS_SCHEMES = [
|
|
9255
|
-
"javascript:",
|
|
9256
|
-
"data:",
|
|
9257
|
-
"vbscript:",
|
|
9258
|
-
"blob:"
|
|
9259
|
-
];
|
|
9849
|
+
var ANCHOR_DANGEROUS_SCHEMES = ["javascript:", "data:", "vbscript:", "blob:"];
|
|
9260
9850
|
var IMG_DANGEROUS_SCHEMES = ["javascript:", "vbscript:", "blob:"];
|
|
9261
9851
|
function isImgSrcDangerous(normalized) {
|
|
9262
9852
|
if (IMG_DANGEROUS_SCHEMES.some((scheme) => normalized.startsWith(scheme))) {
|
|
@@ -9316,6 +9906,117 @@ function validateMarkdown(_element, _key, _context) {
|
|
|
9316
9906
|
function updateMarkdown(_element, _fieldPath, _value, _context) {
|
|
9317
9907
|
}
|
|
9318
9908
|
|
|
9909
|
+
// src/components/registry.ts
|
|
9910
|
+
function validateHiddenElement(element, key, context) {
|
|
9911
|
+
const { scopeRoot } = context;
|
|
9912
|
+
const input = scopeRoot.querySelector(
|
|
9913
|
+
`input[type="hidden"][data-hidden-field="true"][name="${key}"]`
|
|
9914
|
+
);
|
|
9915
|
+
const raw = input?.value ?? "";
|
|
9916
|
+
if (raw === "") {
|
|
9917
|
+
const defaultVal = "default" in element ? element.default : null;
|
|
9918
|
+
return { value: defaultVal !== void 0 ? defaultVal : null, errors: [] };
|
|
9919
|
+
}
|
|
9920
|
+
return { value: deserializeHiddenValue(raw), errors: [] };
|
|
9921
|
+
}
|
|
9922
|
+
function updateHiddenField(_element, fieldPath, value, context) {
|
|
9923
|
+
const { scopeRoot } = context;
|
|
9924
|
+
const input = scopeRoot.querySelector(
|
|
9925
|
+
`input[type="hidden"][data-hidden-field="true"][name="${fieldPath}"]`
|
|
9926
|
+
);
|
|
9927
|
+
if (!input) return;
|
|
9928
|
+
input.value = serializeHiddenValue(value);
|
|
9929
|
+
}
|
|
9930
|
+
var componentRegistry = {
|
|
9931
|
+
text: {
|
|
9932
|
+
validate: validateTextElement,
|
|
9933
|
+
update: updateTextField
|
|
9934
|
+
},
|
|
9935
|
+
textarea: {
|
|
9936
|
+
validate: validateTextareaElement,
|
|
9937
|
+
update: updateTextareaField
|
|
9938
|
+
},
|
|
9939
|
+
number: {
|
|
9940
|
+
validate: validateNumberElement,
|
|
9941
|
+
update: updateNumberField
|
|
9942
|
+
},
|
|
9943
|
+
select: {
|
|
9944
|
+
validate: validateSelectElement,
|
|
9945
|
+
update: updateSelectField
|
|
9946
|
+
},
|
|
9947
|
+
switcher: {
|
|
9948
|
+
validate: validateSwitcherElement,
|
|
9949
|
+
update: updateSwitcherField
|
|
9950
|
+
},
|
|
9951
|
+
boolean: {
|
|
9952
|
+
validate: validateBooleanElement,
|
|
9953
|
+
update: updateBooleanField,
|
|
9954
|
+
ownsLabel: true
|
|
9955
|
+
},
|
|
9956
|
+
file: {
|
|
9957
|
+
validate: validateFileElement,
|
|
9958
|
+
update: updateFileField
|
|
9959
|
+
},
|
|
9960
|
+
files: {
|
|
9961
|
+
// Legacy type - delegates to file
|
|
9962
|
+
validate: validateFileElement,
|
|
9963
|
+
update: updateFileField
|
|
9964
|
+
},
|
|
9965
|
+
colour: {
|
|
9966
|
+
validate: validateColourElement,
|
|
9967
|
+
update: updateColourField
|
|
9968
|
+
},
|
|
9969
|
+
slider: {
|
|
9970
|
+
validate: validateSliderElement,
|
|
9971
|
+
update: updateSliderField
|
|
9972
|
+
},
|
|
9973
|
+
container: {
|
|
9974
|
+
validate: validateContainerElement,
|
|
9975
|
+
update: updateContainerField
|
|
9976
|
+
},
|
|
9977
|
+
group: {
|
|
9978
|
+
// Deprecated type - delegates to container
|
|
9979
|
+
validate: validateGroupElement,
|
|
9980
|
+
update: updateGroupField
|
|
9981
|
+
},
|
|
9982
|
+
table: {
|
|
9983
|
+
validate: validateTableElement,
|
|
9984
|
+
update: updateTableField
|
|
9985
|
+
},
|
|
9986
|
+
richinput: {
|
|
9987
|
+
validate: validateRichInputElement,
|
|
9988
|
+
update: updateRichInputField
|
|
9989
|
+
},
|
|
9990
|
+
hidden: {
|
|
9991
|
+
// Legacy type: `type: "hidden"` — reads/writes DOM <input type="hidden"> element
|
|
9992
|
+
validate: validateHiddenElement,
|
|
9993
|
+
update: updateHiddenField
|
|
9994
|
+
},
|
|
9995
|
+
markdown: {
|
|
9996
|
+
// Display-only element — no value, no errors, skip from form data
|
|
9997
|
+
validate: validateMarkdown,
|
|
9998
|
+
update: updateMarkdown
|
|
9999
|
+
}
|
|
10000
|
+
};
|
|
10001
|
+
function getComponentOperations(elementType) {
|
|
10002
|
+
return componentRegistry[elementType] || null;
|
|
10003
|
+
}
|
|
10004
|
+
function validateElementWithComponent(element, key, context) {
|
|
10005
|
+
const ops = getComponentOperations(element.type);
|
|
10006
|
+
if (ops && ops.validate) {
|
|
10007
|
+
return ops.validate(element, key, context);
|
|
10008
|
+
}
|
|
10009
|
+
return null;
|
|
10010
|
+
}
|
|
10011
|
+
function updateElementWithComponent(element, fieldPath, value, context) {
|
|
10012
|
+
const ops = getComponentOperations(element.type);
|
|
10013
|
+
if (ops && ops.update) {
|
|
10014
|
+
ops.update(element, fieldPath, value, context);
|
|
10015
|
+
return true;
|
|
10016
|
+
}
|
|
10017
|
+
return false;
|
|
10018
|
+
}
|
|
10019
|
+
|
|
9319
10020
|
// src/components/index.ts
|
|
9320
10021
|
function showTooltip(tooltipId, button) {
|
|
9321
10022
|
const tooltip = document.getElementById(tooltipId);
|
|
@@ -9577,6 +10278,7 @@ function createInfoButton(element) {
|
|
|
9577
10278
|
function createLabelContainer(element) {
|
|
9578
10279
|
const label = document.createElement("div");
|
|
9579
10280
|
label.className = "flex items-center mb-1";
|
|
10281
|
+
label.dataset.fbLabelRow = "";
|
|
9580
10282
|
const title = createFieldLabel(element);
|
|
9581
10283
|
label.appendChild(title);
|
|
9582
10284
|
if (element.description || element.hint) {
|
|
@@ -9623,6 +10325,9 @@ function dispatchToRenderer(element, ctx, wrapper, pathKey) {
|
|
|
9623
10325
|
renderSwitcherElement(element, ctx, wrapper, pathKey);
|
|
9624
10326
|
}
|
|
9625
10327
|
break;
|
|
10328
|
+
case "boolean":
|
|
10329
|
+
renderBooleanElement(element, ctx, wrapper, pathKey);
|
|
10330
|
+
break;
|
|
9626
10331
|
case "file":
|
|
9627
10332
|
if (isMultiple) {
|
|
9628
10333
|
renderMultipleFileElement(element, ctx, wrapper, pathKey);
|
|
@@ -9701,8 +10406,11 @@ function renderElement2(element, ctx) {
|
|
|
9701
10406
|
const wrapper = document.createElement("div");
|
|
9702
10407
|
wrapper.className = "mb-2 fb-field-wrapper";
|
|
9703
10408
|
wrapper.setAttribute("data-field-key", element.key);
|
|
9704
|
-
const
|
|
9705
|
-
|
|
10409
|
+
const ops = getComponentOperations(element.type);
|
|
10410
|
+
if (!ops?.ownsLabel) {
|
|
10411
|
+
const label = createLabelContainer(element);
|
|
10412
|
+
wrapper.appendChild(label);
|
|
10413
|
+
}
|
|
9706
10414
|
const pathKey = pathJoin(ctx.path, element.key);
|
|
9707
10415
|
dispatchToRenderer(element, ctx, wrapper, pathKey);
|
|
9708
10416
|
if (initiallyDisabled) {
|
|
@@ -9794,6 +10502,7 @@ var defaultConfig = {
|
|
|
9794
10502
|
invalidFileExtension: 'File "{name}" has unsupported format. Allowed: {formats}',
|
|
9795
10503
|
invalidFileMime: 'File "{name}": file type {type} not allowed (allowed: {mimes})',
|
|
9796
10504
|
fileTooLarge: 'File "{name}" exceeds maximum size of {maxSize}MB',
|
|
10505
|
+
uploadFailed: 'Failed to upload "{name}": {error}',
|
|
9797
10506
|
filesLimitExceeded: "{skipped} file(s) skipped: maximum {max} files allowed",
|
|
9798
10507
|
unsupportedFieldType: "Unsupported field type: {type}",
|
|
9799
10508
|
invalidOption: "Invalid option",
|
|
@@ -9869,6 +10578,7 @@ var defaultConfig = {
|
|
|
9869
10578
|
invalidFileExtension: '\u0424\u0430\u0439\u043B "{name}" \u0438\u043C\u0435\u0435\u0442 \u043D\u0435\u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043C\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442. \u0414\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u044B\u0435: {formats}',
|
|
9870
10579
|
invalidFileMime: '\u0424\u0430\u0439\u043B "{name}": \u0442\u0438\u043F \u0444\u0430\u0439\u043B\u0430 {type} \u043D\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0451\u043D (\u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u044B: {mimes})',
|
|
9871
10580
|
fileTooLarge: '\u0424\u0430\u0439\u043B "{name}" \u043F\u0440\u0435\u0432\u044B\u0448\u0430\u0435\u0442 \u043C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u044C\u043D\u044B\u0439 \u0440\u0430\u0437\u043C\u0435\u0440 {maxSize}\u041C\u0411',
|
|
10581
|
+
uploadFailed: '\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C "{name}": {error}',
|
|
9872
10582
|
filesLimitExceeded: "{skipped} \u0444\u0430\u0439\u043B(\u043E\u0432) \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D\u043E: \u043C\u0430\u043A\u0441\u0438\u043C\u0443\u043C {max} \u0444\u0430\u0439\u043B\u043E\u0432",
|
|
9873
10583
|
unsupportedFieldType: "\u041D\u0435\u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043C\u044B\u0439 \u0442\u0438\u043F \u043F\u043E\u043B\u044F: {type}",
|
|
9874
10584
|
invalidOption: "\u041D\u0435\u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435",
|
|
@@ -9933,28 +10643,52 @@ var defaultTheme = {
|
|
|
9933
10643
|
// blue-500
|
|
9934
10644
|
primaryHoverColor: "#2563eb",
|
|
9935
10645
|
// blue-600
|
|
10646
|
+
primarySoftColor: "#dbeafe",
|
|
10647
|
+
// blue-100
|
|
10648
|
+
primarySoftHoverColor: "#bfdbfe",
|
|
10649
|
+
// blue-200
|
|
9936
10650
|
errorColor: "#ef4444",
|
|
9937
10651
|
// red-500
|
|
9938
10652
|
errorHoverColor: "#dc2626",
|
|
9939
10653
|
// red-600
|
|
9940
10654
|
successColor: "#10b981",
|
|
9941
10655
|
// green-500
|
|
10656
|
+
accentColor: "#f59e0b",
|
|
10657
|
+
// amber-500
|
|
10658
|
+
accentSoftColor: "#fef3c7",
|
|
10659
|
+
// amber-100
|
|
10660
|
+
accentBorderColor: "#fde68a",
|
|
10661
|
+
// amber-200
|
|
10662
|
+
accentTextColor: "#92400e",
|
|
10663
|
+
// amber-800
|
|
9942
10664
|
borderColor: "#d1d5db",
|
|
9943
10665
|
// gray-300
|
|
9944
10666
|
borderHoverColor: "#9ca3af",
|
|
9945
10667
|
// gray-400
|
|
9946
10668
|
borderFocusColor: "#3b82f6",
|
|
9947
10669
|
// blue-500
|
|
10670
|
+
borderStrongColor: "#9ca3af",
|
|
10671
|
+
// gray-400
|
|
9948
10672
|
backgroundColor: "#ffffff",
|
|
9949
10673
|
// white
|
|
9950
10674
|
backgroundHoverColor: "#f9fafb",
|
|
9951
10675
|
// gray-50
|
|
9952
10676
|
backgroundReadonlyColor: "#f3f4f6",
|
|
9953
10677
|
// gray-100
|
|
10678
|
+
pageBackgroundColor: "#f9fafb",
|
|
10679
|
+
// gray-50
|
|
10680
|
+
surfaceSoftColor: "#eff6ff",
|
|
10681
|
+
// blue-50
|
|
10682
|
+
surfaceTintColor: "#f8fafc",
|
|
10683
|
+
// slate-50
|
|
9954
10684
|
textColor: "#1f2937",
|
|
9955
10685
|
// gray-800
|
|
9956
10686
|
textSecondaryColor: "#6b7280",
|
|
9957
10687
|
// gray-500
|
|
10688
|
+
textMutedColor: "#9ca3af",
|
|
10689
|
+
// gray-400
|
|
10690
|
+
textFaintColor: "#cbd5e1",
|
|
10691
|
+
// slate-300
|
|
9958
10692
|
textPlaceholderColor: "#9ca3af",
|
|
9959
10693
|
// gray-400
|
|
9960
10694
|
textDisabledColor: "#d1d5db",
|
|
@@ -9997,6 +10731,12 @@ var defaultTheme = {
|
|
|
9997
10731
|
// 4px (compact density v2)
|
|
9998
10732
|
borderRadius: "0.5rem",
|
|
9999
10733
|
// rounded-lg (8px)
|
|
10734
|
+
borderRadiusSmall: "0.375rem",
|
|
10735
|
+
// 6px
|
|
10736
|
+
borderRadiusLarge: "0.75rem",
|
|
10737
|
+
// 12px
|
|
10738
|
+
borderRadiusXLarge: "1rem",
|
|
10739
|
+
// 16px
|
|
10000
10740
|
borderWidth: "1px",
|
|
10001
10741
|
// Typography
|
|
10002
10742
|
fontSize: "0.875rem",
|
|
@@ -10008,13 +10748,31 @@ var defaultTheme = {
|
|
|
10008
10748
|
fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
|
10009
10749
|
fontWeightNormal: "400",
|
|
10010
10750
|
fontWeightMedium: "500",
|
|
10751
|
+
lineHeight: "1.5",
|
|
10752
|
+
// Shadows
|
|
10753
|
+
shadowCard: "0 1px 2px rgba(15,23,42,.04), 0 1px 0 rgba(15,23,42,.02)",
|
|
10754
|
+
shadowPopover: "0 12px 32px -12px rgba(15,23,42,.18), 0 4px 12px -6px rgba(15,23,42,.08)",
|
|
10011
10755
|
// Focus ring
|
|
10012
10756
|
focusRingWidth: "2px",
|
|
10013
10757
|
focusRingColor: "#3b82f6",
|
|
10014
10758
|
// blue-500
|
|
10015
10759
|
focusRingOpacity: "0.5",
|
|
10016
10760
|
// Transitions
|
|
10017
|
-
transitionDuration: "200ms"
|
|
10761
|
+
transitionDuration: "200ms",
|
|
10762
|
+
// Slide-card defaults — flat-white to match every other item card. The
|
|
10763
|
+
// Picaz theme overrides this with a gradient + shadow to lift the slides.
|
|
10764
|
+
slideCardBg: "#ffffff",
|
|
10765
|
+
slideCardShadow: "0 1px 2px rgba(15,23,42,.04), 0 1px 0 rgba(15,23,42,.02)",
|
|
10766
|
+
slideCardRadius: "0.5rem",
|
|
10767
|
+
// matches borderRadius
|
|
10768
|
+
slideCardMinHeight: "0",
|
|
10769
|
+
slideCardPadding: "12px",
|
|
10770
|
+
// Section-label defaults — same look as the regular field label. Themes
|
|
10771
|
+
// that want the "ПРЕИМУЩЕСТВА" caps style override these three vars.
|
|
10772
|
+
labelSectionFontSize: "0.875rem",
|
|
10773
|
+
// matches fontSize
|
|
10774
|
+
labelSectionLetterSpacing: "normal",
|
|
10775
|
+
labelSectionTextTransform: "none"
|
|
10018
10776
|
};
|
|
10019
10777
|
function generateCSSVariables(theme) {
|
|
10020
10778
|
const mergedTheme = { ...defaultTheme, ...theme };
|
|
@@ -10026,6 +10784,7 @@ function generateCSSVariables(theme) {
|
|
|
10026
10784
|
return cssVars.join("\n");
|
|
10027
10785
|
}
|
|
10028
10786
|
function injectThemeVariables(container, theme) {
|
|
10787
|
+
ensureThemingHooks(container.ownerDocument || document);
|
|
10029
10788
|
const cssVariables = generateCSSVariables(theme);
|
|
10030
10789
|
let styleTag = container.querySelector(
|
|
10031
10790
|
"style[data-fb-theme]"
|
|
@@ -10088,137 +10847,50 @@ var exampleThemes = {
|
|
|
10088
10847
|
fontSize: "16px",
|
|
10089
10848
|
fontSizeSmall: "14px",
|
|
10090
10849
|
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif'
|
|
10091
|
-
}
|
|
10092
|
-
};
|
|
10093
|
-
|
|
10094
|
-
// src/utils/styles.ts
|
|
10095
|
-
function applyActionButtonStyles(button, isFormLevel = false) {
|
|
10096
|
-
button.style.cssText = `
|
|
10097
|
-
background-color: var(--fb-action-bg-color);
|
|
10098
|
-
color: var(--fb-action-text-color);
|
|
10099
|
-
border: var(--fb-border-width) solid var(--fb-action-border-color);
|
|
10100
|
-
padding: ${isFormLevel ? "0.5rem 1rem" : "0.5rem 0.75rem"};
|
|
10101
|
-
font-size: var(--fb-font-size);
|
|
10102
|
-
font-weight: var(--fb-font-weight-medium);
|
|
10103
|
-
border-radius: var(--fb-border-radius);
|
|
10104
|
-
transition: all var(--fb-transition-duration);
|
|
10105
|
-
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
|
10106
|
-
`;
|
|
10107
|
-
button.addEventListener("mouseenter", () => {
|
|
10108
|
-
button.style.backgroundColor = "var(--fb-action-hover-bg-color)";
|
|
10109
|
-
button.style.borderColor = "var(--fb-action-hover-border-color)";
|
|
10110
|
-
});
|
|
10111
|
-
button.addEventListener("mouseleave", () => {
|
|
10112
|
-
button.style.backgroundColor = "var(--fb-action-bg-color)";
|
|
10113
|
-
button.style.borderColor = "var(--fb-action-border-color)";
|
|
10114
|
-
});
|
|
10115
|
-
}
|
|
10116
|
-
|
|
10117
|
-
// src/components/registry.ts
|
|
10118
|
-
function validateHiddenElement(element, key, context) {
|
|
10119
|
-
const { scopeRoot } = context;
|
|
10120
|
-
const input = scopeRoot.querySelector(
|
|
10121
|
-
`input[type="hidden"][data-hidden-field="true"][name="${key}"]`
|
|
10122
|
-
);
|
|
10123
|
-
const raw = input?.value ?? "";
|
|
10124
|
-
if (raw === "") {
|
|
10125
|
-
const defaultVal = "default" in element ? element.default : null;
|
|
10126
|
-
return { value: defaultVal !== void 0 ? defaultVal : null, errors: [] };
|
|
10127
|
-
}
|
|
10128
|
-
return { value: deserializeHiddenValue(raw), errors: [] };
|
|
10129
|
-
}
|
|
10130
|
-
function updateHiddenField(_element, fieldPath, value, context) {
|
|
10131
|
-
const { scopeRoot } = context;
|
|
10132
|
-
const input = scopeRoot.querySelector(
|
|
10133
|
-
`input[type="hidden"][data-hidden-field="true"][name="${fieldPath}"]`
|
|
10134
|
-
);
|
|
10135
|
-
if (!input) return;
|
|
10136
|
-
input.value = serializeHiddenValue(value);
|
|
10137
|
-
}
|
|
10138
|
-
var componentRegistry = {
|
|
10139
|
-
text: {
|
|
10140
|
-
validate: validateTextElement,
|
|
10141
|
-
update: updateTextField
|
|
10142
|
-
},
|
|
10143
|
-
textarea: {
|
|
10144
|
-
validate: validateTextareaElement,
|
|
10145
|
-
update: updateTextareaField
|
|
10146
|
-
},
|
|
10147
|
-
number: {
|
|
10148
|
-
validate: validateNumberElement,
|
|
10149
|
-
update: updateNumberField
|
|
10150
|
-
},
|
|
10151
|
-
select: {
|
|
10152
|
-
validate: validateSelectElement,
|
|
10153
|
-
update: updateSelectField
|
|
10154
|
-
},
|
|
10155
|
-
switcher: {
|
|
10156
|
-
validate: validateSwitcherElement,
|
|
10157
|
-
update: updateSwitcherField
|
|
10158
|
-
},
|
|
10159
|
-
file: {
|
|
10160
|
-
validate: validateFileElement,
|
|
10161
|
-
update: updateFileField
|
|
10162
|
-
},
|
|
10163
|
-
files: {
|
|
10164
|
-
// Legacy type - delegates to file
|
|
10165
|
-
validate: validateFileElement,
|
|
10166
|
-
update: updateFileField
|
|
10167
|
-
},
|
|
10168
|
-
colour: {
|
|
10169
|
-
validate: validateColourElement,
|
|
10170
|
-
update: updateColourField
|
|
10171
|
-
},
|
|
10172
|
-
slider: {
|
|
10173
|
-
validate: validateSliderElement,
|
|
10174
|
-
update: updateSliderField
|
|
10175
|
-
},
|
|
10176
|
-
container: {
|
|
10177
|
-
validate: validateContainerElement,
|
|
10178
|
-
update: updateContainerField
|
|
10179
|
-
},
|
|
10180
|
-
group: {
|
|
10181
|
-
// Deprecated type - delegates to container
|
|
10182
|
-
validate: validateGroupElement,
|
|
10183
|
-
update: updateGroupField
|
|
10184
|
-
},
|
|
10185
|
-
table: {
|
|
10186
|
-
validate: validateTableElement,
|
|
10187
|
-
update: updateTableField
|
|
10188
|
-
},
|
|
10189
|
-
richinput: {
|
|
10190
|
-
validate: validateRichInputElement,
|
|
10191
|
-
update: updateRichInputField
|
|
10192
|
-
},
|
|
10193
|
-
hidden: {
|
|
10194
|
-
// Legacy type: `type: "hidden"` — reads/writes DOM <input type="hidden"> element
|
|
10195
|
-
validate: validateHiddenElement,
|
|
10196
|
-
update: updateHiddenField
|
|
10197
10850
|
},
|
|
10198
|
-
|
|
10199
|
-
|
|
10200
|
-
|
|
10201
|
-
|
|
10851
|
+
// Picaz wizard design tokens — derived from the Picaz Wizard mockups.
|
|
10852
|
+
// Pairs with the host-side .card / .section-num / .lede chrome that wraps the form.
|
|
10853
|
+
picaz: {
|
|
10854
|
+
...defaultTheme,
|
|
10855
|
+
primaryColor: "#2f5bea",
|
|
10856
|
+
primaryHoverColor: "#2349c8",
|
|
10857
|
+
primarySoftColor: "#eaf0ff",
|
|
10858
|
+
primarySoftHoverColor: "#d6e0ff",
|
|
10859
|
+
errorColor: "#ef4444",
|
|
10860
|
+
successColor: "#16a34a",
|
|
10861
|
+
accentColor: "#ffb020",
|
|
10862
|
+
accentSoftColor: "#fff7e6",
|
|
10863
|
+
accentBorderColor: "#fde7b5",
|
|
10864
|
+
accentTextColor: "#92400e",
|
|
10865
|
+
borderColor: "#e3e8f0",
|
|
10866
|
+
borderHoverColor: "#cdd6e3",
|
|
10867
|
+
borderFocusColor: "#2f5bea",
|
|
10868
|
+
borderStrongColor: "#cdd6e3",
|
|
10869
|
+
backgroundColor: "#ffffff",
|
|
10870
|
+
backgroundHoverColor: "#f3f7ff",
|
|
10871
|
+
pageBackgroundColor: "#f6f8fb",
|
|
10872
|
+
surfaceSoftColor: "#eef4ff",
|
|
10873
|
+
surfaceTintColor: "#f3f7ff",
|
|
10874
|
+
textColor: "#0f172a",
|
|
10875
|
+
textSecondaryColor: "#334155",
|
|
10876
|
+
textMutedColor: "#64748b",
|
|
10877
|
+
textFaintColor: "#94a3b8",
|
|
10878
|
+
textPlaceholderColor: "#94a3b8",
|
|
10879
|
+
buttonBgColor: "#2f5bea",
|
|
10880
|
+
buttonHoverBgColor: "#2349c8",
|
|
10881
|
+
fileUploadBgColor: "#fafcff",
|
|
10882
|
+
fileUploadBorderColor: "#cdd6e3",
|
|
10883
|
+
fileUploadHoverBorderColor: "#2f5bea",
|
|
10884
|
+
borderRadius: "12px",
|
|
10885
|
+
borderRadiusSmall: "8px",
|
|
10886
|
+
borderRadiusLarge: "16px",
|
|
10887
|
+
borderRadiusXLarge: "22px",
|
|
10888
|
+
fontFamily: '"Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
|
|
10889
|
+
shadowCard: "0 1px 2px rgba(15,23,42,.04), 0 1px 0 rgba(15,23,42,.02)",
|
|
10890
|
+
shadowPopover: "0 12px 32px -12px rgba(15,23,42,.18), 0 4px 12px -6px rgba(15,23,42,.08)",
|
|
10891
|
+
focusRingColor: "#2f5bea"
|
|
10202
10892
|
}
|
|
10203
10893
|
};
|
|
10204
|
-
function getComponentOperations(elementType) {
|
|
10205
|
-
return componentRegistry[elementType] || null;
|
|
10206
|
-
}
|
|
10207
|
-
function validateElementWithComponent(element, key, context) {
|
|
10208
|
-
const ops = getComponentOperations(element.type);
|
|
10209
|
-
if (ops && ops.validate) {
|
|
10210
|
-
return ops.validate(element, key, context);
|
|
10211
|
-
}
|
|
10212
|
-
return null;
|
|
10213
|
-
}
|
|
10214
|
-
function updateElementWithComponent(element, fieldPath, value, context) {
|
|
10215
|
-
const ops = getComponentOperations(element.type);
|
|
10216
|
-
if (ops && ops.update) {
|
|
10217
|
-
ops.update(element, fieldPath, value, context);
|
|
10218
|
-
return true;
|
|
10219
|
-
}
|
|
10220
|
-
return false;
|
|
10221
|
-
}
|
|
10222
10894
|
|
|
10223
10895
|
// src/instance/FormBuilderInstance.ts
|
|
10224
10896
|
var FormBuilderInstance = class {
|
|
@@ -10343,26 +11015,37 @@ var FormBuilderInstance = class {
|
|
|
10343
11015
|
}
|
|
10344
11016
|
}
|
|
10345
11017
|
/**
|
|
10346
|
-
* Find the DOM element corresponding to a field path (instance-scoped)
|
|
11018
|
+
* Find the DOM element corresponding to a field path (instance-scoped).
|
|
11019
|
+
*
|
|
11020
|
+
* Strategy:
|
|
11021
|
+
* 1. Try a `[name="…"]` lookup first — works for any field that renders
|
|
11022
|
+
* an input/hidden with the path as its name, in either mode. Some
|
|
11023
|
+
* readonly renderers still emit a hidden input (boolean, switcher),
|
|
11024
|
+
* so this path must run regardless of `state.config.readonly`. A
|
|
11025
|
+
* prior version gated this on edit mode only, which made
|
|
11026
|
+
* `updateField` / `setFormData` silently miss readonly boolean
|
|
11027
|
+
* fields whose component also opts out of the standard label row
|
|
11028
|
+
* (`ownsLabel: true`).
|
|
11029
|
+
* 2. If no input matched, fall back to locating the field wrapper by
|
|
11030
|
+
* its visible label text — needed for readonly previews that don't
|
|
11031
|
+
* emit any `name=` attribute (e.g. file/markdown previews).
|
|
10347
11032
|
*/
|
|
10348
11033
|
findFormElementByFieldPath(fieldPath) {
|
|
10349
11034
|
if (!this.state.formRoot) return null;
|
|
10350
|
-
|
|
10351
|
-
|
|
10352
|
-
|
|
11035
|
+
let element = this.state.formRoot.querySelector(
|
|
11036
|
+
`[name="${fieldPath}"]`
|
|
11037
|
+
);
|
|
11038
|
+
if (element) return element;
|
|
11039
|
+
const variations = [
|
|
11040
|
+
fieldPath,
|
|
11041
|
+
fieldPath.replace(/\[(\d+)\]/g, "[$1]"),
|
|
11042
|
+
fieldPath.replace(/\./g, "[") + "]".repeat((fieldPath.match(/\./g) || []).length)
|
|
11043
|
+
];
|
|
11044
|
+
for (const variation of variations) {
|
|
11045
|
+
element = this.state.formRoot.querySelector(
|
|
11046
|
+
`[name="${variation}"]`
|
|
10353
11047
|
);
|
|
10354
11048
|
if (element) return element;
|
|
10355
|
-
const variations = [
|
|
10356
|
-
fieldPath,
|
|
10357
|
-
fieldPath.replace(/\[(\d+)\]/g, "[$1]"),
|
|
10358
|
-
fieldPath.replace(/\./g, "[") + "]".repeat((fieldPath.match(/\./g) || []).length)
|
|
10359
|
-
];
|
|
10360
|
-
for (const variation of variations) {
|
|
10361
|
-
element = this.state.formRoot.querySelector(
|
|
10362
|
-
`[name="${variation}"]`
|
|
10363
|
-
);
|
|
10364
|
-
if (element) return element;
|
|
10365
|
-
}
|
|
10366
11049
|
}
|
|
10367
11050
|
const schemaElement = this.findSchemaElement(fieldPath);
|
|
10368
11051
|
if (!schemaElement) return null;
|