@dmitryvim/form-builder 0.2.17 → 0.2.19

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/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");
@@ -4253,6 +4707,1009 @@ function updateGroupField(element, fieldPath, value, context) {
4253
4707
  return updateContainerField(containerElement, fieldPath, value, context);
4254
4708
  }
4255
4709
 
4710
+ // src/components/table.ts
4711
+ function createEmptyCells(rows, cols) {
4712
+ return Array.from(
4713
+ { length: rows },
4714
+ () => Array.from({ length: cols }, () => "")
4715
+ );
4716
+ }
4717
+ function getShadowingMerge(row, col, merges) {
4718
+ for (const m of merges) {
4719
+ if (m.row === row && m.col === col) {
4720
+ return null;
4721
+ }
4722
+ if (row >= m.row && row < m.row + m.rowspan && col >= m.col && col < m.col + m.colspan) {
4723
+ return m;
4724
+ }
4725
+ }
4726
+ return null;
4727
+ }
4728
+ function getMergeAt(row, col, merges) {
4729
+ return merges.find((m) => m.row === row && m.col === col) ?? null;
4730
+ }
4731
+ function selectionRange(sel) {
4732
+ if (!sel.anchor) return null;
4733
+ const focus = sel.focus ?? sel.anchor;
4734
+ return {
4735
+ r1: Math.min(sel.anchor.row, focus.row),
4736
+ c1: Math.min(sel.anchor.col, focus.col),
4737
+ r2: Math.max(sel.anchor.row, focus.row),
4738
+ c2: Math.max(sel.anchor.col, focus.col)
4739
+ };
4740
+ }
4741
+ function makeOverlayCircleBtn(opts) {
4742
+ const btn = document.createElement("button");
4743
+ btn.type = "button";
4744
+ btn.textContent = opts.label;
4745
+ btn.title = opts.title;
4746
+ btn.style.cssText = `
4747
+ position: absolute;
4748
+ width: ${opts.size}px;
4749
+ height: ${opts.size}px;
4750
+ border-radius: 50%;
4751
+ border: none;
4752
+ background: ${opts.color};
4753
+ color: ${opts.textColor};
4754
+ font-size: ${Math.floor(opts.size * 0.6)}px;
4755
+ line-height: ${opts.size}px;
4756
+ text-align: center;
4757
+ cursor: pointer;
4758
+ z-index: 10;
4759
+ padding: 0;
4760
+ display: flex;
4761
+ align-items: center;
4762
+ justify-content: center;
4763
+ box-shadow: 0 1px 4px rgba(0,0,0,0.2);
4764
+ transition: transform 0.1s, opacity 0.1s;
4765
+ pointer-events: all;
4766
+ `;
4767
+ btn.addEventListener("mouseenter", () => {
4768
+ btn.style.transform = "scale(1.15)";
4769
+ });
4770
+ btn.addEventListener("mouseleave", () => {
4771
+ btn.style.transform = "scale(1)";
4772
+ });
4773
+ btn.addEventListener("click", (e) => {
4774
+ e.stopPropagation();
4775
+ opts.onClick(e);
4776
+ });
4777
+ return btn;
4778
+ }
4779
+ function renderReadonlyTable(data, wrapper) {
4780
+ const { cells, merges = [] } = data;
4781
+ if (cells.length === 0) return;
4782
+ const numCols = cells[0].length;
4783
+ const tableEl = document.createElement("table");
4784
+ tableEl.style.cssText = `
4785
+ width: 100%;
4786
+ border-collapse: collapse;
4787
+ border: var(--fb-border-width) solid var(--fb-border-color);
4788
+ border-radius: var(--fb-border-radius);
4789
+ font-size: var(--fb-font-size);
4790
+ font-family: var(--fb-font-family);
4791
+ color: var(--fb-text-color);
4792
+ `;
4793
+ cells.forEach((rowData, rIdx) => {
4794
+ const section = rIdx === 0 ? tableEl.createTHead() : tableEl.tBodies[0] ?? tableEl.createTBody();
4795
+ const tr = section.insertRow();
4796
+ for (let cIdx = 0; cIdx < numCols; cIdx++) {
4797
+ if (getShadowingMerge(rIdx, cIdx, merges)) {
4798
+ continue;
4799
+ }
4800
+ const merge = getMergeAt(rIdx, cIdx, merges);
4801
+ const td = document.createElement(rIdx === 0 ? "th" : "td");
4802
+ if (merge) {
4803
+ if (merge.rowspan > 1) td.rowSpan = merge.rowspan;
4804
+ if (merge.colspan > 1) td.colSpan = merge.colspan;
4805
+ }
4806
+ td.textContent = rowData[cIdx] ?? "";
4807
+ td.style.cssText = `
4808
+ padding: 6px 10px;
4809
+ border: var(--fb-border-width) solid var(--fb-border-color);
4810
+ text-align: left;
4811
+ vertical-align: top;
4812
+ ${rIdx === 0 ? "background-color: var(--fb-background-hover-color); font-weight: 600;" : ""}
4813
+ `;
4814
+ tr.appendChild(td);
4815
+ }
4816
+ });
4817
+ const scrollWrapper = document.createElement("div");
4818
+ scrollWrapper.style.cssText = "overflow-x: auto; max-width: 100%;";
4819
+ scrollWrapper.appendChild(tableEl);
4820
+ wrapper.appendChild(scrollWrapper);
4821
+ }
4822
+ function startCellEditing(span, r, c, getCells, persistValue, selectCell) {
4823
+ if (span.contentEditable === "true") return;
4824
+ span.contentEditable = "true";
4825
+ span.focus();
4826
+ const domRange = document.createRange();
4827
+ const winSel = window.getSelection();
4828
+ domRange.selectNodeContents(span);
4829
+ domRange.collapse(false);
4830
+ winSel?.removeAllRanges();
4831
+ winSel?.addRange(domRange);
4832
+ function commit() {
4833
+ span.contentEditable = "inherit";
4834
+ const cells = getCells();
4835
+ if (cells[r]) {
4836
+ cells[r][c] = span.textContent ?? "";
4837
+ }
4838
+ persistValue();
4839
+ }
4840
+ function onKeyDown(e) {
4841
+ const cells = getCells();
4842
+ const numCols = cells[0]?.length ?? 0;
4843
+ if (e.key === "Escape") {
4844
+ span.contentEditable = "inherit";
4845
+ span.textContent = cells[r]?.[c] ?? "";
4846
+ span.removeEventListener("keydown", onKeyDown);
4847
+ span.removeEventListener("blur", onBlur);
4848
+ return;
4849
+ }
4850
+ if (e.key === "Enter" && !e.shiftKey) {
4851
+ e.preventDefault();
4852
+ e.stopPropagation();
4853
+ commit();
4854
+ span.removeEventListener("keydown", onKeyDown);
4855
+ span.removeEventListener("blur", onBlur);
4856
+ const nextRow = r + 1 < cells.length ? r + 1 : r;
4857
+ selectCell(nextRow, c);
4858
+ return;
4859
+ }
4860
+ if (e.key === "Tab") {
4861
+ e.preventDefault();
4862
+ e.stopPropagation();
4863
+ commit();
4864
+ span.removeEventListener("keydown", onKeyDown);
4865
+ span.removeEventListener("blur", onBlur);
4866
+ let nr = r;
4867
+ let nc = e.shiftKey ? c - 1 : c + 1;
4868
+ if (nc < 0) {
4869
+ nc = numCols - 1;
4870
+ nr = Math.max(0, r - 1);
4871
+ }
4872
+ if (nc >= numCols) {
4873
+ nc = 0;
4874
+ nr = Math.min(cells.length - 1, r + 1);
4875
+ }
4876
+ selectCell(nr, nc);
4877
+ }
4878
+ }
4879
+ function onBlur() {
4880
+ if (span.contentEditable === "true") {
4881
+ commit();
4882
+ span.removeEventListener("keydown", onKeyDown);
4883
+ span.removeEventListener("blur", onBlur);
4884
+ }
4885
+ }
4886
+ span.addEventListener("keydown", onKeyDown);
4887
+ span.addEventListener("blur", onBlur);
4888
+ }
4889
+ function renderEditTable(element, initialData, pathKey, ctx, wrapper) {
4890
+ const state = ctx.state;
4891
+ const instance = ctx.instance;
4892
+ const cells = initialData.cells.length > 0 ? initialData.cells.map((r) => [...r]) : createEmptyCells(element.rows ?? 3, element.columns ?? 3);
4893
+ let merges = initialData.merges ? [...initialData.merges] : [];
4894
+ const sel = { anchor: null, focus: null, dragging: false };
4895
+ const hiddenInput = document.createElement("input");
4896
+ hiddenInput.type = "hidden";
4897
+ hiddenInput.name = pathKey;
4898
+ hiddenInput.value = JSON.stringify({ cells, merges });
4899
+ wrapper.appendChild(hiddenInput);
4900
+ function persistValue() {
4901
+ hiddenInput.value = JSON.stringify({ cells, merges });
4902
+ if (instance) {
4903
+ instance.triggerOnChange(pathKey, { cells, merges });
4904
+ }
4905
+ }
4906
+ hiddenInput._applyExternalUpdate = (data) => {
4907
+ cells.length = 0;
4908
+ data.cells.forEach((row) => cells.push([...row]));
4909
+ merges.length = 0;
4910
+ if (data.merges) {
4911
+ data.merges.forEach((m) => merges.push({ ...m }));
4912
+ }
4913
+ sel.anchor = null;
4914
+ sel.focus = null;
4915
+ persistValue();
4916
+ rebuild();
4917
+ };
4918
+ const tableWrapper = document.createElement("div");
4919
+ tableWrapper.style.cssText = "position: relative; padding: 20px 20px 20px 24px; overflow-x: auto; max-width: 100%;";
4920
+ const tableEl = document.createElement("table");
4921
+ tableEl.style.cssText = `
4922
+ border-collapse: collapse;
4923
+ font-size: var(--fb-font-size);
4924
+ font-family: var(--fb-font-family);
4925
+ color: var(--fb-text-color);
4926
+ table-layout: fixed;
4927
+ `;
4928
+ tableWrapper.appendChild(tableEl);
4929
+ wrapper.appendChild(tableWrapper);
4930
+ const contextMenu = document.createElement("div");
4931
+ contextMenu.style.cssText = `
4932
+ position: fixed;
4933
+ display: none;
4934
+ background: white;
4935
+ border: 1px solid var(--fb-border-color);
4936
+ border-radius: var(--fb-border-radius);
4937
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
4938
+ padding: 4px;
4939
+ z-index: 1000;
4940
+ gap: 4px;
4941
+ flex-direction: column;
4942
+ `;
4943
+ wrapper.appendChild(contextMenu);
4944
+ function makeContextMenuBtn(label, onClick) {
4945
+ const btn = document.createElement("button");
4946
+ btn.type = "button";
4947
+ btn.textContent = label;
4948
+ btn.style.cssText = `
4949
+ padding: 4px 10px;
4950
+ font-size: var(--fb-font-size-small);
4951
+ color: var(--fb-text-color);
4952
+ border: 1px solid var(--fb-border-color);
4953
+ border-radius: var(--fb-border-radius);
4954
+ background: transparent;
4955
+ cursor: pointer;
4956
+ white-space: nowrap;
4957
+ text-align: left;
4958
+ `;
4959
+ btn.addEventListener("mouseenter", () => {
4960
+ btn.style.background = "var(--fb-background-hover-color)";
4961
+ });
4962
+ btn.addEventListener("mouseleave", () => {
4963
+ btn.style.background = "transparent";
4964
+ });
4965
+ btn.addEventListener("click", () => {
4966
+ hideContextMenu();
4967
+ onClick();
4968
+ });
4969
+ return btn;
4970
+ }
4971
+ function showContextMenu(x, y) {
4972
+ contextMenu.innerHTML = "";
4973
+ contextMenu.style.display = "flex";
4974
+ const range = selectionRange(sel);
4975
+ const isMultiCell = range && (range.r1 !== range.r2 || range.c1 !== range.c2);
4976
+ const isSingleMerged = sel.anchor && getMergeAt(sel.anchor.row, sel.anchor.col, merges);
4977
+ if (isMultiCell) {
4978
+ contextMenu.appendChild(
4979
+ makeContextMenuBtn(t("tableMergeCells", state), mergeCells)
4980
+ );
4981
+ }
4982
+ if (isSingleMerged) {
4983
+ contextMenu.appendChild(
4984
+ makeContextMenuBtn(t("tableSplitCell", state), splitCell)
4985
+ );
4986
+ }
4987
+ if (!contextMenu.firstChild) {
4988
+ hideContextMenu();
4989
+ return;
4990
+ }
4991
+ const menuWidth = 140;
4992
+ const menuHeight = contextMenu.children.length * 32 + 8;
4993
+ const vw = window.innerWidth;
4994
+ const vh = window.innerHeight;
4995
+ const left = x + menuWidth > vw ? x - menuWidth : x;
4996
+ const top = y + menuHeight > vh ? y - menuHeight : y;
4997
+ contextMenu.style.left = `${left}px`;
4998
+ contextMenu.style.top = `${top}px`;
4999
+ }
5000
+ function hideContextMenu() {
5001
+ contextMenu.style.display = "none";
5002
+ }
5003
+ const menuDismissCtrl = new AbortController();
5004
+ document.addEventListener("mousedown", (e) => {
5005
+ if (!wrapper.isConnected) {
5006
+ menuDismissCtrl.abort();
5007
+ return;
5008
+ }
5009
+ if (!contextMenu.contains(e.target)) {
5010
+ hideContextMenu();
5011
+ }
5012
+ }, { signal: menuDismissCtrl.signal });
5013
+ function applySelectionStyles() {
5014
+ const range = selectionRange(sel);
5015
+ const allTds = tableEl.querySelectorAll("td[data-row]");
5016
+ allTds.forEach((td) => {
5017
+ const r = parseInt(td.getAttribute("data-row") || "0", 10);
5018
+ const c = parseInt(td.getAttribute("data-col") || "0", 10);
5019
+ const isAnchor = sel.anchor !== null && sel.anchor.row === r && sel.anchor.col === c;
5020
+ const inRange = range !== null && r >= range.r1 && r <= range.r2 && c >= range.c1 && c <= range.c2;
5021
+ td.style.outline = isAnchor ? "2px solid var(--fb-primary-color, #0066cc)" : "";
5022
+ td.style.outlineOffset = isAnchor ? "-2px" : "";
5023
+ if (r === 0) {
5024
+ td.style.backgroundColor = "var(--fb-background-hover-color)";
5025
+ } else {
5026
+ td.style.backgroundColor = inRange && !isAnchor ? "rgba(0,102,204,0.08)" : "";
5027
+ }
5028
+ });
5029
+ }
5030
+ function selectCell(row, col) {
5031
+ sel.anchor = { row, col };
5032
+ sel.focus = null;
5033
+ applySelectionStyles();
5034
+ tableEl.focus();
5035
+ }
5036
+ function editCell(row, col) {
5037
+ const td = tableEl.querySelector(
5038
+ `td[data-row="${row}"][data-col="${col}"]`
5039
+ );
5040
+ if (!td) return;
5041
+ const span = td.querySelector("span");
5042
+ if (!span) return;
5043
+ sel.anchor = { row, col };
5044
+ sel.focus = null;
5045
+ applySelectionStyles();
5046
+ startCellEditing(span, row, col, () => cells, persistValue, selectCell);
5047
+ }
5048
+ function addRow(afterIndex) {
5049
+ const numCols = cells.length > 0 ? cells[0].length : element.columns ?? 3;
5050
+ const newRow = Array(numCols).fill("");
5051
+ const insertAt = afterIndex !== void 0 ? afterIndex + 1 : cells.length;
5052
+ cells.splice(insertAt, 0, newRow);
5053
+ merges = merges.map((m) => {
5054
+ if (m.row >= insertAt) {
5055
+ return { ...m, row: m.row + 1 };
5056
+ }
5057
+ if (m.row < insertAt && m.row + m.rowspan > insertAt) {
5058
+ return { ...m, rowspan: m.rowspan + 1 };
5059
+ }
5060
+ return m;
5061
+ });
5062
+ persistValue();
5063
+ rebuild();
5064
+ }
5065
+ function removeRow(targetRow) {
5066
+ if (cells.length <= 1) return;
5067
+ const rowToRemove = targetRow !== void 0 ? targetRow : sel.anchor ? sel.anchor.row : cells.length - 1;
5068
+ merges = merges.map((m) => {
5069
+ const mEndRow = m.row + m.rowspan - 1;
5070
+ if (m.row === rowToRemove && m.rowspan === 1) return null;
5071
+ if (m.row === rowToRemove) {
5072
+ return { ...m, row: m.row + 1, rowspan: m.rowspan - 1 };
5073
+ }
5074
+ if (mEndRow === rowToRemove) {
5075
+ return { ...m, rowspan: m.rowspan - 1 };
5076
+ }
5077
+ if (m.row < rowToRemove && mEndRow > rowToRemove) {
5078
+ return { ...m, rowspan: m.rowspan - 1 };
5079
+ }
5080
+ if (m.row > rowToRemove) {
5081
+ return { ...m, row: m.row - 1 };
5082
+ }
5083
+ return m;
5084
+ }).filter((m) => m !== null);
5085
+ cells.splice(rowToRemove, 1);
5086
+ if (sel.anchor && sel.anchor.row >= cells.length) {
5087
+ sel.anchor = { row: cells.length - 1, col: sel.anchor.col };
5088
+ }
5089
+ persistValue();
5090
+ rebuild();
5091
+ }
5092
+ function addColumn(afterIndex) {
5093
+ const insertAt = afterIndex !== void 0 ? afterIndex + 1 : cells[0]?.length ?? 0;
5094
+ cells.forEach((row) => row.splice(insertAt, 0, ""));
5095
+ merges = merges.map((m) => {
5096
+ if (m.col >= insertAt) {
5097
+ return { ...m, col: m.col + 1 };
5098
+ }
5099
+ if (m.col < insertAt && m.col + m.colspan > insertAt) {
5100
+ return { ...m, colspan: m.colspan + 1 };
5101
+ }
5102
+ return m;
5103
+ });
5104
+ persistValue();
5105
+ rebuild();
5106
+ }
5107
+ function removeColumn(targetCol) {
5108
+ if (cells.length === 0 || cells[0].length <= 1) return;
5109
+ const colToRemove = targetCol !== void 0 ? targetCol : sel.anchor ? sel.anchor.col : cells[0].length - 1;
5110
+ merges = merges.map((m) => {
5111
+ const mEndCol = m.col + m.colspan - 1;
5112
+ if (m.col === colToRemove && m.colspan === 1) return null;
5113
+ if (m.col === colToRemove) {
5114
+ return { ...m, col: m.col + 1, colspan: m.colspan - 1 };
5115
+ }
5116
+ if (mEndCol === colToRemove) {
5117
+ return { ...m, colspan: m.colspan - 1 };
5118
+ }
5119
+ if (m.col < colToRemove && mEndCol > colToRemove) {
5120
+ return { ...m, colspan: m.colspan - 1 };
5121
+ }
5122
+ if (m.col > colToRemove) {
5123
+ return { ...m, col: m.col - 1 };
5124
+ }
5125
+ return m;
5126
+ }).filter((m) => m !== null);
5127
+ cells.forEach((row) => row.splice(colToRemove, 1));
5128
+ if (sel.anchor && sel.anchor.col >= cells[0].length) {
5129
+ sel.anchor = { row: sel.anchor.row, col: cells[0].length - 1 };
5130
+ }
5131
+ persistValue();
5132
+ rebuild();
5133
+ }
5134
+ function mergeCells() {
5135
+ const range = selectionRange(sel);
5136
+ if (!range) return;
5137
+ const { r1, c1, r2, c2 } = range;
5138
+ if (r1 === r2 && c1 === c2) return;
5139
+ merges = merges.filter((m) => {
5140
+ const mEndRow = m.row + m.rowspan - 1;
5141
+ const mEndCol = m.col + m.colspan - 1;
5142
+ const overlaps = m.row <= r2 && mEndRow >= r1 && m.col <= c2 && mEndCol >= c1;
5143
+ return !overlaps;
5144
+ });
5145
+ const anchorText = cells[r1][c1];
5146
+ for (let r = r1; r <= r2; r++) {
5147
+ for (let c = c1; c <= c2; c++) {
5148
+ if (r !== r1 || c !== c1) {
5149
+ cells[r][c] = "";
5150
+ }
5151
+ }
5152
+ }
5153
+ cells[r1][c1] = anchorText;
5154
+ merges.push({ row: r1, col: c1, rowspan: r2 - r1 + 1, colspan: c2 - c1 + 1 });
5155
+ sel.anchor = { row: r1, col: c1 };
5156
+ sel.focus = null;
5157
+ persistValue();
5158
+ rebuild();
5159
+ }
5160
+ function splitCell() {
5161
+ if (!sel.anchor) return;
5162
+ const { row, col } = sel.anchor;
5163
+ const mIdx = merges.findIndex((m) => m.row === row && m.col === col);
5164
+ if (mIdx === -1) return;
5165
+ merges.splice(mIdx, 1);
5166
+ sel.focus = null;
5167
+ persistValue();
5168
+ rebuild();
5169
+ }
5170
+ function rebuild() {
5171
+ tableEl.innerHTML = "";
5172
+ const numRows = cells.length;
5173
+ const numCols = numRows > 0 ? cells[0].length : 0;
5174
+ const range = selectionRange(sel);
5175
+ for (let rIdx = 0; rIdx < numRows; rIdx++) {
5176
+ const section = rIdx === 0 ? tableEl.tHead ?? tableEl.createTHead() : tableEl.tBodies[0] ?? tableEl.createTBody();
5177
+ const tr = section.insertRow();
5178
+ for (let cIdx = 0; cIdx < numCols; cIdx++) {
5179
+ if (getShadowingMerge(rIdx, cIdx, merges)) {
5180
+ continue;
5181
+ }
5182
+ const merge = getMergeAt(rIdx, cIdx, merges);
5183
+ const td = document.createElement("td");
5184
+ td.setAttribute("data-row", String(rIdx));
5185
+ td.setAttribute("data-col", String(cIdx));
5186
+ if (merge) {
5187
+ if (merge.rowspan > 1) td.rowSpan = merge.rowspan;
5188
+ if (merge.colspan > 1) td.colSpan = merge.colspan;
5189
+ }
5190
+ const inRange = range !== null && rIdx >= range.r1 && rIdx <= range.r2 && cIdx >= range.c1 && cIdx <= range.c2;
5191
+ const isAnchor = sel.anchor !== null && sel.anchor.row === rIdx && sel.anchor.col === cIdx;
5192
+ td.style.cssText = [
5193
+ "border: var(--fb-border-width) solid var(--fb-border-color);",
5194
+ "padding: 4px 8px;",
5195
+ "min-width: 80px;",
5196
+ "vertical-align: top;",
5197
+ "cursor: text;",
5198
+ "position: relative;",
5199
+ rIdx === 0 ? "background-color: var(--fb-background-hover-color); font-weight: 600;" : "",
5200
+ inRange && !isAnchor ? "background-color: rgba(0,102,204,0.08);" : "",
5201
+ isAnchor ? "outline: 2px solid var(--fb-primary-color, #0066cc); outline-offset: -2px;" : ""
5202
+ ].join(" ");
5203
+ const content = document.createElement("span");
5204
+ content.textContent = cells[rIdx][cIdx];
5205
+ content.style.cssText = "display: block; min-height: 1.4em; white-space: pre-wrap; word-break: break-word; outline: none;";
5206
+ td.appendChild(content);
5207
+ const capturedR = rIdx;
5208
+ const capturedC = cIdx;
5209
+ td.addEventListener("mousedown", (e) => {
5210
+ if (e.target.tagName === "BUTTON") return;
5211
+ if (e.target.contentEditable === "true") return;
5212
+ if (e.button === 2) {
5213
+ const range2 = selectionRange(sel);
5214
+ if (range2 && capturedR >= range2.r1 && capturedR <= range2.r2 && capturedC >= range2.c1 && capturedC <= range2.c2) {
5215
+ return;
5216
+ }
5217
+ }
5218
+ if (e.shiftKey && sel.anchor) {
5219
+ e.preventDefault();
5220
+ sel.focus = { row: capturedR, col: capturedC };
5221
+ sel.dragging = false;
5222
+ applySelectionStyles();
5223
+ } else {
5224
+ sel.anchor = { row: capturedR, col: capturedC };
5225
+ sel.focus = null;
5226
+ sel.dragging = true;
5227
+ applySelectionStyles();
5228
+ }
5229
+ });
5230
+ td.addEventListener("mouseup", (e) => {
5231
+ if (e.target.tagName === "BUTTON") return;
5232
+ if (sel.dragging) {
5233
+ sel.dragging = false;
5234
+ const currentRange = selectionRange(sel);
5235
+ const isSingleCell = !currentRange || currentRange.r1 === currentRange.r2 && currentRange.c1 === currentRange.c2;
5236
+ if (isSingleCell) {
5237
+ editCell(capturedR, capturedC);
5238
+ }
5239
+ }
5240
+ });
5241
+ td.addEventListener("mousemove", (e) => {
5242
+ if (sel.dragging && e.buttons === 1) {
5243
+ const currentAnchor = sel.anchor;
5244
+ if (currentAnchor && (currentAnchor.row !== capturedR || currentAnchor.col !== capturedC)) {
5245
+ sel.focus = { row: capturedR, col: capturedC };
5246
+ applySelectionStyles();
5247
+ }
5248
+ }
5249
+ });
5250
+ td.addEventListener("contextmenu", (e) => {
5251
+ const currentRange = selectionRange(sel);
5252
+ const isMulti = currentRange && (currentRange.r1 !== currentRange.r2 || currentRange.c1 !== currentRange.c2);
5253
+ const isMerged = sel.anchor && getMergeAt(sel.anchor.row, sel.anchor.col, merges);
5254
+ if (isMulti || isMerged) {
5255
+ e.preventDefault();
5256
+ showContextMenu(e.clientX, e.clientY);
5257
+ }
5258
+ });
5259
+ tr.appendChild(td);
5260
+ }
5261
+ }
5262
+ buildInsertOverlays();
5263
+ tableEl.setAttribute("tabindex", "0");
5264
+ tableEl.onkeydown = (e) => {
5265
+ const editing = tableEl.querySelector("[contenteditable='true']");
5266
+ if (editing) return;
5267
+ const anchor = sel.anchor;
5268
+ if (!anchor) return;
5269
+ const numRows2 = cells.length;
5270
+ const numCols2 = numRows2 > 0 ? cells[0].length : 0;
5271
+ const navDeltas = {
5272
+ ArrowUp: [-1, 0],
5273
+ ArrowDown: [1, 0],
5274
+ ArrowLeft: [0, -1],
5275
+ ArrowRight: [0, 1]
5276
+ };
5277
+ if (navDeltas[e.key]) {
5278
+ e.preventDefault();
5279
+ const [dr, dc] = navDeltas[e.key];
5280
+ const newRow = Math.max(0, Math.min(numRows2 - 1, anchor.row + dr));
5281
+ const newCol = Math.max(0, Math.min(numCols2 - 1, anchor.col + dc));
5282
+ if (e.shiftKey) {
5283
+ sel.focus = { row: newRow, col: newCol };
5284
+ applySelectionStyles();
5285
+ } else {
5286
+ selectCell(newRow, newCol);
5287
+ }
5288
+ return;
5289
+ }
5290
+ if (e.key === "Enter") {
5291
+ e.preventDefault();
5292
+ editCell(anchor.row, anchor.col);
5293
+ return;
5294
+ }
5295
+ if (e.key === "Tab") {
5296
+ e.preventDefault();
5297
+ const numCols3 = cells[0]?.length ?? 0;
5298
+ let nr = anchor.row;
5299
+ let nc = e.shiftKey ? anchor.col - 1 : anchor.col + 1;
5300
+ if (nc < 0) {
5301
+ nc = numCols3 - 1;
5302
+ nr = Math.max(0, nr - 1);
5303
+ }
5304
+ if (nc >= numCols3) {
5305
+ nc = 0;
5306
+ nr = Math.min(cells.length - 1, nr + 1);
5307
+ }
5308
+ selectCell(nr, nc);
5309
+ return;
5310
+ }
5311
+ if (e.key === "m" && e.ctrlKey && !e.shiftKey) {
5312
+ e.preventDefault();
5313
+ mergeCells();
5314
+ return;
5315
+ }
5316
+ if (e.key === "M" && e.ctrlKey && e.shiftKey) {
5317
+ e.preventDefault();
5318
+ splitCell();
5319
+ }
5320
+ };
5321
+ tableEl.oncopy = (e) => {
5322
+ const range2 = selectionRange(sel);
5323
+ if (!range2) return;
5324
+ e.preventDefault();
5325
+ const { r1, c1, r2, c2 } = range2;
5326
+ const tsvRows = [];
5327
+ const htmlRows = [];
5328
+ for (let r = r1; r <= r2; r++) {
5329
+ const tsvCols = [];
5330
+ const htmlCols = [];
5331
+ for (let c = c1; c <= c2; c++) {
5332
+ const val = cells[r]?.[c] ?? "";
5333
+ tsvCols.push(val);
5334
+ const escaped = val.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
5335
+ htmlCols.push(`<td>${escaped}</td>`);
5336
+ }
5337
+ tsvRows.push(tsvCols.join(" "));
5338
+ htmlRows.push(`<tr>${htmlCols.join("")}</tr>`);
5339
+ }
5340
+ const tsvText = tsvRows.join("\n");
5341
+ const htmlText = `<table>${htmlRows.join("")}</table>`;
5342
+ e.clipboardData?.setData("text/plain", tsvText);
5343
+ e.clipboardData?.setData("text/html", htmlText);
5344
+ };
5345
+ tableEl.onpaste = (e) => {
5346
+ const anchor = sel.anchor;
5347
+ if (!anchor) return;
5348
+ const text = e.clipboardData?.getData("text/plain") ?? "";
5349
+ const isMultiCell = text.includes(" ") || text.split(/\r?\n/).filter((l) => l).length > 1;
5350
+ const editing = tableEl.querySelector("[contenteditable='true']");
5351
+ if (editing && !isMultiCell) return;
5352
+ e.preventDefault();
5353
+ if (editing) {
5354
+ editing.contentEditable = "inherit";
5355
+ const r = parseInt(
5356
+ editing.closest("td")?.getAttribute("data-row") ?? "0",
5357
+ 10
5358
+ );
5359
+ const c = parseInt(
5360
+ editing.closest("td")?.getAttribute("data-col") ?? "0",
5361
+ 10
5362
+ );
5363
+ if (cells[r]) {
5364
+ cells[r][c] = editing.textContent ?? "";
5365
+ }
5366
+ }
5367
+ if (!text.trim()) return;
5368
+ const pasteRows = text.split(/\r?\n/).map((line) => line.split(" "));
5369
+ if (pasteRows.length > 1 && pasteRows[pasteRows.length - 1].length === 1 && pasteRows[pasteRows.length - 1][0] === "") {
5370
+ pasteRows.pop();
5371
+ }
5372
+ const startR = anchor.row;
5373
+ const startC = anchor.col;
5374
+ const neededRows = startR + pasteRows.length;
5375
+ while (cells.length < neededRows) {
5376
+ cells.push(Array(cells[0]?.length ?? 1).fill(""));
5377
+ }
5378
+ const maxPasteCols = Math.max(...pasteRows.map((r) => r.length));
5379
+ const neededCols = startC + maxPasteCols;
5380
+ if (cells[0] && neededCols > cells[0].length) {
5381
+ const extraCols = neededCols - cells[0].length;
5382
+ cells.forEach((row) => {
5383
+ for (let i = 0; i < extraCols; i++) row.push("");
5384
+ });
5385
+ }
5386
+ for (let pr = 0; pr < pasteRows.length; pr++) {
5387
+ for (let pc = 0; pc < pasteRows[pr].length; pc++) {
5388
+ const tr = startR + pr;
5389
+ const tc = startC + pc;
5390
+ if (cells[tr]) {
5391
+ cells[tr][tc] = pasteRows[pr][pc];
5392
+ }
5393
+ }
5394
+ }
5395
+ persistValue();
5396
+ rebuild();
5397
+ };
5398
+ }
5399
+ const insertColBtn = makeOverlayCircleBtn({
5400
+ label: "+",
5401
+ title: t("tableAddColumn", state),
5402
+ size: 20,
5403
+ color: "var(--fb-primary-color, #0066cc)",
5404
+ textColor: "white",
5405
+ onClick: () => {
5406
+ const afterIdx = parseInt(insertColBtn.dataset.afterCol ?? "0", 10);
5407
+ addColumn(afterIdx);
5408
+ }
5409
+ });
5410
+ insertColBtn.style.position = "absolute";
5411
+ insertColBtn.style.display = "none";
5412
+ tableWrapper.appendChild(insertColBtn);
5413
+ const insertRowBtn = makeOverlayCircleBtn({
5414
+ label: "+",
5415
+ title: t("tableAddRow", state),
5416
+ size: 20,
5417
+ color: "var(--fb-primary-color, #0066cc)",
5418
+ textColor: "white",
5419
+ onClick: () => {
5420
+ const afterIdx = parseInt(insertRowBtn.dataset.afterRow ?? "0", 10);
5421
+ addRow(afterIdx);
5422
+ }
5423
+ });
5424
+ insertRowBtn.style.position = "absolute";
5425
+ insertRowBtn.style.display = "none";
5426
+ tableWrapper.appendChild(insertRowBtn);
5427
+ let colRemoveBtns = [];
5428
+ let rowRemoveBtns = [];
5429
+ const addLastColBtn = makeOverlayCircleBtn({
5430
+ label: "+",
5431
+ title: t("tableAddColumn", state),
5432
+ size: 20,
5433
+ color: "var(--fb-primary-color, #0066cc)",
5434
+ textColor: "white",
5435
+ onClick: () => addColumn()
5436
+ });
5437
+ addLastColBtn.style.position = "absolute";
5438
+ addLastColBtn.style.display = "none";
5439
+ tableWrapper.appendChild(addLastColBtn);
5440
+ const addLastRowBtn = makeOverlayCircleBtn({
5441
+ label: "+",
5442
+ title: t("tableAddRow", state),
5443
+ size: 20,
5444
+ color: "var(--fb-primary-color, #0066cc)",
5445
+ textColor: "white",
5446
+ onClick: () => addRow()
5447
+ });
5448
+ addLastRowBtn.style.position = "absolute";
5449
+ addLastRowBtn.style.display = "none";
5450
+ tableWrapper.appendChild(addLastRowBtn);
5451
+ function buildInsertOverlays() {
5452
+ colRemoveBtns.forEach((b) => b.remove());
5453
+ colRemoveBtns = [];
5454
+ rowRemoveBtns.forEach((b) => b.remove());
5455
+ rowRemoveBtns = [];
5456
+ const numCols = cells.length > 0 ? cells[0].length : 0;
5457
+ const numRows = cells.length;
5458
+ if (numCols > 1) {
5459
+ const headerCells = Array.from(
5460
+ tableEl.querySelectorAll("thead td[data-col]")
5461
+ );
5462
+ for (const hc of headerCells) {
5463
+ const colIdx = parseInt(hc.getAttribute("data-col") ?? "0", 10);
5464
+ const btn = makeOverlayCircleBtn({
5465
+ label: "\xD7",
5466
+ title: t("tableRemoveColumn", state),
5467
+ size: 16,
5468
+ color: "var(--fb-error-color, #dc3545)",
5469
+ textColor: "white",
5470
+ onClick: () => removeColumn(colIdx)
5471
+ });
5472
+ btn.setAttribute("data-action", "remove-col");
5473
+ btn.setAttribute("data-col", String(colIdx));
5474
+ btn.style.position = "absolute";
5475
+ btn.style.display = "none";
5476
+ tableWrapper.appendChild(btn);
5477
+ colRemoveBtns.push(btn);
5478
+ }
5479
+ }
5480
+ if (numRows > 1) {
5481
+ const allRowElements = [
5482
+ ...tableEl.tHead ? Array.from(tableEl.tHead.rows) : [],
5483
+ ...tableEl.tBodies[0] ? Array.from(tableEl.tBodies[0].rows) : []
5484
+ ].filter((r) => r.querySelector("td[data-row]"));
5485
+ for (const rowEl of allRowElements) {
5486
+ const firstTd = rowEl.querySelector("td[data-row]");
5487
+ if (!firstTd) continue;
5488
+ const rowIdx = parseInt(firstTd.getAttribute("data-row") ?? "0", 10);
5489
+ const btn = makeOverlayCircleBtn({
5490
+ label: "\xD7",
5491
+ title: t("tableRemoveRow", state),
5492
+ size: 16,
5493
+ color: "var(--fb-error-color, #dc3545)",
5494
+ textColor: "white",
5495
+ onClick: () => removeRow(rowIdx)
5496
+ });
5497
+ btn.setAttribute("data-action", "remove-row");
5498
+ btn.setAttribute("data-row", String(rowIdx));
5499
+ btn.style.position = "absolute";
5500
+ btn.style.display = "none";
5501
+ tableWrapper.appendChild(btn);
5502
+ rowRemoveBtns.push(btn);
5503
+ }
5504
+ }
5505
+ function updateTopZoneOverlays(mx, wr, tblR, scrollL, active) {
5506
+ const headerCells = active ? Array.from(tableEl.querySelectorAll("thead td[data-col]")) : [];
5507
+ let closestColIdx = -1;
5508
+ let closestColDist = Infinity;
5509
+ let closestBorderX = -1;
5510
+ let closestAfterCol = -1;
5511
+ let closestBorderDist = Infinity;
5512
+ for (let i = 0; i < headerCells.length; i++) {
5513
+ const cellRect = headerCells[i].getBoundingClientRect();
5514
+ const centerX = (cellRect.left + cellRect.right) / 2;
5515
+ const dist = Math.abs(mx - centerX);
5516
+ if (dist < closestColDist) {
5517
+ closestColDist = dist;
5518
+ closestColIdx = i;
5519
+ }
5520
+ const borderDist = Math.abs(mx - cellRect.right);
5521
+ if (borderDist < closestBorderDist && borderDist < 20) {
5522
+ closestBorderDist = borderDist;
5523
+ closestBorderX = cellRect.right - wr.left + scrollL;
5524
+ closestAfterCol = parseInt(headerCells[i].getAttribute("data-col") ?? "0", 10);
5525
+ }
5526
+ }
5527
+ colRemoveBtns.forEach((btn, idx) => {
5528
+ if (!active || idx !== closestColIdx) {
5529
+ btn.style.display = "none";
5530
+ return;
5531
+ }
5532
+ const cellRect = headerCells[idx].getBoundingClientRect();
5533
+ const centerX = (cellRect.left + cellRect.right) / 2 - wr.left + scrollL;
5534
+ btn.style.left = `${centerX - 8}px`;
5535
+ btn.style.top = "2px";
5536
+ btn.style.display = "flex";
5537
+ });
5538
+ if (active && closestAfterCol >= 0) {
5539
+ insertColBtn.style.display = "flex";
5540
+ insertColBtn.style.left = `${closestBorderX - 10}px`;
5541
+ insertColBtn.style.top = `${tblR.top - wr.top - 10}px`;
5542
+ insertColBtn.dataset.afterCol = String(closestAfterCol);
5543
+ } else {
5544
+ insertColBtn.style.display = "none";
5545
+ }
5546
+ }
5547
+ function updateLeftZoneOverlays(my, wr, tblR, scrollL, active) {
5548
+ const allRowEls = [];
5549
+ if (active) {
5550
+ if (tableEl.tHead) {
5551
+ for (const row of Array.from(tableEl.tHead.rows)) {
5552
+ if (row.querySelector("td[data-row]")) allRowEls.push(row);
5553
+ }
5554
+ }
5555
+ if (tableEl.tBodies[0]) {
5556
+ for (const row of Array.from(tableEl.tBodies[0].rows)) {
5557
+ if (row.querySelector("td[data-row]")) allRowEls.push(row);
5558
+ }
5559
+ }
5560
+ }
5561
+ let closestRowIdx = -1;
5562
+ let closestRowDist = Infinity;
5563
+ let closestBorderY = -1;
5564
+ let closestAfterRow = -1;
5565
+ let closestRowBorderDist = Infinity;
5566
+ for (let i = 0; i < allRowEls.length; i++) {
5567
+ const trRect = allRowEls[i].getBoundingClientRect();
5568
+ const centerY = (trRect.top + trRect.bottom) / 2;
5569
+ const dist = Math.abs(my - centerY);
5570
+ if (dist < closestRowDist) {
5571
+ closestRowDist = dist;
5572
+ closestRowIdx = i;
5573
+ }
5574
+ const borderDist = Math.abs(my - trRect.bottom);
5575
+ if (borderDist < closestRowBorderDist && borderDist < 14) {
5576
+ closestRowBorderDist = borderDist;
5577
+ closestBorderY = trRect.bottom - wr.top;
5578
+ const firstTd = allRowEls[i].querySelector("td[data-row]");
5579
+ closestAfterRow = parseInt(firstTd?.getAttribute("data-row") ?? "0", 10);
5580
+ }
5581
+ }
5582
+ rowRemoveBtns.forEach((btn, idx) => {
5583
+ if (!active || idx !== closestRowIdx) {
5584
+ btn.style.display = "none";
5585
+ return;
5586
+ }
5587
+ const trRect = allRowEls[idx].getBoundingClientRect();
5588
+ const centerY = (trRect.top + trRect.bottom) / 2 - wr.top;
5589
+ btn.style.left = "4px";
5590
+ btn.style.top = `${centerY - 8}px`;
5591
+ btn.style.display = "flex";
5592
+ });
5593
+ if (active && closestAfterRow >= 0) {
5594
+ insertRowBtn.style.display = "flex";
5595
+ insertRowBtn.style.top = `${closestBorderY - 10}px`;
5596
+ insertRowBtn.style.left = `${tblR.left - wr.left + scrollL - 10}px`;
5597
+ insertRowBtn.dataset.afterRow = String(closestAfterRow);
5598
+ } else {
5599
+ insertRowBtn.style.display = "none";
5600
+ }
5601
+ }
5602
+ let rafPending = false;
5603
+ tableWrapper.onmousemove = (e) => {
5604
+ const target = e.target;
5605
+ if (target.tagName === "BUTTON" && target.parentElement === tableWrapper) return;
5606
+ if (rafPending) return;
5607
+ rafPending = true;
5608
+ const mx = e.clientX;
5609
+ const my = e.clientY;
5610
+ requestAnimationFrame(() => {
5611
+ rafPending = false;
5612
+ const wr = tableWrapper.getBoundingClientRect();
5613
+ const tblR = tableEl.getBoundingClientRect();
5614
+ const scrollL = tableWrapper.scrollLeft;
5615
+ const inTopZone = my >= wr.top && my < tblR.top + 4;
5616
+ const inLeftZone = mx >= wr.left && mx < tblR.left + 4;
5617
+ const visibleRight = Math.min(tblR.right, wr.right);
5618
+ const inRightZone = mx > visibleRight - 20 && mx <= wr.right;
5619
+ const inBottomZone = my > tblR.bottom - 4 && my <= wr.bottom + 20;
5620
+ updateTopZoneOverlays(mx, wr, tblR, scrollL, inTopZone);
5621
+ updateLeftZoneOverlays(my, wr, tblR, scrollL, inLeftZone);
5622
+ addLastColBtn.style.display = inRightZone ? "flex" : "none";
5623
+ if (inRightZone) {
5624
+ addLastColBtn.style.left = `${wr.right - wr.left + scrollL - 20}px`;
5625
+ addLastColBtn.style.top = `${(tblR.top + tblR.bottom) / 2 - wr.top - 10}px`;
5626
+ }
5627
+ addLastRowBtn.style.display = inBottomZone ? "flex" : "none";
5628
+ if (inBottomZone) {
5629
+ const visibleCenterX = (wr.left + wr.right) / 2 - wr.left + scrollL;
5630
+ addLastRowBtn.style.left = `${visibleCenterX - 10}px`;
5631
+ addLastRowBtn.style.top = `${tblR.bottom - wr.top - 10}px`;
5632
+ }
5633
+ });
5634
+ };
5635
+ tableWrapper.onmouseleave = () => {
5636
+ colRemoveBtns.forEach((btn) => {
5637
+ btn.style.display = "none";
5638
+ });
5639
+ rowRemoveBtns.forEach((btn) => {
5640
+ btn.style.display = "none";
5641
+ });
5642
+ insertColBtn.style.display = "none";
5643
+ insertRowBtn.style.display = "none";
5644
+ addLastColBtn.style.display = "none";
5645
+ addLastRowBtn.style.display = "none";
5646
+ };
5647
+ }
5648
+ rebuild();
5649
+ }
5650
+ function defaultTableData(element) {
5651
+ return {
5652
+ cells: createEmptyCells(element.rows ?? 3, element.columns ?? 3),
5653
+ merges: []
5654
+ };
5655
+ }
5656
+ function isTableData(v) {
5657
+ return v !== null && typeof v === "object" && "cells" in v && Array.isArray(v.cells);
5658
+ }
5659
+ function renderTableElement(element, ctx, wrapper, pathKey) {
5660
+ const state = ctx.state;
5661
+ const rawPrefill = ctx.prefill[element.key];
5662
+ const initialData = isTableData(rawPrefill) ? rawPrefill : isTableData(element.default) ? element.default : defaultTableData(element);
5663
+ if (state.config.readonly) {
5664
+ renderReadonlyTable(initialData, wrapper);
5665
+ } else {
5666
+ renderEditTable(element, initialData, pathKey, ctx, wrapper);
5667
+ }
5668
+ }
5669
+ function validateTableElement(element, key, context) {
5670
+ const { scopeRoot, skipValidation } = context;
5671
+ const errors = [];
5672
+ const hiddenInput = scopeRoot.querySelector(
5673
+ `[name="${key}"]`
5674
+ );
5675
+ if (!hiddenInput) {
5676
+ return { value: null, errors };
5677
+ }
5678
+ let value = null;
5679
+ try {
5680
+ value = JSON.parse(hiddenInput.value);
5681
+ } catch {
5682
+ errors.push(`${key}: invalid table data`);
5683
+ return { value: null, errors };
5684
+ }
5685
+ if (!skipValidation && element.required) {
5686
+ const hasContent = value.cells.some(
5687
+ (row) => row.some((cell) => cell.trim() !== "")
5688
+ );
5689
+ if (!hasContent) {
5690
+ errors.push(`${key}: ${t("required", context.state)}`);
5691
+ }
5692
+ }
5693
+ return { value, errors };
5694
+ }
5695
+ function updateTableField(_element, fieldPath, value, context) {
5696
+ const { scopeRoot } = context;
5697
+ const hiddenInput = scopeRoot.querySelector(
5698
+ `[name="${fieldPath}"]`
5699
+ );
5700
+ if (!hiddenInput) {
5701
+ console.warn(
5702
+ `updateTableField: no hidden input found for "${fieldPath}". Re-render to reflect new data.`
5703
+ );
5704
+ return;
5705
+ }
5706
+ if (isTableData(value) && hiddenInput._applyExternalUpdate) {
5707
+ hiddenInput._applyExternalUpdate(value);
5708
+ } else {
5709
+ hiddenInput.value = JSON.stringify(value);
5710
+ }
5711
+ }
5712
+
4256
5713
  // src/components/index.ts
4257
5714
  function showTooltip(tooltipId, button) {
4258
5715
  const tooltip = document.getElementById(tooltipId);
@@ -4558,6 +6015,13 @@ function dispatchToRenderer(element, ctx, wrapper, pathKey) {
4558
6015
  renderSelectElement(element, ctx, wrapper, pathKey);
4559
6016
  }
4560
6017
  break;
6018
+ case "switcher":
6019
+ if (isMultiple) {
6020
+ renderMultipleSwitcherElement(element, ctx, wrapper, pathKey);
6021
+ } else {
6022
+ renderSwitcherElement(element, ctx, wrapper, pathKey);
6023
+ }
6024
+ break;
4561
6025
  case "file":
4562
6026
  if (isMultiple) {
4563
6027
  renderMultipleFileElement(element, ctx, wrapper, pathKey);
@@ -4592,6 +6056,9 @@ function dispatchToRenderer(element, ctx, wrapper, pathKey) {
4592
6056
  renderSingleContainerElement(element, ctx, wrapper, pathKey);
4593
6057
  }
4594
6058
  break;
6059
+ case "table":
6060
+ renderTableElement(element, ctx, wrapper, pathKey);
6061
+ break;
4595
6062
  default: {
4596
6063
  const unsupported = document.createElement("div");
4597
6064
  unsupported.className = "text-red-500 text-sm";
@@ -4681,7 +6148,14 @@ var defaultConfig = {
4681
6148
  invalidHexColour: "Invalid hex color",
4682
6149
  minFiles: "Minimum {min} files required",
4683
6150
  maxFiles: "Maximum {max} files allowed",
4684
- unsupportedFieldType: "Unsupported field type: {type}"
6151
+ unsupportedFieldType: "Unsupported field type: {type}",
6152
+ invalidOption: "Invalid option",
6153
+ tableAddRow: "Add row",
6154
+ tableAddColumn: "Add column",
6155
+ tableRemoveRow: "Remove row",
6156
+ tableRemoveColumn: "Remove column",
6157
+ tableMergeCells: "Merge cells (Ctrl+M)",
6158
+ tableSplitCell: "Split cell (Ctrl+Shift+M)"
4685
6159
  },
4686
6160
  ru: {
4687
6161
  // UI texts
@@ -4726,7 +6200,14 @@ var defaultConfig = {
4726
6200
  invalidHexColour: "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442 \u0446\u0432\u0435\u0442\u0430",
4727
6201
  minFiles: "\u041C\u0438\u043D\u0438\u043C\u0443\u043C {min} \u0444\u0430\u0439\u043B\u043E\u0432",
4728
6202
  maxFiles: "\u041C\u0430\u043A\u0441\u0438\u043C\u0443\u043C {max} \u0444\u0430\u0439\u043B\u043E\u0432",
4729
- 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}"
6203
+ 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}",
6204
+ invalidOption: "\u041D\u0435\u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435",
6205
+ tableAddRow: "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u0441\u0442\u0440\u043E\u043A\u0443",
6206
+ tableAddColumn: "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u0441\u0442\u043E\u043B\u0431\u0435\u0446",
6207
+ tableRemoveRow: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0441\u0442\u0440\u043E\u043A\u0443",
6208
+ tableRemoveColumn: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0441\u0442\u043E\u043B\u0431\u0435\u0446",
6209
+ tableMergeCells: "\u041E\u0431\u044A\u0435\u0434\u0438\u043D\u0438\u0442\u044C \u044F\u0447\u0435\u0439\u043A\u0438 (Ctrl+M)",
6210
+ tableSplitCell: "\u0420\u0430\u0437\u0434\u0435\u043B\u0438\u0442\u044C \u044F\u0447\u0435\u0439\u043A\u0443 (Ctrl+Shift+M)"
4730
6211
  }
4731
6212
  },
4732
6213
  theme: {}
@@ -4971,6 +6452,10 @@ var componentRegistry = {
4971
6452
  validate: validateSelectElement,
4972
6453
  update: updateSelectField
4973
6454
  },
6455
+ switcher: {
6456
+ validate: validateSwitcherElement,
6457
+ update: updateSwitcherField
6458
+ },
4974
6459
  file: {
4975
6460
  validate: validateFileElement,
4976
6461
  update: updateFileField
@@ -4996,6 +6481,10 @@ var componentRegistry = {
4996
6481
  // Deprecated type - delegates to container
4997
6482
  validate: validateGroupElement,
4998
6483
  update: updateGroupField
6484
+ },
6485
+ table: {
6486
+ validate: validateTableElement,
6487
+ update: updateTableField
4999
6488
  }
5000
6489
  };
5001
6490
  function getComponentOperations(elementType) {