@dmitryvim/form-builder 0.2.16 → 0.2.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/browser/formbuilder.min.js +86 -58
- package/dist/browser/formbuilder.v0.2.18.min.js +386 -0
- package/dist/cjs/index.cjs +508 -24
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/index.js +499 -24
- package/dist/esm/index.js.map +1 -1
- package/dist/form-builder.js +86 -58
- package/dist/types/components/index.d.ts +2 -1
- package/dist/types/components/switcher.d.ts +13 -0
- package/dist/types/components/textarea.d.ts +2 -1
- 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 +9 -1
- package/package.json +2 -2
- package/dist/browser/formbuilder.v0.2.16.min.js +0 -358
package/dist/esm/index.js
CHANGED
|
@@ -719,6 +719,21 @@ function updateTextField(element, fieldPath, value, context) {
|
|
|
719
719
|
}
|
|
720
720
|
|
|
721
721
|
// src/components/textarea.ts
|
|
722
|
+
function applyAutoExpand(textarea) {
|
|
723
|
+
textarea.style.overflow = "hidden";
|
|
724
|
+
textarea.style.resize = "none";
|
|
725
|
+
const lineCount = (textarea.value.match(/\n/g) || []).length + 1;
|
|
726
|
+
textarea.rows = Math.max(1, lineCount);
|
|
727
|
+
const resize = () => {
|
|
728
|
+
if (!textarea.isConnected) return;
|
|
729
|
+
textarea.style.height = "0";
|
|
730
|
+
textarea.style.height = `${textarea.scrollHeight}px`;
|
|
731
|
+
};
|
|
732
|
+
textarea.addEventListener("input", resize);
|
|
733
|
+
setTimeout(() => {
|
|
734
|
+
if (textarea.isConnected) resize();
|
|
735
|
+
}, 0);
|
|
736
|
+
}
|
|
722
737
|
function renderTextareaElement(element, ctx, wrapper, pathKey) {
|
|
723
738
|
const state = ctx.state;
|
|
724
739
|
const textareaWrapper = document.createElement("div");
|
|
@@ -739,6 +754,9 @@ function renderTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
739
754
|
textareaInput.addEventListener("blur", handleChange);
|
|
740
755
|
textareaInput.addEventListener("input", handleChange);
|
|
741
756
|
}
|
|
757
|
+
if (element.autoExpand || state.config.readonly) {
|
|
758
|
+
applyAutoExpand(textareaInput);
|
|
759
|
+
}
|
|
742
760
|
textareaWrapper.appendChild(textareaInput);
|
|
743
761
|
if (!state.config.readonly && (element.minLength != null || element.maxLength != null)) {
|
|
744
762
|
const counter = createCharCounter(element, textareaInput, true);
|
|
@@ -787,6 +805,9 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
787
805
|
textareaInput.addEventListener("blur", handleChange);
|
|
788
806
|
textareaInput.addEventListener("input", handleChange);
|
|
789
807
|
}
|
|
808
|
+
if (element.autoExpand || state.config.readonly) {
|
|
809
|
+
applyAutoExpand(textareaInput);
|
|
810
|
+
}
|
|
790
811
|
textareaContainer.appendChild(textareaInput);
|
|
791
812
|
if (!state.config.readonly && (element.minLength != null || element.maxLength != null)) {
|
|
792
813
|
const counter = createCharCounter(element, textareaInput, true);
|
|
@@ -888,6 +909,24 @@ function validateTextareaElement(element, key, context) {
|
|
|
888
909
|
}
|
|
889
910
|
function updateTextareaField(element, fieldPath, value, context) {
|
|
890
911
|
updateTextField(element, fieldPath, value, context);
|
|
912
|
+
const { scopeRoot, state } = context;
|
|
913
|
+
const shouldAutoExpand = element.autoExpand || state.config.readonly;
|
|
914
|
+
if (!shouldAutoExpand) return;
|
|
915
|
+
if (element.multiple) {
|
|
916
|
+
const textareas = scopeRoot.querySelectorAll(
|
|
917
|
+
`textarea[name^="${fieldPath}["]`
|
|
918
|
+
);
|
|
919
|
+
textareas.forEach((textarea) => {
|
|
920
|
+
textarea.dispatchEvent(new Event("input"));
|
|
921
|
+
});
|
|
922
|
+
} else {
|
|
923
|
+
const textarea = scopeRoot.querySelector(
|
|
924
|
+
`textarea[name="${fieldPath}"]`
|
|
925
|
+
);
|
|
926
|
+
if (textarea) {
|
|
927
|
+
textarea.dispatchEvent(new Event("input"));
|
|
928
|
+
}
|
|
929
|
+
}
|
|
891
930
|
}
|
|
892
931
|
|
|
893
932
|
// src/components/number.ts
|
|
@@ -1546,6 +1585,421 @@ function updateSelectField(element, fieldPath, value, context) {
|
|
|
1546
1585
|
}
|
|
1547
1586
|
}
|
|
1548
1587
|
|
|
1588
|
+
// src/components/switcher.ts
|
|
1589
|
+
function applySelectedStyle(btn) {
|
|
1590
|
+
btn.style.backgroundColor = "var(--fb-primary-color)";
|
|
1591
|
+
btn.style.color = "#ffffff";
|
|
1592
|
+
btn.style.borderColor = "var(--fb-primary-color)";
|
|
1593
|
+
}
|
|
1594
|
+
function applyUnselectedStyle(btn) {
|
|
1595
|
+
btn.style.backgroundColor = "transparent";
|
|
1596
|
+
btn.style.color = "var(--fb-text-color)";
|
|
1597
|
+
btn.style.borderColor = "var(--fb-border-color)";
|
|
1598
|
+
}
|
|
1599
|
+
function buildSegmentedGroup(element, currentValue, hiddenInput, readonly, onChange) {
|
|
1600
|
+
const options = element.options || [];
|
|
1601
|
+
const group = document.createElement("div");
|
|
1602
|
+
group.className = "fb-switcher-group";
|
|
1603
|
+
group.style.cssText = `
|
|
1604
|
+
display: inline-flex;
|
|
1605
|
+
flex-direction: row;
|
|
1606
|
+
flex-wrap: nowrap;
|
|
1607
|
+
`;
|
|
1608
|
+
const buttons = [];
|
|
1609
|
+
options.forEach((option, index) => {
|
|
1610
|
+
const btn = document.createElement("button");
|
|
1611
|
+
btn.type = "button";
|
|
1612
|
+
btn.className = "fb-switcher-btn";
|
|
1613
|
+
btn.dataset.value = option.value;
|
|
1614
|
+
btn.textContent = option.label;
|
|
1615
|
+
btn.style.cssText = `
|
|
1616
|
+
padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
|
|
1617
|
+
font-size: var(--fb-font-size);
|
|
1618
|
+
border-width: var(--fb-border-width);
|
|
1619
|
+
border-style: solid;
|
|
1620
|
+
cursor: ${readonly ? "default" : "pointer"};
|
|
1621
|
+
transition: background-color var(--fb-transition-duration), color var(--fb-transition-duration), border-color var(--fb-transition-duration);
|
|
1622
|
+
white-space: nowrap;
|
|
1623
|
+
line-height: 1.25;
|
|
1624
|
+
outline: none;
|
|
1625
|
+
`;
|
|
1626
|
+
if (options.length === 1) {
|
|
1627
|
+
btn.style.borderRadius = "var(--fb-border-radius)";
|
|
1628
|
+
} else if (index === 0) {
|
|
1629
|
+
btn.style.borderRadius = "var(--fb-border-radius) 0 0 var(--fb-border-radius)";
|
|
1630
|
+
btn.style.borderRightWidth = "0";
|
|
1631
|
+
} else if (index === options.length - 1) {
|
|
1632
|
+
btn.style.borderRadius = "0 var(--fb-border-radius) var(--fb-border-radius) 0";
|
|
1633
|
+
} else {
|
|
1634
|
+
btn.style.borderRadius = "0";
|
|
1635
|
+
btn.style.borderRightWidth = "0";
|
|
1636
|
+
}
|
|
1637
|
+
if (option.value === currentValue) {
|
|
1638
|
+
applySelectedStyle(btn);
|
|
1639
|
+
} else {
|
|
1640
|
+
applyUnselectedStyle(btn);
|
|
1641
|
+
}
|
|
1642
|
+
if (!readonly) {
|
|
1643
|
+
btn.addEventListener("click", () => {
|
|
1644
|
+
hiddenInput.value = option.value;
|
|
1645
|
+
buttons.forEach((b) => {
|
|
1646
|
+
if (b.dataset.value === option.value) {
|
|
1647
|
+
applySelectedStyle(b);
|
|
1648
|
+
} else {
|
|
1649
|
+
applyUnselectedStyle(b);
|
|
1650
|
+
}
|
|
1651
|
+
});
|
|
1652
|
+
if (onChange) {
|
|
1653
|
+
onChange(option.value);
|
|
1654
|
+
}
|
|
1655
|
+
});
|
|
1656
|
+
btn.addEventListener("mouseenter", () => {
|
|
1657
|
+
if (hiddenInput.value !== option.value) {
|
|
1658
|
+
btn.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
1659
|
+
}
|
|
1660
|
+
});
|
|
1661
|
+
btn.addEventListener("mouseleave", () => {
|
|
1662
|
+
if (hiddenInput.value !== option.value) {
|
|
1663
|
+
btn.style.backgroundColor = "transparent";
|
|
1664
|
+
}
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
buttons.push(btn);
|
|
1668
|
+
group.appendChild(btn);
|
|
1669
|
+
});
|
|
1670
|
+
return group;
|
|
1671
|
+
}
|
|
1672
|
+
function renderSwitcherElement(element, ctx, wrapper, pathKey) {
|
|
1673
|
+
const state = ctx.state;
|
|
1674
|
+
const initialValue = String(ctx.prefill[element.key] ?? element.default ?? "");
|
|
1675
|
+
const hiddenInput = document.createElement("input");
|
|
1676
|
+
hiddenInput.type = "hidden";
|
|
1677
|
+
hiddenInput.name = pathKey;
|
|
1678
|
+
hiddenInput.value = initialValue;
|
|
1679
|
+
const readonly = state.config.readonly;
|
|
1680
|
+
const onChange = !readonly && ctx.instance ? (value) => {
|
|
1681
|
+
ctx.instance.triggerOnChange(pathKey, value);
|
|
1682
|
+
} : null;
|
|
1683
|
+
const group = buildSegmentedGroup(
|
|
1684
|
+
element,
|
|
1685
|
+
initialValue,
|
|
1686
|
+
hiddenInput,
|
|
1687
|
+
readonly,
|
|
1688
|
+
onChange
|
|
1689
|
+
);
|
|
1690
|
+
wrapper.appendChild(hiddenInput);
|
|
1691
|
+
wrapper.appendChild(group);
|
|
1692
|
+
if (!readonly) {
|
|
1693
|
+
const hint = document.createElement("p");
|
|
1694
|
+
hint.className = "text-xs text-gray-500 mt-1";
|
|
1695
|
+
hint.textContent = makeFieldHint(element, state);
|
|
1696
|
+
wrapper.appendChild(hint);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
function renderMultipleSwitcherElement(element, ctx, wrapper, pathKey) {
|
|
1700
|
+
const state = ctx.state;
|
|
1701
|
+
const prefillValues = ctx.prefill[element.key] || [];
|
|
1702
|
+
const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
|
|
1703
|
+
const minCount = element.minCount ?? 1;
|
|
1704
|
+
const maxCount = element.maxCount ?? Infinity;
|
|
1705
|
+
while (values.length < minCount) {
|
|
1706
|
+
values.push(element.default || element.options?.[0]?.value || "");
|
|
1707
|
+
}
|
|
1708
|
+
const readonly = state.config.readonly;
|
|
1709
|
+
const container = document.createElement("div");
|
|
1710
|
+
container.className = "space-y-2";
|
|
1711
|
+
wrapper.appendChild(container);
|
|
1712
|
+
function updateIndices() {
|
|
1713
|
+
const items = container.querySelectorAll(".multiple-switcher-item");
|
|
1714
|
+
items.forEach((item, index) => {
|
|
1715
|
+
const input = item.querySelector("input[type=hidden]");
|
|
1716
|
+
if (input) {
|
|
1717
|
+
input.name = `${pathKey}[${index}]`;
|
|
1718
|
+
}
|
|
1719
|
+
});
|
|
1720
|
+
}
|
|
1721
|
+
function addSwitcherItem(value = "", index = -1) {
|
|
1722
|
+
const currentIndex = index === -1 ? container.children.length : index;
|
|
1723
|
+
const itemPathKey = `${pathKey}[${currentIndex}]`;
|
|
1724
|
+
const itemWrapper = document.createElement("div");
|
|
1725
|
+
itemWrapper.className = "multiple-switcher-item flex items-center gap-2";
|
|
1726
|
+
const hiddenInput = document.createElement("input");
|
|
1727
|
+
hiddenInput.type = "hidden";
|
|
1728
|
+
hiddenInput.name = itemPathKey;
|
|
1729
|
+
hiddenInput.value = value;
|
|
1730
|
+
itemWrapper.appendChild(hiddenInput);
|
|
1731
|
+
const onChange = !readonly && ctx.instance ? (val) => {
|
|
1732
|
+
ctx.instance.triggerOnChange(hiddenInput.name, val);
|
|
1733
|
+
} : null;
|
|
1734
|
+
const group = buildSegmentedGroup(
|
|
1735
|
+
element,
|
|
1736
|
+
value,
|
|
1737
|
+
hiddenInput,
|
|
1738
|
+
readonly,
|
|
1739
|
+
onChange
|
|
1740
|
+
);
|
|
1741
|
+
itemWrapper.appendChild(group);
|
|
1742
|
+
if (index === -1) {
|
|
1743
|
+
container.appendChild(itemWrapper);
|
|
1744
|
+
} else {
|
|
1745
|
+
container.insertBefore(itemWrapper, container.children[index]);
|
|
1746
|
+
}
|
|
1747
|
+
updateIndices();
|
|
1748
|
+
return itemWrapper;
|
|
1749
|
+
}
|
|
1750
|
+
function updateRemoveButtons() {
|
|
1751
|
+
if (readonly) return;
|
|
1752
|
+
const items = container.querySelectorAll(".multiple-switcher-item");
|
|
1753
|
+
const currentCount = items.length;
|
|
1754
|
+
items.forEach((item) => {
|
|
1755
|
+
let removeBtn = item.querySelector(
|
|
1756
|
+
".remove-item-btn"
|
|
1757
|
+
);
|
|
1758
|
+
if (!removeBtn) {
|
|
1759
|
+
removeBtn = document.createElement("button");
|
|
1760
|
+
removeBtn.type = "button";
|
|
1761
|
+
removeBtn.className = "remove-item-btn px-2 py-1 rounded";
|
|
1762
|
+
removeBtn.style.cssText = `
|
|
1763
|
+
color: var(--fb-error-color);
|
|
1764
|
+
background-color: transparent;
|
|
1765
|
+
transition: background-color var(--fb-transition-duration);
|
|
1766
|
+
`;
|
|
1767
|
+
removeBtn.innerHTML = "\u2715";
|
|
1768
|
+
removeBtn.addEventListener("mouseenter", () => {
|
|
1769
|
+
removeBtn.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
1770
|
+
});
|
|
1771
|
+
removeBtn.addEventListener("mouseleave", () => {
|
|
1772
|
+
removeBtn.style.backgroundColor = "transparent";
|
|
1773
|
+
});
|
|
1774
|
+
removeBtn.onclick = () => {
|
|
1775
|
+
const currentIndex = Array.from(container.children).indexOf(
|
|
1776
|
+
item
|
|
1777
|
+
);
|
|
1778
|
+
if (container.children.length > minCount) {
|
|
1779
|
+
values.splice(currentIndex, 1);
|
|
1780
|
+
item.remove();
|
|
1781
|
+
updateIndices();
|
|
1782
|
+
updateAddButton();
|
|
1783
|
+
updateRemoveButtons();
|
|
1784
|
+
}
|
|
1785
|
+
};
|
|
1786
|
+
item.appendChild(removeBtn);
|
|
1787
|
+
}
|
|
1788
|
+
const disabled = currentCount <= minCount;
|
|
1789
|
+
removeBtn.disabled = disabled;
|
|
1790
|
+
removeBtn.style.opacity = disabled ? "0.5" : "1";
|
|
1791
|
+
removeBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
let addRow = null;
|
|
1795
|
+
let countDisplay = null;
|
|
1796
|
+
if (!readonly) {
|
|
1797
|
+
addRow = document.createElement("div");
|
|
1798
|
+
addRow.className = "flex items-center gap-3 mt-2";
|
|
1799
|
+
const addBtn = document.createElement("button");
|
|
1800
|
+
addBtn.type = "button";
|
|
1801
|
+
addBtn.className = "add-switcher-btn px-3 py-1 rounded";
|
|
1802
|
+
addBtn.style.cssText = `
|
|
1803
|
+
color: var(--fb-primary-color);
|
|
1804
|
+
border: var(--fb-border-width) solid var(--fb-primary-color);
|
|
1805
|
+
background-color: transparent;
|
|
1806
|
+
font-size: var(--fb-font-size);
|
|
1807
|
+
transition: all var(--fb-transition-duration);
|
|
1808
|
+
`;
|
|
1809
|
+
addBtn.textContent = "+";
|
|
1810
|
+
addBtn.addEventListener("mouseenter", () => {
|
|
1811
|
+
addBtn.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
1812
|
+
});
|
|
1813
|
+
addBtn.addEventListener("mouseleave", () => {
|
|
1814
|
+
addBtn.style.backgroundColor = "transparent";
|
|
1815
|
+
});
|
|
1816
|
+
addBtn.onclick = () => {
|
|
1817
|
+
const defaultValue = element.default || element.options?.[0]?.value || "";
|
|
1818
|
+
values.push(defaultValue);
|
|
1819
|
+
addSwitcherItem(defaultValue);
|
|
1820
|
+
updateAddButton();
|
|
1821
|
+
updateRemoveButtons();
|
|
1822
|
+
};
|
|
1823
|
+
countDisplay = document.createElement("span");
|
|
1824
|
+
countDisplay.className = "text-sm text-gray-500";
|
|
1825
|
+
addRow.appendChild(addBtn);
|
|
1826
|
+
addRow.appendChild(countDisplay);
|
|
1827
|
+
wrapper.appendChild(addRow);
|
|
1828
|
+
}
|
|
1829
|
+
function updateAddButton() {
|
|
1830
|
+
if (!addRow || !countDisplay) return;
|
|
1831
|
+
const addBtn = addRow.querySelector(
|
|
1832
|
+
".add-switcher-btn"
|
|
1833
|
+
);
|
|
1834
|
+
if (addBtn) {
|
|
1835
|
+
const disabled = values.length >= maxCount;
|
|
1836
|
+
addBtn.disabled = disabled;
|
|
1837
|
+
addBtn.style.opacity = disabled ? "0.5" : "1";
|
|
1838
|
+
addBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
1839
|
+
}
|
|
1840
|
+
countDisplay.textContent = `${values.length}/${maxCount === Infinity ? "\u221E" : maxCount}`;
|
|
1841
|
+
}
|
|
1842
|
+
values.forEach((value) => addSwitcherItem(value));
|
|
1843
|
+
updateAddButton();
|
|
1844
|
+
updateRemoveButtons();
|
|
1845
|
+
if (!readonly) {
|
|
1846
|
+
const hint = document.createElement("p");
|
|
1847
|
+
hint.className = "text-xs text-gray-500 mt-1";
|
|
1848
|
+
hint.textContent = makeFieldHint(element, state);
|
|
1849
|
+
wrapper.appendChild(hint);
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
function validateSwitcherElement(element, key, context) {
|
|
1853
|
+
const errors = [];
|
|
1854
|
+
const { scopeRoot, skipValidation } = context;
|
|
1855
|
+
const markValidity = (input, errorMessage) => {
|
|
1856
|
+
if (!input) return;
|
|
1857
|
+
const errorId = `error-${input.getAttribute("name") || Math.random().toString(36).substring(7)}`;
|
|
1858
|
+
let errorElement = document.getElementById(errorId);
|
|
1859
|
+
if (errorMessage) {
|
|
1860
|
+
input.classList.add("invalid");
|
|
1861
|
+
input.title = errorMessage;
|
|
1862
|
+
if (!errorElement) {
|
|
1863
|
+
errorElement = document.createElement("div");
|
|
1864
|
+
errorElement.id = errorId;
|
|
1865
|
+
errorElement.className = "error-message";
|
|
1866
|
+
errorElement.style.cssText = `
|
|
1867
|
+
color: var(--fb-error-color);
|
|
1868
|
+
font-size: var(--fb-font-size-small);
|
|
1869
|
+
margin-top: 0.25rem;
|
|
1870
|
+
`;
|
|
1871
|
+
if (input.nextSibling) {
|
|
1872
|
+
input.parentNode?.insertBefore(errorElement, input.nextSibling);
|
|
1873
|
+
} else {
|
|
1874
|
+
input.parentNode?.appendChild(errorElement);
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
errorElement.textContent = errorMessage;
|
|
1878
|
+
errorElement.style.display = "block";
|
|
1879
|
+
} else {
|
|
1880
|
+
input.classList.remove("invalid");
|
|
1881
|
+
input.title = "";
|
|
1882
|
+
if (errorElement) {
|
|
1883
|
+
errorElement.remove();
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
};
|
|
1887
|
+
const validateMultipleCount = (fieldKey, values, el, filterFn) => {
|
|
1888
|
+
if (skipValidation) return;
|
|
1889
|
+
const { state } = context;
|
|
1890
|
+
const filteredValues = values.filter(filterFn);
|
|
1891
|
+
const minCount = "minCount" in el ? el.minCount ?? 1 : 1;
|
|
1892
|
+
const maxCount = "maxCount" in el ? el.maxCount ?? Infinity : Infinity;
|
|
1893
|
+
if (el.required && filteredValues.length === 0) {
|
|
1894
|
+
errors.push(`${fieldKey}: ${t("required", state)}`);
|
|
1895
|
+
}
|
|
1896
|
+
if (filteredValues.length < minCount) {
|
|
1897
|
+
errors.push(`${fieldKey}: ${t("minItems", state, { min: minCount })}`);
|
|
1898
|
+
}
|
|
1899
|
+
if (filteredValues.length > maxCount) {
|
|
1900
|
+
errors.push(`${fieldKey}: ${t("maxItems", state, { max: maxCount })}`);
|
|
1901
|
+
}
|
|
1902
|
+
};
|
|
1903
|
+
const validOptionValues = new Set(
|
|
1904
|
+
"options" in element ? element.options.map((o) => o.value) : []
|
|
1905
|
+
);
|
|
1906
|
+
if ("multiple" in element && element.multiple) {
|
|
1907
|
+
const inputs = scopeRoot.querySelectorAll(
|
|
1908
|
+
`input[type="hidden"][name^="${key}["]`
|
|
1909
|
+
);
|
|
1910
|
+
const values = [];
|
|
1911
|
+
inputs.forEach((input) => {
|
|
1912
|
+
const val = input?.value ?? "";
|
|
1913
|
+
values.push(val);
|
|
1914
|
+
if (!skipValidation && val !== "" && !validOptionValues.has(val)) {
|
|
1915
|
+
const msg = t("invalidOption", context.state);
|
|
1916
|
+
markValidity(input, msg);
|
|
1917
|
+
errors.push(`${key}: ${msg}`);
|
|
1918
|
+
} else {
|
|
1919
|
+
markValidity(input, null);
|
|
1920
|
+
}
|
|
1921
|
+
});
|
|
1922
|
+
validateMultipleCount(key, values, element, (v) => v !== "");
|
|
1923
|
+
return { value: values, errors };
|
|
1924
|
+
} else {
|
|
1925
|
+
const input = scopeRoot.querySelector(
|
|
1926
|
+
`input[type="hidden"][name$="${key}"]`
|
|
1927
|
+
);
|
|
1928
|
+
const val = input?.value ?? "";
|
|
1929
|
+
if (!skipValidation && element.required && val === "") {
|
|
1930
|
+
const msg = t("required", context.state);
|
|
1931
|
+
errors.push(`${key}: ${msg}`);
|
|
1932
|
+
markValidity(input, msg);
|
|
1933
|
+
return { value: null, errors };
|
|
1934
|
+
}
|
|
1935
|
+
if (!skipValidation && val !== "" && !validOptionValues.has(val)) {
|
|
1936
|
+
const msg = t("invalidOption", context.state);
|
|
1937
|
+
errors.push(`${key}: ${msg}`);
|
|
1938
|
+
markValidity(input, msg);
|
|
1939
|
+
return { value: null, errors };
|
|
1940
|
+
}
|
|
1941
|
+
markValidity(input, null);
|
|
1942
|
+
return { value: val === "" ? null : val, errors };
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
function updateSwitcherField(element, fieldPath, value, context) {
|
|
1946
|
+
const { scopeRoot } = context;
|
|
1947
|
+
if ("multiple" in element && element.multiple) {
|
|
1948
|
+
if (!Array.isArray(value)) {
|
|
1949
|
+
console.warn(
|
|
1950
|
+
`updateSwitcherField: Expected array for multiple field "${fieldPath}", got ${typeof value}`
|
|
1951
|
+
);
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1954
|
+
const inputs = scopeRoot.querySelectorAll(
|
|
1955
|
+
`input[type="hidden"][name^="${fieldPath}["]`
|
|
1956
|
+
);
|
|
1957
|
+
inputs.forEach((input, index) => {
|
|
1958
|
+
if (index < value.length) {
|
|
1959
|
+
const newVal = value[index] != null ? String(value[index]) : "";
|
|
1960
|
+
input.value = newVal;
|
|
1961
|
+
const group = input.parentElement?.querySelector(".fb-switcher-group");
|
|
1962
|
+
if (group) {
|
|
1963
|
+
group.querySelectorAll(".fb-switcher-btn").forEach((btn) => {
|
|
1964
|
+
if (btn.dataset.value === newVal) {
|
|
1965
|
+
applySelectedStyle(btn);
|
|
1966
|
+
} else {
|
|
1967
|
+
applyUnselectedStyle(btn);
|
|
1968
|
+
}
|
|
1969
|
+
});
|
|
1970
|
+
}
|
|
1971
|
+
input.classList.remove("invalid");
|
|
1972
|
+
input.title = "";
|
|
1973
|
+
}
|
|
1974
|
+
});
|
|
1975
|
+
if (value.length !== inputs.length) {
|
|
1976
|
+
console.warn(
|
|
1977
|
+
`updateSwitcherField: Multiple field "${fieldPath}" has ${inputs.length} inputs but received ${value.length} values. Consider re-rendering for add/remove.`
|
|
1978
|
+
);
|
|
1979
|
+
}
|
|
1980
|
+
} else {
|
|
1981
|
+
const input = scopeRoot.querySelector(
|
|
1982
|
+
`input[type="hidden"][name="${fieldPath}"]`
|
|
1983
|
+
);
|
|
1984
|
+
if (input) {
|
|
1985
|
+
const newVal = value != null ? String(value) : "";
|
|
1986
|
+
input.value = newVal;
|
|
1987
|
+
const group = input.parentElement?.querySelector(".fb-switcher-group");
|
|
1988
|
+
if (group) {
|
|
1989
|
+
group.querySelectorAll(".fb-switcher-btn").forEach((btn) => {
|
|
1990
|
+
if (btn.dataset.value === newVal) {
|
|
1991
|
+
applySelectedStyle(btn);
|
|
1992
|
+
} else {
|
|
1993
|
+
applyUnselectedStyle(btn);
|
|
1994
|
+
}
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
input.classList.remove("invalid");
|
|
1998
|
+
input.title = "";
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
|
|
1549
2003
|
// src/components/file.ts
|
|
1550
2004
|
function renderLocalImagePreview(container, file, fileName, state) {
|
|
1551
2005
|
const img = document.createElement("img");
|
|
@@ -3877,10 +4331,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
3877
4331
|
rem.addEventListener("mouseleave", () => {
|
|
3878
4332
|
rem.style.backgroundColor = "transparent";
|
|
3879
4333
|
});
|
|
3880
|
-
rem.onclick = () =>
|
|
3881
|
-
item.remove();
|
|
3882
|
-
updateAddButton();
|
|
3883
|
-
};
|
|
4334
|
+
rem.onclick = () => handleRemoveItem(item);
|
|
3884
4335
|
item.style.position = "relative";
|
|
3885
4336
|
item.appendChild(rem);
|
|
3886
4337
|
}
|
|
@@ -3902,6 +4353,10 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
3902
4353
|
}
|
|
3903
4354
|
countDisplay.textContent = `${currentCount}/${max === Infinity ? "\u221E" : max}`;
|
|
3904
4355
|
};
|
|
4356
|
+
const handleRemoveItem = (item) => {
|
|
4357
|
+
item.remove();
|
|
4358
|
+
updateAddButton();
|
|
4359
|
+
};
|
|
3905
4360
|
if (pre && Array.isArray(pre)) {
|
|
3906
4361
|
pre.forEach((prefillObj, idx) => {
|
|
3907
4362
|
const mergedPrefill = mergeWithDefaults(prefillObj || {}, childDefaults);
|
|
@@ -3945,10 +4400,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
3945
4400
|
rem.addEventListener("mouseleave", () => {
|
|
3946
4401
|
rem.style.backgroundColor = "transparent";
|
|
3947
4402
|
});
|
|
3948
|
-
rem.onclick = () =>
|
|
3949
|
-
item.remove();
|
|
3950
|
-
updateAddButton();
|
|
3951
|
-
};
|
|
4403
|
+
rem.onclick = () => handleRemoveItem(item);
|
|
3952
4404
|
item.style.position = "relative";
|
|
3953
4405
|
item.appendChild(rem);
|
|
3954
4406
|
}
|
|
@@ -3999,8 +4451,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
3999
4451
|
});
|
|
4000
4452
|
rem.onclick = () => {
|
|
4001
4453
|
if (countItems() > min) {
|
|
4002
|
-
item
|
|
4003
|
-
updateAddButton();
|
|
4454
|
+
handleRemoveItem(item);
|
|
4004
4455
|
}
|
|
4005
4456
|
};
|
|
4006
4457
|
item.style.position = "relative";
|
|
@@ -4058,15 +4509,16 @@ function validateContainerElement(element, key, context) {
|
|
|
4058
4509
|
"[data-container-item]"
|
|
4059
4510
|
);
|
|
4060
4511
|
const containerWrappers = Array.from(allContainerWrappers).filter((el) => {
|
|
4061
|
-
const attr = el.getAttribute("data-container-item");
|
|
4062
|
-
|
|
4512
|
+
const attr = el.getAttribute("data-container-item") || "";
|
|
4513
|
+
if (!attr.startsWith(`${key}[`)) return false;
|
|
4514
|
+
const suffix = attr.slice(key.length);
|
|
4515
|
+
return /^\[\d+\]$/.test(suffix);
|
|
4063
4516
|
});
|
|
4064
|
-
|
|
4065
|
-
for (let i = 0; i < itemCount; i++) {
|
|
4517
|
+
containerWrappers.forEach((itemContainer) => {
|
|
4066
4518
|
const itemData = {};
|
|
4067
|
-
const
|
|
4068
|
-
|
|
4069
|
-
)
|
|
4519
|
+
const containerAttr = itemContainer.getAttribute("data-container-item") || "";
|
|
4520
|
+
const indexMatch = containerAttr.match(/\[(\d+)\]$/);
|
|
4521
|
+
const domIndex = indexMatch ? parseInt(indexMatch[1], 10) : 0;
|
|
4070
4522
|
element.elements.forEach((child) => {
|
|
4071
4523
|
if (child.enableIf) {
|
|
4072
4524
|
try {
|
|
@@ -4083,7 +4535,7 @@ function validateContainerElement(element, key, context) {
|
|
|
4083
4535
|
}
|
|
4084
4536
|
} catch (error) {
|
|
4085
4537
|
console.error(
|
|
4086
|
-
`Error evaluating enableIf for field "${child.key}" in container "${key}[${
|
|
4538
|
+
`Error evaluating enableIf for field "${child.key}" in container "${key}[${domIndex}]":`,
|
|
4087
4539
|
error
|
|
4088
4540
|
);
|
|
4089
4541
|
}
|
|
@@ -4091,7 +4543,7 @@ function validateContainerElement(element, key, context) {
|
|
|
4091
4543
|
if (child.hidden || child.type === "hidden") {
|
|
4092
4544
|
itemData[child.key] = child.default !== void 0 ? child.default : null;
|
|
4093
4545
|
} else {
|
|
4094
|
-
const childKey = `${key}[${
|
|
4546
|
+
const childKey = `${key}[${domIndex}].${child.key}`;
|
|
4095
4547
|
itemData[child.key] = validateElement(
|
|
4096
4548
|
{ ...child, key: childKey },
|
|
4097
4549
|
{ path },
|
|
@@ -4100,7 +4552,7 @@ function validateContainerElement(element, key, context) {
|
|
|
4100
4552
|
}
|
|
4101
4553
|
});
|
|
4102
4554
|
items.push(itemData);
|
|
4103
|
-
}
|
|
4555
|
+
});
|
|
4104
4556
|
validateContainerCount(key, items, element);
|
|
4105
4557
|
return { value: items, errors };
|
|
4106
4558
|
} else {
|
|
@@ -4560,6 +5012,13 @@ function dispatchToRenderer(element, ctx, wrapper, pathKey) {
|
|
|
4560
5012
|
renderSelectElement(element, ctx, wrapper, pathKey);
|
|
4561
5013
|
}
|
|
4562
5014
|
break;
|
|
5015
|
+
case "switcher":
|
|
5016
|
+
if (isMultiple) {
|
|
5017
|
+
renderMultipleSwitcherElement(element, ctx, wrapper, pathKey);
|
|
5018
|
+
} else {
|
|
5019
|
+
renderSwitcherElement(element, ctx, wrapper, pathKey);
|
|
5020
|
+
}
|
|
5021
|
+
break;
|
|
4563
5022
|
case "file":
|
|
4564
5023
|
if (isMultiple) {
|
|
4565
5024
|
renderMultipleFileElement(element, ctx, wrapper, pathKey);
|
|
@@ -4683,7 +5142,8 @@ var defaultConfig = {
|
|
|
4683
5142
|
invalidHexColour: "Invalid hex color",
|
|
4684
5143
|
minFiles: "Minimum {min} files required",
|
|
4685
5144
|
maxFiles: "Maximum {max} files allowed",
|
|
4686
|
-
unsupportedFieldType: "Unsupported field type: {type}"
|
|
5145
|
+
unsupportedFieldType: "Unsupported field type: {type}",
|
|
5146
|
+
invalidOption: "Invalid option"
|
|
4687
5147
|
},
|
|
4688
5148
|
ru: {
|
|
4689
5149
|
// UI texts
|
|
@@ -4728,7 +5188,8 @@ var defaultConfig = {
|
|
|
4728
5188
|
invalidHexColour: "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442 \u0446\u0432\u0435\u0442\u0430",
|
|
4729
5189
|
minFiles: "\u041C\u0438\u043D\u0438\u043C\u0443\u043C {min} \u0444\u0430\u0439\u043B\u043E\u0432",
|
|
4730
5190
|
maxFiles: "\u041C\u0430\u043A\u0441\u0438\u043C\u0443\u043C {max} \u0444\u0430\u0439\u043B\u043E\u0432",
|
|
4731
|
-
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}"
|
|
5191
|
+
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}",
|
|
5192
|
+
invalidOption: "\u041D\u0435\u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435"
|
|
4732
5193
|
}
|
|
4733
5194
|
},
|
|
4734
5195
|
theme: {}
|
|
@@ -4973,6 +5434,10 @@ var componentRegistry = {
|
|
|
4973
5434
|
validate: validateSelectElement,
|
|
4974
5435
|
update: updateSelectField
|
|
4975
5436
|
},
|
|
5437
|
+
switcher: {
|
|
5438
|
+
validate: validateSwitcherElement,
|
|
5439
|
+
update: updateSwitcherField
|
|
5440
|
+
},
|
|
4976
5441
|
file: {
|
|
4977
5442
|
validate: validateFileElement,
|
|
4978
5443
|
update: updateFileField
|
|
@@ -5751,8 +6216,18 @@ var FormBuilderInstance = class {
|
|
|
5751
6216
|
if ((element.type === "container" || element.type === "group") && "elements" in element && element.elements) {
|
|
5752
6217
|
const containerData = formData?.[element.key];
|
|
5753
6218
|
if (Array.isArray(containerData)) {
|
|
5754
|
-
|
|
5755
|
-
|
|
6219
|
+
const containerItems = this.state.formRoot.querySelectorAll(
|
|
6220
|
+
`[data-container-item]`
|
|
6221
|
+
);
|
|
6222
|
+
const directItems = Array.from(containerItems).filter((el) => {
|
|
6223
|
+
const attr = el.getAttribute("data-container-item") || "";
|
|
6224
|
+
if (!attr.startsWith(`${fullPath}[`)) return false;
|
|
6225
|
+
const suffix = attr.slice(fullPath.length);
|
|
6226
|
+
return /^\[\d+\]$/.test(suffix);
|
|
6227
|
+
});
|
|
6228
|
+
directItems.forEach((el) => {
|
|
6229
|
+
const attr = el.getAttribute("data-container-item") || "";
|
|
6230
|
+
checkElements(element.elements, attr);
|
|
5756
6231
|
});
|
|
5757
6232
|
} else {
|
|
5758
6233
|
checkElements(element.elements, fullPath);
|