@dmitryvim/form-builder 0.2.34 → 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 +411 -162
- package/dist/browser/formbuilder.v0.3.0.min.js +1411 -0
- package/dist/cjs/index.cjs +1003 -399
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/index.js +995 -398
- package/dist/esm/index.js.map +1 -1
- package/dist/form-builder.js +411 -162
- package/dist/types/components/boolean.d.ts +4 -0
- 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/index.d.ts +1 -1
- package/dist/types/types/schema.d.ts +40 -1
- package/dist/types/utils/styles.d.ts +39 -0
- package/package.json +1 -1
- package/dist/browser/formbuilder.v0.2.34.min.js +0 -1162
package/dist/esm/index.js
CHANGED
|
@@ -399,6 +399,81 @@ function deepEqual(a, b) {
|
|
|
399
399
|
}
|
|
400
400
|
|
|
401
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
|
+
}
|
|
402
477
|
function mountCounterInLabel(wrapper, counter) {
|
|
403
478
|
const labelRow = wrapper.querySelector(
|
|
404
479
|
":scope > [data-fb-label-row]"
|
|
@@ -560,46 +635,94 @@ function applyActionButtonStyles(button, isFormLevel = false) {
|
|
|
560
635
|
}
|
|
561
636
|
|
|
562
637
|
// src/components/text.ts
|
|
563
|
-
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) {
|
|
564
705
|
const counter = document.createElement("span");
|
|
565
706
|
counter.className = "char-counter";
|
|
566
707
|
counter.style.cssText = `
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
708
|
+
margin-top: 4px;
|
|
709
|
+
padding-right: 12px;
|
|
710
|
+
text-align: right;
|
|
570
711
|
font-size: var(--fb-font-size-small);
|
|
571
|
-
|
|
712
|
+
line-height: 1;
|
|
713
|
+
color: var(--fb-error-color);
|
|
572
714
|
pointer-events: none;
|
|
573
|
-
|
|
574
|
-
padding: 0 4px;
|
|
715
|
+
display: none;
|
|
575
716
|
`;
|
|
576
717
|
const updateCounter = () => {
|
|
577
718
|
const len = input.value.length;
|
|
578
|
-
const min = element.minLength;
|
|
579
719
|
const max = element.maxLength;
|
|
580
|
-
if (
|
|
581
|
-
counter.textContent = "";
|
|
582
|
-
return;
|
|
583
|
-
}
|
|
584
|
-
if (len === 0 || min != null && len < min) {
|
|
585
|
-
if (min != null && max != null) {
|
|
586
|
-
counter.textContent = `${min}-${max}`;
|
|
587
|
-
} else if (max != null) {
|
|
588
|
-
counter.textContent = `\u2264${max}`;
|
|
589
|
-
} else if (min != null) {
|
|
590
|
-
counter.textContent = `\u2265${min}`;
|
|
591
|
-
}
|
|
592
|
-
counter.style.color = "var(--fb-text-secondary-color)";
|
|
593
|
-
} else if (max != null && len > max) {
|
|
720
|
+
if (max != null && len > max) {
|
|
594
721
|
counter.textContent = `${len}/${max}`;
|
|
595
|
-
counter.style.
|
|
722
|
+
counter.style.display = "block";
|
|
596
723
|
} else {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
} else {
|
|
600
|
-
counter.textContent = `${len}`;
|
|
601
|
-
}
|
|
602
|
-
counter.style.color = "var(--fb-text-secondary-color)";
|
|
724
|
+
counter.textContent = "";
|
|
725
|
+
counter.style.display = "none";
|
|
603
726
|
}
|
|
604
727
|
};
|
|
605
728
|
input.addEventListener("input", updateCounter);
|
|
@@ -612,26 +735,32 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
|
|
|
612
735
|
const inputWrapper = document.createElement("div");
|
|
613
736
|
inputWrapper.style.cssText = "position: relative;";
|
|
614
737
|
const hasCharCounter = !readonly && (element.minLength != null || element.maxLength != null);
|
|
615
|
-
const textInput = document.createElement("
|
|
616
|
-
textInput.
|
|
738
|
+
const textInput = document.createElement("textarea");
|
|
739
|
+
textInput.rows = 1;
|
|
617
740
|
textInput.className = "w-full rounded-lg";
|
|
618
741
|
textInput.style.cssText = `
|
|
619
742
|
padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
|
|
620
|
-
${hasCharCounter ? "padding-right: 60px;" : ""}
|
|
621
743
|
border: var(--fb-border-width) solid var(--fb-border-color);
|
|
622
744
|
border-radius: var(--fb-border-radius);
|
|
623
745
|
background-color: ${readonly ? "var(--fb-background-readonly-color)" : "var(--fb-background-color)"};
|
|
624
746
|
color: var(--fb-text-color);
|
|
625
747
|
font-size: var(--fb-font-size);
|
|
626
748
|
font-family: var(--fb-font-family);
|
|
749
|
+
line-height: var(--fb-line-height, 1.5);
|
|
627
750
|
transition: all var(--fb-transition-duration) ease-in-out;
|
|
628
751
|
width: 100%;
|
|
629
752
|
box-sizing: border-box;
|
|
753
|
+
resize: none;
|
|
754
|
+
overflow: hidden;
|
|
755
|
+
word-break: break-word;
|
|
756
|
+
overflow-wrap: anywhere;
|
|
630
757
|
`;
|
|
631
758
|
textInput.name = pathKey;
|
|
632
759
|
textInput.placeholder = element.placeholder || "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442";
|
|
633
760
|
textInput.value = ctx.prefill[element.key] || element.default || "";
|
|
634
761
|
textInput.readOnly = readonly;
|
|
762
|
+
applySingleLineMode(textInput);
|
|
763
|
+
applyAutoExpand(textInput);
|
|
635
764
|
if (!readonly) {
|
|
636
765
|
textInput.addEventListener("focus", () => {
|
|
637
766
|
textInput.style.borderColor = "var(--fb-border-focus-color)";
|
|
@@ -663,7 +792,7 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
|
|
|
663
792
|
}
|
|
664
793
|
inputWrapper.appendChild(textInput);
|
|
665
794
|
if (hasCharCounter) {
|
|
666
|
-
const counter = createCharCounter(element, textInput
|
|
795
|
+
const counter = createCharCounter(element, textInput);
|
|
667
796
|
inputWrapper.appendChild(counter);
|
|
668
797
|
}
|
|
669
798
|
wrapper.appendChild(inputWrapper);
|
|
@@ -673,132 +802,83 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
673
802
|
const readonly = isElementReadonly(element, state, ctx);
|
|
674
803
|
const prefillValues = ctx.prefill[element.key] || [];
|
|
675
804
|
const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
|
|
676
|
-
const hasCharCounter = !readonly && (element.minLength != null || element.maxLength != null);
|
|
677
805
|
const minCount = element.minCount ?? 1;
|
|
678
806
|
const maxCount = element.maxCount ?? Infinity;
|
|
679
807
|
while (values.length < minCount) {
|
|
680
808
|
values.push(element.default || "");
|
|
681
809
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
810
|
+
ensureChipStyles(document);
|
|
811
|
+
const list = document.createElement("div");
|
|
812
|
+
list.className = "fb-chip-list";
|
|
813
|
+
wrapper.appendChild(list);
|
|
685
814
|
function updateIndices() {
|
|
686
|
-
const items =
|
|
687
|
-
items.forEach((
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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}`;
|
|
691
822
|
}
|
|
692
823
|
});
|
|
693
824
|
}
|
|
694
|
-
function
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
const
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
font-size: var(--fb-font-size);
|
|
709
|
-
font-family: var(--fb-font-family);
|
|
710
|
-
transition: all var(--fb-transition-duration) ease-in-out;
|
|
711
|
-
width: 100%;
|
|
712
|
-
box-sizing: border-box;
|
|
713
|
-
`;
|
|
714
|
-
textInput.placeholder = element.placeholder || t("placeholderText", state);
|
|
715
|
-
textInput.value = value;
|
|
716
|
-
textInput.readOnly = readonly;
|
|
717
|
-
if (!readonly) {
|
|
718
|
-
textInput.addEventListener("focus", () => {
|
|
719
|
-
textInput.style.borderColor = "var(--fb-border-focus-color)";
|
|
720
|
-
textInput.style.outline = `var(--fb-focus-ring-width) solid var(--fb-focus-ring-color)`;
|
|
721
|
-
textInput.style.outlineOffset = "0";
|
|
722
|
-
});
|
|
723
|
-
textInput.addEventListener("blur", () => {
|
|
724
|
-
textInput.style.borderColor = "var(--fb-border-color)";
|
|
725
|
-
textInput.style.outline = "none";
|
|
726
|
-
});
|
|
727
|
-
textInput.addEventListener("mouseenter", () => {
|
|
728
|
-
if (document.activeElement !== textInput) {
|
|
729
|
-
textInput.style.borderColor = "var(--fb-border-hover-color)";
|
|
730
|
-
}
|
|
731
|
-
});
|
|
732
|
-
textInput.addEventListener("mouseleave", () => {
|
|
733
|
-
if (document.activeElement !== textInput) {
|
|
734
|
-
textInput.style.borderColor = "var(--fb-border-color)";
|
|
735
|
-
}
|
|
736
|
-
});
|
|
737
|
-
}
|
|
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);
|
|
738
839
|
if (!readonly && ctx.instance) {
|
|
739
840
|
const handleChange = () => {
|
|
740
|
-
|
|
741
|
-
|
|
841
|
+
ctx.instance.triggerOnChange(
|
|
842
|
+
input.name,
|
|
843
|
+
input.value === "" ? null : input.value
|
|
844
|
+
);
|
|
742
845
|
};
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
}
|
|
746
|
-
inputContainer.appendChild(textInput);
|
|
747
|
-
if (hasCharCounter) {
|
|
748
|
-
const counter = createCharCounter(element, textInput, false);
|
|
749
|
-
inputContainer.appendChild(counter);
|
|
846
|
+
input.addEventListener("blur", handleChange);
|
|
847
|
+
input.addEventListener("input", handleChange);
|
|
750
848
|
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
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);
|
|
756
871
|
}
|
|
872
|
+
list.appendChild(chip);
|
|
757
873
|
updateIndices();
|
|
758
|
-
return
|
|
874
|
+
return chip;
|
|
759
875
|
}
|
|
760
876
|
function updateRemoveButtons() {
|
|
761
877
|
if (readonly) return;
|
|
762
|
-
const
|
|
763
|
-
const
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
".remove-item-btn"
|
|
767
|
-
);
|
|
768
|
-
if (!removeBtn) {
|
|
769
|
-
removeBtn = document.createElement("button");
|
|
770
|
-
removeBtn.type = "button";
|
|
771
|
-
removeBtn.className = "remove-item-btn px-2 py-1 rounded";
|
|
772
|
-
removeBtn.style.cssText = `
|
|
773
|
-
color: var(--fb-error-color);
|
|
774
|
-
background-color: transparent;
|
|
775
|
-
transition: background-color var(--fb-transition-duration);
|
|
776
|
-
`;
|
|
777
|
-
removeBtn.innerHTML = "\u2715";
|
|
778
|
-
removeBtn.addEventListener("mouseenter", () => {
|
|
779
|
-
removeBtn.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
780
|
-
});
|
|
781
|
-
removeBtn.addEventListener("mouseleave", () => {
|
|
782
|
-
removeBtn.style.backgroundColor = "transparent";
|
|
783
|
-
});
|
|
784
|
-
removeBtn.onclick = () => {
|
|
785
|
-
const currentIndex = Array.from(container.children).indexOf(
|
|
786
|
-
item
|
|
787
|
-
);
|
|
788
|
-
if (container.children.length > minCount) {
|
|
789
|
-
values.splice(currentIndex, 1);
|
|
790
|
-
item.remove();
|
|
791
|
-
updateIndices();
|
|
792
|
-
updateAddButton();
|
|
793
|
-
updateRemoveButtons();
|
|
794
|
-
}
|
|
795
|
-
};
|
|
796
|
-
item.appendChild(removeBtn);
|
|
797
|
-
}
|
|
798
|
-
const disabled = currentCount <= minCount;
|
|
799
|
-
removeBtn.disabled = disabled;
|
|
800
|
-
removeBtn.style.opacity = disabled ? "0.5" : "1";
|
|
801
|
-
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;
|
|
802
882
|
});
|
|
803
883
|
}
|
|
804
884
|
let addUpdate = null;
|
|
@@ -807,7 +887,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
807
887
|
"text",
|
|
808
888
|
() => {
|
|
809
889
|
values.push(element.default || "");
|
|
810
|
-
|
|
890
|
+
addChip(element.default || "");
|
|
811
891
|
updateAddButton();
|
|
812
892
|
updateRemoveButtons();
|
|
813
893
|
},
|
|
@@ -820,7 +900,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
820
900
|
function updateAddButton() {
|
|
821
901
|
if (addUpdate) addUpdate(values.length, maxCount);
|
|
822
902
|
}
|
|
823
|
-
values.forEach((value) =>
|
|
903
|
+
values.forEach((value) => addChip(value));
|
|
824
904
|
updateAddButton();
|
|
825
905
|
updateRemoveButtons();
|
|
826
906
|
}
|
|
@@ -843,10 +923,12 @@ function validateTextElement(element, key, context) {
|
|
|
843
923
|
font-size: var(--fb-font-size-small);
|
|
844
924
|
margin-top: 0.25rem;
|
|
845
925
|
`;
|
|
846
|
-
|
|
847
|
-
|
|
926
|
+
const chipAncestor = input.closest?.(".fb-chip");
|
|
927
|
+
const anchor = chipAncestor || input;
|
|
928
|
+
if (anchor.nextSibling) {
|
|
929
|
+
anchor.parentNode?.insertBefore(errorElement, anchor.nextSibling);
|
|
848
930
|
} else {
|
|
849
|
-
|
|
931
|
+
anchor.parentNode?.appendChild(errorElement);
|
|
850
932
|
}
|
|
851
933
|
}
|
|
852
934
|
errorElement.textContent = errorMessage;
|
|
@@ -895,7 +977,7 @@ function validateTextElement(element, key, context) {
|
|
|
895
977
|
}
|
|
896
978
|
};
|
|
897
979
|
if (element.multiple) {
|
|
898
|
-
const inputs = scopeRoot.querySelectorAll(`[name^="${key}["]`);
|
|
980
|
+
const inputs = scopeRoot.querySelectorAll(`[name^="${key}\\["]`);
|
|
899
981
|
const values = [];
|
|
900
982
|
const rawValues = [];
|
|
901
983
|
inputs.forEach((input, index) => {
|
|
@@ -944,12 +1026,14 @@ function updateTextField(element, fieldPath, value, context) {
|
|
|
944
1026
|
);
|
|
945
1027
|
return;
|
|
946
1028
|
}
|
|
947
|
-
const inputs = scopeRoot.querySelectorAll(`[name^="${fieldPath}["]`);
|
|
1029
|
+
const inputs = scopeRoot.querySelectorAll(`[name^="${fieldPath}\\["]`);
|
|
948
1030
|
inputs.forEach((input, index) => {
|
|
949
1031
|
if (index < value.length) {
|
|
950
1032
|
input.value = value[index] != null ? String(value[index]) : "";
|
|
951
1033
|
input.classList.remove("invalid");
|
|
952
1034
|
input.title = "";
|
|
1035
|
+
clearFieldError(input);
|
|
1036
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
953
1037
|
}
|
|
954
1038
|
});
|
|
955
1039
|
if (value.length !== inputs.length) {
|
|
@@ -963,26 +1047,15 @@ function updateTextField(element, fieldPath, value, context) {
|
|
|
963
1047
|
input.value = value != null ? String(value) : "";
|
|
964
1048
|
input.classList.remove("invalid");
|
|
965
1049
|
input.title = "";
|
|
1050
|
+
clearFieldError(input);
|
|
1051
|
+
if (input instanceof HTMLTextAreaElement) {
|
|
1052
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1053
|
+
}
|
|
966
1054
|
}
|
|
967
1055
|
}
|
|
968
1056
|
}
|
|
969
1057
|
|
|
970
1058
|
// src/components/textarea.ts
|
|
971
|
-
function applyAutoExpand(textarea) {
|
|
972
|
-
textarea.style.overflow = "hidden";
|
|
973
|
-
textarea.style.resize = "none";
|
|
974
|
-
const lineCount = (textarea.value.match(/\n/g) || []).length + 1;
|
|
975
|
-
textarea.rows = Math.max(1, lineCount);
|
|
976
|
-
const resize = () => {
|
|
977
|
-
if (!textarea.isConnected) return;
|
|
978
|
-
textarea.style.height = "0";
|
|
979
|
-
textarea.style.height = `${textarea.scrollHeight}px`;
|
|
980
|
-
};
|
|
981
|
-
textarea.addEventListener("input", resize);
|
|
982
|
-
setTimeout(() => {
|
|
983
|
-
if (textarea.isConnected) resize();
|
|
984
|
-
}, 0);
|
|
985
|
-
}
|
|
986
1059
|
function renderTextareaElement(element, ctx, wrapper, pathKey) {
|
|
987
1060
|
const state = ctx.state;
|
|
988
1061
|
const readonly = isElementReadonly(element, state, ctx);
|
|
@@ -1013,7 +1086,7 @@ function renderTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
1013
1086
|
}
|
|
1014
1087
|
textareaWrapper.appendChild(textareaInput);
|
|
1015
1088
|
if (!readonly && (element.minLength != null || element.maxLength != null)) {
|
|
1016
|
-
const counter = createCharCounter(element, textareaInput
|
|
1089
|
+
const counter = createCharCounter(element, textareaInput);
|
|
1017
1090
|
textareaWrapper.appendChild(counter);
|
|
1018
1091
|
}
|
|
1019
1092
|
wrapper.appendChild(textareaWrapper);
|
|
@@ -1069,7 +1142,7 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
1069
1142
|
}
|
|
1070
1143
|
textareaContainer.appendChild(textareaInput);
|
|
1071
1144
|
if (!readonly && (element.minLength != null || element.maxLength != null)) {
|
|
1072
|
-
const counter = createCharCounter(element, textareaInput
|
|
1145
|
+
const counter = createCharCounter(element, textareaInput);
|
|
1073
1146
|
textareaContainer.appendChild(counter);
|
|
1074
1147
|
}
|
|
1075
1148
|
itemWrapper.appendChild(textareaContainer);
|
|
@@ -1163,6 +1236,89 @@ function updateTextareaField(element, fieldPath, value, context) {
|
|
|
1163
1236
|
}
|
|
1164
1237
|
|
|
1165
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
|
+
}
|
|
1166
1322
|
function createNumberRangeHint(element, input) {
|
|
1167
1323
|
const hint = document.createElement("span");
|
|
1168
1324
|
hint.className = "number-range-hint";
|
|
@@ -1209,14 +1365,6 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1209
1365
|
inputWrapper.style.cssText = "position: relative;";
|
|
1210
1366
|
const numberInput = document.createElement("input");
|
|
1211
1367
|
numberInput.type = "number";
|
|
1212
|
-
numberInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
|
|
1213
|
-
numberInput.style.cssText = `
|
|
1214
|
-
padding: var(--fb-input-padding-y) 60px var(--fb-input-padding-y) var(--fb-input-padding-x);
|
|
1215
|
-
font-size: var(--fb-font-size);
|
|
1216
|
-
font-family: var(--fb-font-family);
|
|
1217
|
-
width: 100%;
|
|
1218
|
-
box-sizing: border-box;
|
|
1219
|
-
`;
|
|
1220
1368
|
numberInput.name = pathKey;
|
|
1221
1369
|
numberInput.placeholder = element.placeholder || "0";
|
|
1222
1370
|
if (element.min !== void 0) numberInput.min = element.min.toString();
|
|
@@ -1224,6 +1372,16 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1224
1372
|
if (element.step !== void 0) numberInput.step = element.step.toString();
|
|
1225
1373
|
numberInput.value = ctx.prefill[element.key] || element.default || "";
|
|
1226
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
|
+
}
|
|
1227
1385
|
if (!readonly && ctx.instance) {
|
|
1228
1386
|
const handleChange = () => {
|
|
1229
1387
|
const value = numberInput.value ? parseFloat(numberInput.value) : null;
|
|
@@ -1232,10 +1390,14 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1232
1390
|
numberInput.addEventListener("blur", handleChange);
|
|
1233
1391
|
numberInput.addEventListener("input", handleChange);
|
|
1234
1392
|
}
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
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
|
+
}
|
|
1239
1401
|
}
|
|
1240
1402
|
wrapper.appendChild(inputWrapper);
|
|
1241
1403
|
}
|
|
@@ -1489,13 +1651,14 @@ function updateNumberField(element, fieldPath, value, context) {
|
|
|
1489
1651
|
return;
|
|
1490
1652
|
}
|
|
1491
1653
|
const inputs = scopeRoot.querySelectorAll(
|
|
1492
|
-
`[name^="${fieldPath}["]`
|
|
1654
|
+
`[name^="${fieldPath}\\["]`
|
|
1493
1655
|
);
|
|
1494
1656
|
inputs.forEach((input, index) => {
|
|
1495
1657
|
if (index < value.length) {
|
|
1496
1658
|
input.value = value[index] != null ? String(value[index]) : "";
|
|
1497
1659
|
input.classList.remove("invalid");
|
|
1498
1660
|
input.title = "";
|
|
1661
|
+
clearFieldError(input);
|
|
1499
1662
|
}
|
|
1500
1663
|
});
|
|
1501
1664
|
if (value.length !== inputs.length) {
|
|
@@ -1511,6 +1674,7 @@ function updateNumberField(element, fieldPath, value, context) {
|
|
|
1511
1674
|
input.value = value != null ? String(value) : "";
|
|
1512
1675
|
input.classList.remove("invalid");
|
|
1513
1676
|
input.title = "";
|
|
1677
|
+
clearFieldError(input);
|
|
1514
1678
|
}
|
|
1515
1679
|
}
|
|
1516
1680
|
}
|
|
@@ -1722,7 +1886,7 @@ function validateSelectElement(element, key, context) {
|
|
|
1722
1886
|
};
|
|
1723
1887
|
if ("multiple" in element && element.multiple) {
|
|
1724
1888
|
const inputs = scopeRoot.querySelectorAll(
|
|
1725
|
-
`[name^="${key}["]`
|
|
1889
|
+
`[name^="${key}\\["]`
|
|
1726
1890
|
);
|
|
1727
1891
|
const values = [];
|
|
1728
1892
|
inputs.forEach((input) => {
|
|
@@ -1758,7 +1922,7 @@ function updateSelectField(element, fieldPath, value, context) {
|
|
|
1758
1922
|
return;
|
|
1759
1923
|
}
|
|
1760
1924
|
const selects = scopeRoot.querySelectorAll(
|
|
1761
|
-
`[name^="${fieldPath}["]`
|
|
1925
|
+
`[name^="${fieldPath}\\["]`
|
|
1762
1926
|
);
|
|
1763
1927
|
selects.forEach((select, index) => {
|
|
1764
1928
|
if (index < value.length) {
|
|
@@ -1769,6 +1933,7 @@ function updateSelectField(element, fieldPath, value, context) {
|
|
|
1769
1933
|
});
|
|
1770
1934
|
select.classList.remove("invalid");
|
|
1771
1935
|
select.title = "";
|
|
1936
|
+
clearFieldError(select);
|
|
1772
1937
|
}
|
|
1773
1938
|
});
|
|
1774
1939
|
if (value.length !== selects.length) {
|
|
@@ -1788,72 +1953,147 @@ function updateSelectField(element, fieldPath, value, context) {
|
|
|
1788
1953
|
});
|
|
1789
1954
|
select.classList.remove("invalid");
|
|
1790
1955
|
select.title = "";
|
|
1956
|
+
clearFieldError(select);
|
|
1791
1957
|
}
|
|
1792
1958
|
}
|
|
1793
1959
|
}
|
|
1794
1960
|
|
|
1795
1961
|
// src/components/switcher.ts
|
|
1796
|
-
function applySelectedStyle(btn) {
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
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
|
+
}
|
|
1800
1972
|
}
|
|
1801
|
-
function applyUnselectedStyle(btn) {
|
|
1802
|
-
btn.style.backgroundColor = "transparent";
|
|
1973
|
+
function applyUnselectedStyle(btn, isPreset) {
|
|
1974
|
+
btn.style.backgroundColor = isPreset ? "var(--fb-background-color)" : "transparent";
|
|
1803
1975
|
btn.style.color = "var(--fb-text-color)";
|
|
1804
1976
|
btn.style.borderColor = "var(--fb-border-color)";
|
|
1805
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
|
+
}
|
|
1806
2035
|
function buildSegmentedGroup(element, currentValue, hiddenInput, readonly, onChange) {
|
|
1807
2036
|
const options = element.options || [];
|
|
2037
|
+
const isPresetMode = options.some((o) => o.subtitle || o.iconUrl);
|
|
1808
2038
|
const group = document.createElement("div");
|
|
1809
2039
|
group.className = "fb-switcher-group";
|
|
1810
|
-
group.style.cssText = `
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
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
|
+
`;
|
|
1815
2050
|
const buttons = [];
|
|
1816
2051
|
options.forEach((option, index) => {
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
btn.dataset.value = option.value;
|
|
1821
|
-
btn.textContent = option.label;
|
|
1822
|
-
btn.style.cssText = `
|
|
1823
|
-
padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
|
|
1824
|
-
font-size: var(--fb-font-size);
|
|
1825
|
-
border-width: var(--fb-border-width);
|
|
1826
|
-
border-style: solid;
|
|
1827
|
-
cursor: ${readonly ? "default" : "pointer"};
|
|
1828
|
-
transition: background-color var(--fb-transition-duration), color var(--fb-transition-duration), border-color var(--fb-transition-duration);
|
|
1829
|
-
white-space: nowrap;
|
|
1830
|
-
line-height: 1.25;
|
|
1831
|
-
outline: none;
|
|
1832
|
-
`;
|
|
1833
|
-
if (options.length === 1) {
|
|
1834
|
-
btn.style.borderRadius = "var(--fb-border-radius)";
|
|
1835
|
-
} else if (index === 0) {
|
|
1836
|
-
btn.style.borderRadius = "var(--fb-border-radius) 0 0 var(--fb-border-radius)";
|
|
1837
|
-
btn.style.borderRightWidth = "0";
|
|
1838
|
-
} else if (index === options.length - 1) {
|
|
1839
|
-
btn.style.borderRadius = "0 var(--fb-border-radius) var(--fb-border-radius) 0";
|
|
2052
|
+
let btn;
|
|
2053
|
+
if (isPresetMode) {
|
|
2054
|
+
btn = buildPresetCard(option, readonly);
|
|
1840
2055
|
} else {
|
|
1841
|
-
btn
|
|
1842
|
-
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
|
+
}
|
|
1843
2083
|
}
|
|
1844
2084
|
if (option.value === currentValue) {
|
|
1845
|
-
applySelectedStyle(btn);
|
|
2085
|
+
applySelectedStyle(btn, isPresetMode);
|
|
1846
2086
|
} else {
|
|
1847
|
-
applyUnselectedStyle(btn);
|
|
2087
|
+
applyUnselectedStyle(btn, isPresetMode);
|
|
1848
2088
|
}
|
|
1849
2089
|
if (!readonly) {
|
|
1850
2090
|
btn.addEventListener("click", () => {
|
|
1851
2091
|
hiddenInput.value = option.value;
|
|
1852
2092
|
buttons.forEach((b) => {
|
|
1853
2093
|
if (b.dataset.value === option.value) {
|
|
1854
|
-
applySelectedStyle(b);
|
|
2094
|
+
applySelectedStyle(b, isPresetMode);
|
|
1855
2095
|
} else {
|
|
1856
|
-
applyUnselectedStyle(b);
|
|
2096
|
+
applyUnselectedStyle(b, isPresetMode);
|
|
1857
2097
|
}
|
|
1858
2098
|
});
|
|
1859
2099
|
if (onChange) {
|
|
@@ -1867,7 +2107,7 @@ function buildSegmentedGroup(element, currentValue, hiddenInput, readonly, onCha
|
|
|
1867
2107
|
});
|
|
1868
2108
|
btn.addEventListener("mouseleave", () => {
|
|
1869
2109
|
if (hiddenInput.value !== option.value) {
|
|
1870
|
-
btn.style.backgroundColor = "transparent";
|
|
2110
|
+
btn.style.backgroundColor = isPresetMode ? "var(--fb-background-color)" : "transparent";
|
|
1871
2111
|
}
|
|
1872
2112
|
});
|
|
1873
2113
|
}
|
|
@@ -2086,7 +2326,7 @@ function validateSwitcherElement(element, key, context) {
|
|
|
2086
2326
|
);
|
|
2087
2327
|
if ("multiple" in element && element.multiple) {
|
|
2088
2328
|
const inputs = scopeRoot.querySelectorAll(
|
|
2089
|
-
`input[type="hidden"][name^="${key}["]`
|
|
2329
|
+
`input[type="hidden"][name^="${key}\\["]`
|
|
2090
2330
|
);
|
|
2091
2331
|
const values = [];
|
|
2092
2332
|
inputs.forEach((input) => {
|
|
@@ -2133,7 +2373,7 @@ function updateSwitcherField(element, fieldPath, value, context) {
|
|
|
2133
2373
|
return;
|
|
2134
2374
|
}
|
|
2135
2375
|
const inputs = scopeRoot.querySelectorAll(
|
|
2136
|
-
`input[type="hidden"][name^="${fieldPath}["]`
|
|
2376
|
+
`input[type="hidden"][name^="${fieldPath}\\["]`
|
|
2137
2377
|
);
|
|
2138
2378
|
inputs.forEach((input, index) => {
|
|
2139
2379
|
if (index < value.length) {
|
|
@@ -2142,15 +2382,17 @@ function updateSwitcherField(element, fieldPath, value, context) {
|
|
|
2142
2382
|
const group = input.parentElement?.querySelector(".fb-switcher-group");
|
|
2143
2383
|
if (group) {
|
|
2144
2384
|
group.querySelectorAll(".fb-switcher-btn").forEach((btn) => {
|
|
2385
|
+
const isPreset = isPresetButton(btn);
|
|
2145
2386
|
if (btn.dataset.value === newVal) {
|
|
2146
|
-
applySelectedStyle(btn);
|
|
2387
|
+
applySelectedStyle(btn, isPreset);
|
|
2147
2388
|
} else {
|
|
2148
|
-
applyUnselectedStyle(btn);
|
|
2389
|
+
applyUnselectedStyle(btn, isPreset);
|
|
2149
2390
|
}
|
|
2150
2391
|
});
|
|
2151
2392
|
}
|
|
2152
2393
|
input.classList.remove("invalid");
|
|
2153
2394
|
input.title = "";
|
|
2395
|
+
clearFieldError(input);
|
|
2154
2396
|
}
|
|
2155
2397
|
});
|
|
2156
2398
|
if (value.length !== inputs.length) {
|
|
@@ -2168,19 +2410,211 @@ function updateSwitcherField(element, fieldPath, value, context) {
|
|
|
2168
2410
|
const group = input.parentElement?.querySelector(".fb-switcher-group");
|
|
2169
2411
|
if (group) {
|
|
2170
2412
|
group.querySelectorAll(".fb-switcher-btn").forEach((btn) => {
|
|
2413
|
+
const isPreset = isPresetButton(btn);
|
|
2171
2414
|
if (btn.dataset.value === newVal) {
|
|
2172
|
-
applySelectedStyle(btn);
|
|
2415
|
+
applySelectedStyle(btn, isPreset);
|
|
2173
2416
|
} else {
|
|
2174
|
-
applyUnselectedStyle(btn);
|
|
2417
|
+
applyUnselectedStyle(btn, isPreset);
|
|
2175
2418
|
}
|
|
2176
2419
|
});
|
|
2177
2420
|
}
|
|
2178
2421
|
input.classList.remove("invalid");
|
|
2179
2422
|
input.title = "";
|
|
2423
|
+
clearFieldError(input);
|
|
2180
2424
|
}
|
|
2181
2425
|
}
|
|
2182
2426
|
}
|
|
2183
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;
|
|
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);
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2184
2618
|
// src/components/file/constraints.ts
|
|
2185
2619
|
function getAllowedExtensions(accept) {
|
|
2186
2620
|
if (!accept) return [];
|
|
@@ -2281,14 +2715,20 @@ function ensureFileStyles() {
|
|
|
2281
2715
|
}
|
|
2282
2716
|
|
|
2283
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). */
|
|
2284
2720
|
.fb-wide-tile {
|
|
2285
2721
|
width: 100%;
|
|
2722
|
+
box-sizing: border-box;
|
|
2286
2723
|
border-radius: 0.75rem;
|
|
2287
2724
|
border: 1px dashed #60a5fa;
|
|
2288
2725
|
background: rgba(239,246,255,0.5);
|
|
2289
2726
|
display: flex;
|
|
2727
|
+
flex-wrap: wrap;
|
|
2728
|
+
align-items: stretch;
|
|
2729
|
+
gap: 0;
|
|
2290
2730
|
overflow: hidden;
|
|
2291
|
-
height: 180px;
|
|
2731
|
+
min-height: 180px;
|
|
2292
2732
|
transition: border-color 150ms, background 150ms, box-shadow 150ms;
|
|
2293
2733
|
cursor: pointer;
|
|
2294
2734
|
}
|
|
@@ -2302,9 +2742,12 @@ function ensureFileStyles() {
|
|
|
2302
2742
|
box-shadow: 0 0 0 4px rgba(191,219,254,0.7);
|
|
2303
2743
|
}
|
|
2304
2744
|
|
|
2305
|
-
/* 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. */
|
|
2306
2748
|
.fb-wide-tile-upload {
|
|
2307
|
-
flex: 1;
|
|
2749
|
+
flex: 1 1 220px;
|
|
2750
|
+
min-height: 140px;
|
|
2308
2751
|
display: flex;
|
|
2309
2752
|
flex-direction: column;
|
|
2310
2753
|
align-items: center;
|
|
@@ -2317,24 +2760,21 @@ function ensureFileStyles() {
|
|
|
2317
2760
|
background: transparent;
|
|
2318
2761
|
border: none;
|
|
2319
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);
|
|
2320
2766
|
}
|
|
2321
2767
|
.fb-wide-tile-upload:hover {
|
|
2322
|
-
background: rgba(191,219,254,0.25);
|
|
2323
|
-
}
|
|
2324
|
-
|
|
2325
|
-
/* Vertical dashed divider between upload and library zones */
|
|
2326
|
-
.fb-wide-tile-divider {
|
|
2327
|
-
width: 1px;
|
|
2328
|
-
margin: 16px 0;
|
|
2329
|
-
border-left: 1px dashed rgba(96,165,250,0.5);
|
|
2330
|
-
background: transparent;
|
|
2331
|
-
flex-shrink: 0;
|
|
2768
|
+
background: rgba(191,219,254,0.25);
|
|
2332
2769
|
}
|
|
2333
|
-
|
|
2334
|
-
|
|
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. */
|
|
2335
2775
|
.fb-wide-tile-library {
|
|
2336
|
-
|
|
2337
|
-
|
|
2776
|
+
flex: 0 0 176px;
|
|
2777
|
+
min-height: 120px;
|
|
2338
2778
|
display: flex;
|
|
2339
2779
|
flex-direction: column;
|
|
2340
2780
|
align-items: center;
|
|
@@ -2351,6 +2791,10 @@ function ensureFileStyles() {
|
|
|
2351
2791
|
.fb-wide-tile-library:hover {
|
|
2352
2792
|
background: rgba(191,219,254,0.25);
|
|
2353
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. */
|
|
2354
2798
|
|
|
2355
2799
|
/* \u2500\u2500\u2500 Multi-file outer grid container \u2500\u2500\u2500 */
|
|
2356
2800
|
.fb-multi-outer {
|
|
@@ -2729,6 +3173,39 @@ function ensureFileStyles() {
|
|
|
2729
3173
|
}
|
|
2730
3174
|
`;
|
|
2731
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
|
+
}
|
|
2732
3209
|
}
|
|
2733
3210
|
|
|
2734
3211
|
// src/components/file/dom.ts
|
|
@@ -4074,21 +4551,21 @@ function buildWideTile(state, hasLibrary, onUploadClick, onLibraryClick, isDragO
|
|
|
4074
4551
|
};
|
|
4075
4552
|
outer.appendChild(uploadBtn);
|
|
4076
4553
|
if (hasLibrary && onLibraryClick) {
|
|
4077
|
-
const divider = document.createElement("div");
|
|
4078
|
-
divider.className = "fb-wide-tile-divider";
|
|
4079
|
-
outer.appendChild(divider);
|
|
4080
4554
|
const libBtn = document.createElement("button");
|
|
4081
4555
|
libBtn.type = "button";
|
|
4082
4556
|
libBtn.className = "fb-wide-tile-library fb-file-library-card";
|
|
4083
4557
|
const libIcon = document.createElement("span");
|
|
4558
|
+
libIcon.className = "fb-wide-tile-library-icon";
|
|
4084
4559
|
libIcon.style.cssText = "width:28px;height:28px;display:block;flex-shrink:0;";
|
|
4085
4560
|
libIcon.innerHTML = ICON_LIBRARY2;
|
|
4086
4561
|
libBtn.appendChild(libIcon);
|
|
4087
4562
|
const libLabel = document.createElement("div");
|
|
4563
|
+
libLabel.className = "fb-wide-tile-library-label";
|
|
4088
4564
|
libLabel.style.cssText = "font-size:13px;font-weight:600;text-align:center;";
|
|
4089
4565
|
libLabel.textContent = t("fromLibrary", state);
|
|
4090
4566
|
libBtn.appendChild(libLabel);
|
|
4091
4567
|
const libHint = document.createElement("div");
|
|
4568
|
+
libHint.className = "fb-wide-tile-library-hint";
|
|
4092
4569
|
libHint.style.cssText = "font-size:11px;opacity:0.75;text-align:center;";
|
|
4093
4570
|
libHint.textContent = t("libraryHint", state);
|
|
4094
4571
|
libBtn.appendChild(libHint);
|
|
@@ -5330,7 +5807,7 @@ function validateColourElement(element, key, context) {
|
|
|
5330
5807
|
};
|
|
5331
5808
|
if (element.multiple) {
|
|
5332
5809
|
const hexInputs = scopeRoot.querySelectorAll(
|
|
5333
|
-
`[name^="${key}["].colour-hex-input`
|
|
5810
|
+
`[name^="${key}\\["].colour-hex-input`
|
|
5334
5811
|
);
|
|
5335
5812
|
const values = [];
|
|
5336
5813
|
hexInputs.forEach((input, index) => {
|
|
@@ -5379,7 +5856,7 @@ function updateColourField(element, fieldPath, value, context) {
|
|
|
5379
5856
|
return;
|
|
5380
5857
|
}
|
|
5381
5858
|
const hexInputs = scopeRoot.querySelectorAll(
|
|
5382
|
-
`[name^="${fieldPath}["].colour-hex-input`
|
|
5859
|
+
`[name^="${fieldPath}\\["].colour-hex-input`
|
|
5383
5860
|
);
|
|
5384
5861
|
hexInputs.forEach((hexInput, index) => {
|
|
5385
5862
|
if (index < value.length) {
|
|
@@ -5387,6 +5864,7 @@ function updateColourField(element, fieldPath, value, context) {
|
|
|
5387
5864
|
hexInput.value = normalized;
|
|
5388
5865
|
hexInput.classList.remove("invalid");
|
|
5389
5866
|
hexInput.title = "";
|
|
5867
|
+
clearFieldError(hexInput);
|
|
5390
5868
|
const wrapper = hexInput.closest(".colour-picker-wrapper");
|
|
5391
5869
|
if (wrapper) {
|
|
5392
5870
|
const swatch = wrapper.querySelector(".colour-swatch");
|
|
@@ -5416,6 +5894,7 @@ function updateColourField(element, fieldPath, value, context) {
|
|
|
5416
5894
|
hexInput.value = normalized;
|
|
5417
5895
|
hexInput.classList.remove("invalid");
|
|
5418
5896
|
hexInput.title = "";
|
|
5897
|
+
clearFieldError(hexInput);
|
|
5419
5898
|
const wrapper = hexInput.closest(".colour-picker-wrapper");
|
|
5420
5899
|
if (wrapper) {
|
|
5421
5900
|
const swatch = wrapper.querySelector(".colour-swatch");
|
|
@@ -5818,7 +6297,7 @@ function validateSliderElement(element, key, context) {
|
|
|
5818
6297
|
};
|
|
5819
6298
|
if (element.multiple) {
|
|
5820
6299
|
const sliders = scopeRoot.querySelectorAll(
|
|
5821
|
-
`input[type="range"][name^="${key}["]`
|
|
6300
|
+
`input[type="range"][name^="${key}\\["]`
|
|
5822
6301
|
);
|
|
5823
6302
|
const values = [];
|
|
5824
6303
|
sliders.forEach((slider, index) => {
|
|
@@ -5869,7 +6348,7 @@ function updateSliderField(element, fieldPath, value, context) {
|
|
|
5869
6348
|
return;
|
|
5870
6349
|
}
|
|
5871
6350
|
const sliders = scopeRoot.querySelectorAll(
|
|
5872
|
-
`input[type="range"][name^="${fieldPath}["]`
|
|
6351
|
+
`input[type="range"][name^="${fieldPath}\\["]`
|
|
5873
6352
|
);
|
|
5874
6353
|
sliders.forEach((slider, index) => {
|
|
5875
6354
|
if (index < value.length && value[index] !== null) {
|
|
@@ -5897,6 +6376,7 @@ function updateSliderField(element, fieldPath, value, context) {
|
|
|
5897
6376
|
}
|
|
5898
6377
|
slider.classList.remove("invalid");
|
|
5899
6378
|
slider.title = "";
|
|
6379
|
+
clearFieldError(slider);
|
|
5900
6380
|
}
|
|
5901
6381
|
});
|
|
5902
6382
|
if (value.length !== sliders.length) {
|
|
@@ -5933,6 +6413,7 @@ function updateSliderField(element, fieldPath, value, context) {
|
|
|
5933
6413
|
}
|
|
5934
6414
|
slider.classList.remove("invalid");
|
|
5935
6415
|
slider.title = "";
|
|
6416
|
+
clearFieldError(slider);
|
|
5936
6417
|
}
|
|
5937
6418
|
}
|
|
5938
6419
|
}
|
|
@@ -6055,34 +6536,24 @@ function getChildWrapperClass(isSlides, columns) {
|
|
|
6055
6536
|
const cols = columns || 1;
|
|
6056
6537
|
return cols === 1 ? "space-y-2" : `grid grid-cols-${cols} gap-2`;
|
|
6057
6538
|
}
|
|
6058
|
-
function mountRemoveButton(item, onRemove) {
|
|
6539
|
+
function mountRemoveButton(item, onRemove, state) {
|
|
6059
6540
|
const rem = document.createElement("button");
|
|
6060
6541
|
rem.type = "button";
|
|
6061
6542
|
rem.className = "fb-item-remove";
|
|
6543
|
+
rem.setAttribute("aria-label", t("removeElement", state));
|
|
6062
6544
|
rem.style.cssText = `
|
|
6063
|
-
width:
|
|
6064
|
-
height:
|
|
6545
|
+
width: 24px;
|
|
6546
|
+
height: 24px;
|
|
6065
6547
|
display: inline-flex;
|
|
6066
6548
|
align-items: center;
|
|
6067
6549
|
justify-content: center;
|
|
6068
6550
|
padding: 0;
|
|
6069
|
-
line-height: 1;
|
|
6070
|
-
font-size: 14px;
|
|
6071
|
-
color: var(--fb-error-color);
|
|
6072
|
-
background-color: transparent;
|
|
6073
6551
|
border: 0;
|
|
6074
6552
|
border-radius: 4px;
|
|
6075
6553
|
cursor: pointer;
|
|
6076
6554
|
flex-shrink: 0;
|
|
6077
|
-
transition: background-color var(--fb-transition-duration);
|
|
6078
6555
|
`;
|
|
6079
|
-
rem.
|
|
6080
|
-
rem.addEventListener("mouseenter", () => {
|
|
6081
|
-
rem.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
6082
|
-
});
|
|
6083
|
-
rem.addEventListener("mouseleave", () => {
|
|
6084
|
-
rem.style.backgroundColor = "transparent";
|
|
6085
|
-
});
|
|
6556
|
+
rem.innerHTML = BIN_ICON_SVG;
|
|
6086
6557
|
rem.onclick = onRemove;
|
|
6087
6558
|
const labelRow = item.querySelector("[data-fb-label-row]");
|
|
6088
6559
|
if (labelRow) {
|
|
@@ -6108,7 +6579,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
6108
6579
|
itemsWrap.className = "fb-container-slides";
|
|
6109
6580
|
const slideCols = element.columns;
|
|
6110
6581
|
const gridTemplateColumns = typeof slideCols === "number" && slideCols > 0 ? `repeat(${slideCols}, 1fr)` : "repeat(auto-fit, minmax(280px, 1fr))";
|
|
6111
|
-
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;`;
|
|
6112
6583
|
} else {
|
|
6113
6584
|
itemsWrap.className = "space-y-2";
|
|
6114
6585
|
}
|
|
@@ -6139,6 +6610,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
6139
6610
|
const item = document.createElement("div");
|
|
6140
6611
|
item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
|
|
6141
6612
|
item.setAttribute("data-container-item", `${element.key}[${idx}]`);
|
|
6613
|
+
if (isSlides) {
|
|
6614
|
+
item.setAttribute("data-fb-slide-card", "");
|
|
6615
|
+
}
|
|
6142
6616
|
const childWrapper = document.createElement("div");
|
|
6143
6617
|
childWrapper.className = getChildWrapperClass(isSlides, element.columns);
|
|
6144
6618
|
element.elements.forEach((child) => {
|
|
@@ -6155,7 +6629,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
6155
6629
|
});
|
|
6156
6630
|
item.appendChild(childWrapper);
|
|
6157
6631
|
if (!containerIsReadonly) {
|
|
6158
|
-
mountRemoveButton(item, () => handleRemoveItem(item));
|
|
6632
|
+
mountRemoveButton(item, () => handleRemoveItem(item), state);
|
|
6159
6633
|
}
|
|
6160
6634
|
if (slideAddTile && slideAddTile.parentElement === itemsWrap) {
|
|
6161
6635
|
itemsWrap.insertBefore(item, slideAddTile);
|
|
@@ -6201,6 +6675,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
6201
6675
|
const item = document.createElement("div");
|
|
6202
6676
|
item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
|
|
6203
6677
|
item.setAttribute("data-container-item", `${element.key}[${idx}]`);
|
|
6678
|
+
if (isSlides) {
|
|
6679
|
+
item.setAttribute("data-fb-slide-card", "");
|
|
6680
|
+
}
|
|
6204
6681
|
const childWrapper = document.createElement("div");
|
|
6205
6682
|
if (isSlides) {
|
|
6206
6683
|
childWrapper.className = "space-y-2";
|
|
@@ -6224,7 +6701,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
6224
6701
|
});
|
|
6225
6702
|
item.appendChild(childWrapper);
|
|
6226
6703
|
if (!containerIsReadonly) {
|
|
6227
|
-
mountRemoveButton(item, () => handleRemoveItem(item));
|
|
6704
|
+
mountRemoveButton(item, () => handleRemoveItem(item), ctx.state);
|
|
6228
6705
|
}
|
|
6229
6706
|
itemsWrap.appendChild(item);
|
|
6230
6707
|
});
|
|
@@ -6244,6 +6721,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
6244
6721
|
const item = document.createElement("div");
|
|
6245
6722
|
item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
|
|
6246
6723
|
item.setAttribute("data-container-item", `${element.key}[${idx}]`);
|
|
6724
|
+
if (isSlides) {
|
|
6725
|
+
item.setAttribute("data-fb-slide-card", "");
|
|
6726
|
+
}
|
|
6247
6727
|
const childWrapper = document.createElement("div");
|
|
6248
6728
|
if (isSlides) {
|
|
6249
6729
|
childWrapper.className = "space-y-2";
|
|
@@ -6268,11 +6748,15 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
6268
6748
|
}
|
|
6269
6749
|
});
|
|
6270
6750
|
item.appendChild(childWrapper);
|
|
6271
|
-
mountRemoveButton(
|
|
6272
|
-
|
|
6273
|
-
|
|
6274
|
-
|
|
6275
|
-
|
|
6751
|
+
mountRemoveButton(
|
|
6752
|
+
item,
|
|
6753
|
+
() => {
|
|
6754
|
+
if (countItems() > min) {
|
|
6755
|
+
handleRemoveItem(item);
|
|
6756
|
+
}
|
|
6757
|
+
},
|
|
6758
|
+
ctx.state
|
|
6759
|
+
);
|
|
6276
6760
|
itemsWrap.appendChild(item);
|
|
6277
6761
|
}
|
|
6278
6762
|
}
|
|
@@ -8897,7 +9381,7 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
|
|
|
8897
9381
|
if (element.minLength != null || element.maxLength != null) {
|
|
8898
9382
|
const counterRow = document.createElement("div");
|
|
8899
9383
|
counterRow.style.cssText = "position: relative; padding: 2px 10px 4px; text-align: right;";
|
|
8900
|
-
const counter = createCharCounter(element, textarea
|
|
9384
|
+
const counter = createCharCounter(element, textarea);
|
|
8901
9385
|
counter.style.cssText = `
|
|
8902
9386
|
position: static;
|
|
8903
9387
|
display: inline-block;
|
|
@@ -9422,6 +9906,117 @@ function validateMarkdown(_element, _key, _context) {
|
|
|
9422
9906
|
function updateMarkdown(_element, _fieldPath, _value, _context) {
|
|
9423
9907
|
}
|
|
9424
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
|
+
|
|
9425
10020
|
// src/components/index.ts
|
|
9426
10021
|
function showTooltip(tooltipId, button) {
|
|
9427
10022
|
const tooltip = document.getElementById(tooltipId);
|
|
@@ -9730,6 +10325,9 @@ function dispatchToRenderer(element, ctx, wrapper, pathKey) {
|
|
|
9730
10325
|
renderSwitcherElement(element, ctx, wrapper, pathKey);
|
|
9731
10326
|
}
|
|
9732
10327
|
break;
|
|
10328
|
+
case "boolean":
|
|
10329
|
+
renderBooleanElement(element, ctx, wrapper, pathKey);
|
|
10330
|
+
break;
|
|
9733
10331
|
case "file":
|
|
9734
10332
|
if (isMultiple) {
|
|
9735
10333
|
renderMultipleFileElement(element, ctx, wrapper, pathKey);
|
|
@@ -9808,8 +10406,11 @@ function renderElement2(element, ctx) {
|
|
|
9808
10406
|
const wrapper = document.createElement("div");
|
|
9809
10407
|
wrapper.className = "mb-2 fb-field-wrapper";
|
|
9810
10408
|
wrapper.setAttribute("data-field-key", element.key);
|
|
9811
|
-
const
|
|
9812
|
-
|
|
10409
|
+
const ops = getComponentOperations(element.type);
|
|
10410
|
+
if (!ops?.ownsLabel) {
|
|
10411
|
+
const label = createLabelContainer(element);
|
|
10412
|
+
wrapper.appendChild(label);
|
|
10413
|
+
}
|
|
9813
10414
|
const pathKey = pathJoin(ctx.path, element.key);
|
|
9814
10415
|
dispatchToRenderer(element, ctx, wrapper, pathKey);
|
|
9815
10416
|
if (initiallyDisabled) {
|
|
@@ -10042,28 +10643,52 @@ var defaultTheme = {
|
|
|
10042
10643
|
// blue-500
|
|
10043
10644
|
primaryHoverColor: "#2563eb",
|
|
10044
10645
|
// blue-600
|
|
10646
|
+
primarySoftColor: "#dbeafe",
|
|
10647
|
+
// blue-100
|
|
10648
|
+
primarySoftHoverColor: "#bfdbfe",
|
|
10649
|
+
// blue-200
|
|
10045
10650
|
errorColor: "#ef4444",
|
|
10046
10651
|
// red-500
|
|
10047
10652
|
errorHoverColor: "#dc2626",
|
|
10048
10653
|
// red-600
|
|
10049
10654
|
successColor: "#10b981",
|
|
10050
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
|
|
10051
10664
|
borderColor: "#d1d5db",
|
|
10052
10665
|
// gray-300
|
|
10053
10666
|
borderHoverColor: "#9ca3af",
|
|
10054
10667
|
// gray-400
|
|
10055
10668
|
borderFocusColor: "#3b82f6",
|
|
10056
10669
|
// blue-500
|
|
10670
|
+
borderStrongColor: "#9ca3af",
|
|
10671
|
+
// gray-400
|
|
10057
10672
|
backgroundColor: "#ffffff",
|
|
10058
10673
|
// white
|
|
10059
10674
|
backgroundHoverColor: "#f9fafb",
|
|
10060
10675
|
// gray-50
|
|
10061
10676
|
backgroundReadonlyColor: "#f3f4f6",
|
|
10062
10677
|
// gray-100
|
|
10678
|
+
pageBackgroundColor: "#f9fafb",
|
|
10679
|
+
// gray-50
|
|
10680
|
+
surfaceSoftColor: "#eff6ff",
|
|
10681
|
+
// blue-50
|
|
10682
|
+
surfaceTintColor: "#f8fafc",
|
|
10683
|
+
// slate-50
|
|
10063
10684
|
textColor: "#1f2937",
|
|
10064
10685
|
// gray-800
|
|
10065
10686
|
textSecondaryColor: "#6b7280",
|
|
10066
10687
|
// gray-500
|
|
10688
|
+
textMutedColor: "#9ca3af",
|
|
10689
|
+
// gray-400
|
|
10690
|
+
textFaintColor: "#cbd5e1",
|
|
10691
|
+
// slate-300
|
|
10067
10692
|
textPlaceholderColor: "#9ca3af",
|
|
10068
10693
|
// gray-400
|
|
10069
10694
|
textDisabledColor: "#d1d5db",
|
|
@@ -10106,6 +10731,12 @@ var defaultTheme = {
|
|
|
10106
10731
|
// 4px (compact density v2)
|
|
10107
10732
|
borderRadius: "0.5rem",
|
|
10108
10733
|
// rounded-lg (8px)
|
|
10734
|
+
borderRadiusSmall: "0.375rem",
|
|
10735
|
+
// 6px
|
|
10736
|
+
borderRadiusLarge: "0.75rem",
|
|
10737
|
+
// 12px
|
|
10738
|
+
borderRadiusXLarge: "1rem",
|
|
10739
|
+
// 16px
|
|
10109
10740
|
borderWidth: "1px",
|
|
10110
10741
|
// Typography
|
|
10111
10742
|
fontSize: "0.875rem",
|
|
@@ -10117,13 +10748,31 @@ var defaultTheme = {
|
|
|
10117
10748
|
fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
|
10118
10749
|
fontWeightNormal: "400",
|
|
10119
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)",
|
|
10120
10755
|
// Focus ring
|
|
10121
10756
|
focusRingWidth: "2px",
|
|
10122
10757
|
focusRingColor: "#3b82f6",
|
|
10123
10758
|
// blue-500
|
|
10124
10759
|
focusRingOpacity: "0.5",
|
|
10125
10760
|
// Transitions
|
|
10126
|
-
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"
|
|
10127
10776
|
};
|
|
10128
10777
|
function generateCSSVariables(theme) {
|
|
10129
10778
|
const mergedTheme = { ...defaultTheme, ...theme };
|
|
@@ -10135,6 +10784,7 @@ function generateCSSVariables(theme) {
|
|
|
10135
10784
|
return cssVars.join("\n");
|
|
10136
10785
|
}
|
|
10137
10786
|
function injectThemeVariables(container, theme) {
|
|
10787
|
+
ensureThemingHooks(container.ownerDocument || document);
|
|
10138
10788
|
const cssVariables = generateCSSVariables(theme);
|
|
10139
10789
|
let styleTag = container.querySelector(
|
|
10140
10790
|
"style[data-fb-theme]"
|
|
@@ -10197,114 +10847,50 @@ var exampleThemes = {
|
|
|
10197
10847
|
fontSize: "16px",
|
|
10198
10848
|
fontSizeSmall: "14px",
|
|
10199
10849
|
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif'
|
|
10200
|
-
}
|
|
10201
|
-
};
|
|
10202
|
-
|
|
10203
|
-
// src/components/registry.ts
|
|
10204
|
-
function validateHiddenElement(element, key, context) {
|
|
10205
|
-
const { scopeRoot } = context;
|
|
10206
|
-
const input = scopeRoot.querySelector(
|
|
10207
|
-
`input[type="hidden"][data-hidden-field="true"][name="${key}"]`
|
|
10208
|
-
);
|
|
10209
|
-
const raw = input?.value ?? "";
|
|
10210
|
-
if (raw === "") {
|
|
10211
|
-
const defaultVal = "default" in element ? element.default : null;
|
|
10212
|
-
return { value: defaultVal !== void 0 ? defaultVal : null, errors: [] };
|
|
10213
|
-
}
|
|
10214
|
-
return { value: deserializeHiddenValue(raw), errors: [] };
|
|
10215
|
-
}
|
|
10216
|
-
function updateHiddenField(_element, fieldPath, value, context) {
|
|
10217
|
-
const { scopeRoot } = context;
|
|
10218
|
-
const input = scopeRoot.querySelector(
|
|
10219
|
-
`input[type="hidden"][data-hidden-field="true"][name="${fieldPath}"]`
|
|
10220
|
-
);
|
|
10221
|
-
if (!input) return;
|
|
10222
|
-
input.value = serializeHiddenValue(value);
|
|
10223
|
-
}
|
|
10224
|
-
var componentRegistry = {
|
|
10225
|
-
text: {
|
|
10226
|
-
validate: validateTextElement,
|
|
10227
|
-
update: updateTextField
|
|
10228
|
-
},
|
|
10229
|
-
textarea: {
|
|
10230
|
-
validate: validateTextareaElement,
|
|
10231
|
-
update: updateTextareaField
|
|
10232
|
-
},
|
|
10233
|
-
number: {
|
|
10234
|
-
validate: validateNumberElement,
|
|
10235
|
-
update: updateNumberField
|
|
10236
|
-
},
|
|
10237
|
-
select: {
|
|
10238
|
-
validate: validateSelectElement,
|
|
10239
|
-
update: updateSelectField
|
|
10240
|
-
},
|
|
10241
|
-
switcher: {
|
|
10242
|
-
validate: validateSwitcherElement,
|
|
10243
|
-
update: updateSwitcherField
|
|
10244
|
-
},
|
|
10245
|
-
file: {
|
|
10246
|
-
validate: validateFileElement,
|
|
10247
|
-
update: updateFileField
|
|
10248
|
-
},
|
|
10249
|
-
files: {
|
|
10250
|
-
// Legacy type - delegates to file
|
|
10251
|
-
validate: validateFileElement,
|
|
10252
|
-
update: updateFileField
|
|
10253
|
-
},
|
|
10254
|
-
colour: {
|
|
10255
|
-
validate: validateColourElement,
|
|
10256
|
-
update: updateColourField
|
|
10257
|
-
},
|
|
10258
|
-
slider: {
|
|
10259
|
-
validate: validateSliderElement,
|
|
10260
|
-
update: updateSliderField
|
|
10261
|
-
},
|
|
10262
|
-
container: {
|
|
10263
|
-
validate: validateContainerElement,
|
|
10264
|
-
update: updateContainerField
|
|
10265
|
-
},
|
|
10266
|
-
group: {
|
|
10267
|
-
// Deprecated type - delegates to container
|
|
10268
|
-
validate: validateGroupElement,
|
|
10269
|
-
update: updateGroupField
|
|
10270
|
-
},
|
|
10271
|
-
table: {
|
|
10272
|
-
validate: validateTableElement,
|
|
10273
|
-
update: updateTableField
|
|
10274
|
-
},
|
|
10275
|
-
richinput: {
|
|
10276
|
-
validate: validateRichInputElement,
|
|
10277
|
-
update: updateRichInputField
|
|
10278
|
-
},
|
|
10279
|
-
hidden: {
|
|
10280
|
-
// Legacy type: `type: "hidden"` — reads/writes DOM <input type="hidden"> element
|
|
10281
|
-
validate: validateHiddenElement,
|
|
10282
|
-
update: updateHiddenField
|
|
10283
10850
|
},
|
|
10284
|
-
|
|
10285
|
-
|
|
10286
|
-
|
|
10287
|
-
|
|
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"
|
|
10288
10892
|
}
|
|
10289
10893
|
};
|
|
10290
|
-
function getComponentOperations(elementType) {
|
|
10291
|
-
return componentRegistry[elementType] || null;
|
|
10292
|
-
}
|
|
10293
|
-
function validateElementWithComponent(element, key, context) {
|
|
10294
|
-
const ops = getComponentOperations(element.type);
|
|
10295
|
-
if (ops && ops.validate) {
|
|
10296
|
-
return ops.validate(element, key, context);
|
|
10297
|
-
}
|
|
10298
|
-
return null;
|
|
10299
|
-
}
|
|
10300
|
-
function updateElementWithComponent(element, fieldPath, value, context) {
|
|
10301
|
-
const ops = getComponentOperations(element.type);
|
|
10302
|
-
if (ops && ops.update) {
|
|
10303
|
-
ops.update(element, fieldPath, value, context);
|
|
10304
|
-
return true;
|
|
10305
|
-
}
|
|
10306
|
-
return false;
|
|
10307
|
-
}
|
|
10308
10894
|
|
|
10309
10895
|
// src/instance/FormBuilderInstance.ts
|
|
10310
10896
|
var FormBuilderInstance = class {
|
|
@@ -10429,26 +11015,37 @@ var FormBuilderInstance = class {
|
|
|
10429
11015
|
}
|
|
10430
11016
|
}
|
|
10431
11017
|
/**
|
|
10432
|
-
* 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).
|
|
10433
11032
|
*/
|
|
10434
11033
|
findFormElementByFieldPath(fieldPath) {
|
|
10435
11034
|
if (!this.state.formRoot) return null;
|
|
10436
|
-
|
|
10437
|
-
|
|
10438
|
-
|
|
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}"]`
|
|
10439
11047
|
);
|
|
10440
11048
|
if (element) return element;
|
|
10441
|
-
const variations = [
|
|
10442
|
-
fieldPath,
|
|
10443
|
-
fieldPath.replace(/\[(\d+)\]/g, "[$1]"),
|
|
10444
|
-
fieldPath.replace(/\./g, "[") + "]".repeat((fieldPath.match(/\./g) || []).length)
|
|
10445
|
-
];
|
|
10446
|
-
for (const variation of variations) {
|
|
10447
|
-
element = this.state.formRoot.querySelector(
|
|
10448
|
-
`[name="${variation}"]`
|
|
10449
|
-
);
|
|
10450
|
-
if (element) return element;
|
|
10451
|
-
}
|
|
10452
11049
|
}
|
|
10453
11050
|
const schemaElement = this.findSchemaElement(fieldPath);
|
|
10454
11051
|
if (!schemaElement) return null;
|