@dmitryvim/form-builder 0.2.18 → 0.2.20
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 +57 -0
- package/dist/browser/formbuilder.min.js +273 -76
- package/dist/browser/formbuilder.v0.2.20.min.js +583 -0
- package/dist/cjs/index.cjs +2548 -184
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/index.js +2500 -191
- package/dist/esm/index.js.map +1 -1
- package/dist/form-builder.js +273 -76
- package/dist/types/components/index.d.ts +3 -1
- package/dist/types/components/richinput.d.ts +4 -0
- package/dist/types/components/table.d.ts +4 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/types/config.d.ts +10 -0
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/types/schema.d.ts +41 -1
- package/dist/types/utils/helpers.d.ts +5 -0
- package/package.json +1 -1
- package/dist/browser/formbuilder.v0.2.18.min.js +0 -386
package/dist/esm/index.js
CHANGED
|
@@ -218,6 +218,11 @@ function pathJoin(base, key) {
|
|
|
218
218
|
function clear(node) {
|
|
219
219
|
while (node.firstChild) node.removeChild(node.firstChild);
|
|
220
220
|
}
|
|
221
|
+
function formatFileSize(bytes) {
|
|
222
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
223
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
224
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
225
|
+
}
|
|
221
226
|
|
|
222
227
|
// src/utils/enable-conditions.ts
|
|
223
228
|
function getValueByPath(data, path) {
|
|
@@ -4707,211 +4712,2481 @@ function updateGroupField(element, fieldPath, value, context) {
|
|
|
4707
4712
|
return updateContainerField(containerElement, fieldPath, value, context);
|
|
4708
4713
|
}
|
|
4709
4714
|
|
|
4710
|
-
// src/components/
|
|
4711
|
-
function
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
document.body.appendChild(tooltip);
|
|
4726
|
-
}
|
|
4727
|
-
tooltip.style.visibility = "hidden";
|
|
4728
|
-
tooltip.style.position = "fixed";
|
|
4729
|
-
tooltip.classList.remove("hidden");
|
|
4730
|
-
const tooltipRect = tooltip.getBoundingClientRect();
|
|
4731
|
-
tooltip.classList.add("hidden");
|
|
4732
|
-
tooltip.style.visibility = "visible";
|
|
4733
|
-
let left = rect.left;
|
|
4734
|
-
let top = rect.bottom + 5;
|
|
4735
|
-
if (left + tooltipRect.width > viewportWidth) {
|
|
4736
|
-
left = rect.right - tooltipRect.width;
|
|
4737
|
-
}
|
|
4738
|
-
if (top + tooltipRect.height > viewportHeight) {
|
|
4739
|
-
top = rect.top - tooltipRect.height - 5;
|
|
4740
|
-
}
|
|
4741
|
-
if (left < 10) {
|
|
4742
|
-
left = 10;
|
|
4743
|
-
}
|
|
4744
|
-
if (top < 10) {
|
|
4745
|
-
top = rect.bottom + 5;
|
|
4715
|
+
// src/components/table.ts
|
|
4716
|
+
function createEmptyCells(rows, cols) {
|
|
4717
|
+
return Array.from(
|
|
4718
|
+
{ length: rows },
|
|
4719
|
+
() => Array.from({ length: cols }, () => "")
|
|
4720
|
+
);
|
|
4721
|
+
}
|
|
4722
|
+
function getShadowingMerge(row, col, merges) {
|
|
4723
|
+
for (const m of merges) {
|
|
4724
|
+
if (m.row === row && m.col === col) {
|
|
4725
|
+
return null;
|
|
4726
|
+
}
|
|
4727
|
+
if (row >= m.row && row < m.row + m.rowspan && col >= m.col && col < m.col + m.colspan) {
|
|
4728
|
+
return m;
|
|
4729
|
+
}
|
|
4746
4730
|
}
|
|
4747
|
-
|
|
4748
|
-
tooltip.style.top = `${top}px`;
|
|
4749
|
-
tooltip.classList.remove("hidden");
|
|
4750
|
-
setTimeout(() => {
|
|
4751
|
-
tooltip.classList.add("hidden");
|
|
4752
|
-
}, 25e3);
|
|
4731
|
+
return null;
|
|
4753
4732
|
}
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4733
|
+
function getMergeAt(row, col, merges) {
|
|
4734
|
+
return merges.find((m) => m.row === row && m.col === col) ?? null;
|
|
4735
|
+
}
|
|
4736
|
+
function selectionRange(sel) {
|
|
4737
|
+
if (!sel.anchor) return null;
|
|
4738
|
+
const focus = sel.focus ?? sel.anchor;
|
|
4739
|
+
return {
|
|
4740
|
+
r1: Math.min(sel.anchor.row, focus.row),
|
|
4741
|
+
c1: Math.min(sel.anchor.col, focus.col),
|
|
4742
|
+
r2: Math.max(sel.anchor.row, focus.row),
|
|
4743
|
+
c2: Math.max(sel.anchor.col, focus.col)
|
|
4744
|
+
};
|
|
4745
|
+
}
|
|
4746
|
+
function makeOverlayCircleBtn(opts) {
|
|
4747
|
+
const btn = document.createElement("button");
|
|
4748
|
+
btn.type = "button";
|
|
4749
|
+
btn.textContent = opts.label;
|
|
4750
|
+
btn.title = opts.title;
|
|
4751
|
+
btn.style.cssText = `
|
|
4752
|
+
position: absolute;
|
|
4753
|
+
width: ${opts.size}px;
|
|
4754
|
+
height: ${opts.size}px;
|
|
4755
|
+
border-radius: 50%;
|
|
4756
|
+
border: none;
|
|
4757
|
+
background: ${opts.color};
|
|
4758
|
+
color: ${opts.textColor};
|
|
4759
|
+
font-size: ${Math.floor(opts.size * 0.6)}px;
|
|
4760
|
+
line-height: ${opts.size}px;
|
|
4761
|
+
text-align: center;
|
|
4762
|
+
cursor: pointer;
|
|
4763
|
+
z-index: 10;
|
|
4764
|
+
padding: 0;
|
|
4765
|
+
display: flex;
|
|
4766
|
+
align-items: center;
|
|
4767
|
+
justify-content: center;
|
|
4768
|
+
box-shadow: 0 1px 4px rgba(0,0,0,0.2);
|
|
4769
|
+
transition: transform 0.1s, opacity 0.1s;
|
|
4770
|
+
pointer-events: all;
|
|
4771
|
+
`;
|
|
4772
|
+
btn.addEventListener("mouseenter", () => {
|
|
4773
|
+
btn.style.transform = "scale(1.15)";
|
|
4774
|
+
});
|
|
4775
|
+
btn.addEventListener("mouseleave", () => {
|
|
4776
|
+
btn.style.transform = "scale(1)";
|
|
4777
|
+
});
|
|
4778
|
+
btn.addEventListener("click", (e) => {
|
|
4779
|
+
e.stopPropagation();
|
|
4780
|
+
opts.onClick(e);
|
|
4781
|
+
});
|
|
4782
|
+
return btn;
|
|
4783
|
+
}
|
|
4784
|
+
function renderReadonlyTable(data, wrapper) {
|
|
4785
|
+
const { cells, merges = [] } = data;
|
|
4786
|
+
if (cells.length === 0) return;
|
|
4787
|
+
const numCols = cells[0].length;
|
|
4788
|
+
const tableEl = document.createElement("table");
|
|
4789
|
+
tableEl.style.cssText = `
|
|
4790
|
+
width: 100%;
|
|
4791
|
+
border-collapse: collapse;
|
|
4792
|
+
border: var(--fb-border-width) solid var(--fb-border-color);
|
|
4793
|
+
border-radius: var(--fb-border-radius);
|
|
4794
|
+
font-size: var(--fb-font-size);
|
|
4795
|
+
font-family: var(--fb-font-family);
|
|
4796
|
+
color: var(--fb-text-color);
|
|
4797
|
+
`;
|
|
4798
|
+
cells.forEach((rowData, rIdx) => {
|
|
4799
|
+
const section = rIdx === 0 ? tableEl.createTHead() : tableEl.tBodies[0] ?? tableEl.createTBody();
|
|
4800
|
+
const tr = section.insertRow();
|
|
4801
|
+
for (let cIdx = 0; cIdx < numCols; cIdx++) {
|
|
4802
|
+
if (getShadowingMerge(rIdx, cIdx, merges)) {
|
|
4803
|
+
continue;
|
|
4804
|
+
}
|
|
4805
|
+
const merge = getMergeAt(rIdx, cIdx, merges);
|
|
4806
|
+
const td = document.createElement(rIdx === 0 ? "th" : "td");
|
|
4807
|
+
if (merge) {
|
|
4808
|
+
if (merge.rowspan > 1) td.rowSpan = merge.rowspan;
|
|
4809
|
+
if (merge.colspan > 1) td.colSpan = merge.colspan;
|
|
4810
|
+
}
|
|
4811
|
+
td.textContent = rowData[cIdx] ?? "";
|
|
4812
|
+
td.style.cssText = `
|
|
4813
|
+
padding: 6px 10px;
|
|
4814
|
+
border: var(--fb-border-width) solid var(--fb-border-color);
|
|
4815
|
+
text-align: left;
|
|
4816
|
+
vertical-align: top;
|
|
4817
|
+
${rIdx === 0 ? "background-color: var(--fb-background-hover-color); font-weight: 600;" : ""}
|
|
4818
|
+
`;
|
|
4819
|
+
tr.appendChild(td);
|
|
4763
4820
|
}
|
|
4764
4821
|
});
|
|
4822
|
+
const scrollWrapper = document.createElement("div");
|
|
4823
|
+
scrollWrapper.style.cssText = "overflow-x: auto; max-width: 100%;";
|
|
4824
|
+
scrollWrapper.appendChild(tableEl);
|
|
4825
|
+
wrapper.appendChild(scrollWrapper);
|
|
4765
4826
|
}
|
|
4766
|
-
function
|
|
4767
|
-
if (
|
|
4768
|
-
|
|
4827
|
+
function startCellEditing(span, r, c, getCells, persistValue, selectCell) {
|
|
4828
|
+
if (span.contentEditable === "true") return;
|
|
4829
|
+
span.contentEditable = "true";
|
|
4830
|
+
span.focus();
|
|
4831
|
+
const domRange = document.createRange();
|
|
4832
|
+
const winSel = window.getSelection();
|
|
4833
|
+
domRange.selectNodeContents(span);
|
|
4834
|
+
domRange.collapse(false);
|
|
4835
|
+
winSel?.removeAllRanges();
|
|
4836
|
+
winSel?.addRange(domRange);
|
|
4837
|
+
function commit() {
|
|
4838
|
+
span.contentEditable = "inherit";
|
|
4839
|
+
const cells = getCells();
|
|
4840
|
+
if (cells[r]) {
|
|
4841
|
+
cells[r][c] = span.textContent ?? "";
|
|
4842
|
+
}
|
|
4843
|
+
persistValue();
|
|
4844
|
+
}
|
|
4845
|
+
function onKeyDown(e) {
|
|
4846
|
+
const cells = getCells();
|
|
4847
|
+
const numCols = cells[0]?.length ?? 0;
|
|
4848
|
+
if (e.key === "Escape") {
|
|
4849
|
+
span.contentEditable = "inherit";
|
|
4850
|
+
span.textContent = cells[r]?.[c] ?? "";
|
|
4851
|
+
span.removeEventListener("keydown", onKeyDown);
|
|
4852
|
+
span.removeEventListener("blur", onBlur);
|
|
4853
|
+
return;
|
|
4854
|
+
}
|
|
4855
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
4856
|
+
e.preventDefault();
|
|
4857
|
+
e.stopPropagation();
|
|
4858
|
+
commit();
|
|
4859
|
+
span.removeEventListener("keydown", onKeyDown);
|
|
4860
|
+
span.removeEventListener("blur", onBlur);
|
|
4861
|
+
const nextRow = r + 1 < cells.length ? r + 1 : r;
|
|
4862
|
+
selectCell(nextRow, c);
|
|
4863
|
+
return;
|
|
4864
|
+
}
|
|
4865
|
+
if (e.key === "Tab") {
|
|
4866
|
+
e.preventDefault();
|
|
4867
|
+
e.stopPropagation();
|
|
4868
|
+
commit();
|
|
4869
|
+
span.removeEventListener("keydown", onKeyDown);
|
|
4870
|
+
span.removeEventListener("blur", onBlur);
|
|
4871
|
+
let nr = r;
|
|
4872
|
+
let nc = e.shiftKey ? c - 1 : c + 1;
|
|
4873
|
+
if (nc < 0) {
|
|
4874
|
+
nc = numCols - 1;
|
|
4875
|
+
nr = Math.max(0, r - 1);
|
|
4876
|
+
}
|
|
4877
|
+
if (nc >= numCols) {
|
|
4878
|
+
nc = 0;
|
|
4879
|
+
nr = Math.min(cells.length - 1, r + 1);
|
|
4880
|
+
}
|
|
4881
|
+
selectCell(nr, nc);
|
|
4882
|
+
}
|
|
4769
4883
|
}
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
rootFormData,
|
|
4777
|
-
containerData
|
|
4778
|
-
);
|
|
4779
|
-
return !shouldEnable;
|
|
4780
|
-
} catch (error) {
|
|
4781
|
-
console.error(
|
|
4782
|
-
`Error evaluating enableIf for field "${element.key}":`,
|
|
4783
|
-
error
|
|
4784
|
-
);
|
|
4884
|
+
function onBlur() {
|
|
4885
|
+
if (span.contentEditable === "true") {
|
|
4886
|
+
commit();
|
|
4887
|
+
span.removeEventListener("keydown", onKeyDown);
|
|
4888
|
+
span.removeEventListener("blur", onBlur);
|
|
4889
|
+
}
|
|
4785
4890
|
}
|
|
4786
|
-
|
|
4891
|
+
span.addEventListener("keydown", onKeyDown);
|
|
4892
|
+
span.addEventListener("blur", onBlur);
|
|
4787
4893
|
}
|
|
4788
|
-
function
|
|
4789
|
-
const
|
|
4790
|
-
|
|
4791
|
-
);
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4894
|
+
function renderEditTable(element, initialData, pathKey, ctx, wrapper) {
|
|
4895
|
+
const state = ctx.state;
|
|
4896
|
+
const instance = ctx.instance;
|
|
4897
|
+
const cells = initialData.cells.length > 0 ? initialData.cells.map((r) => [...r]) : createEmptyCells(element.rows ?? 3, element.columns ?? 3);
|
|
4898
|
+
let merges = initialData.merges ? [...initialData.merges] : [];
|
|
4899
|
+
const sel = { anchor: null, focus: null, dragging: false };
|
|
4900
|
+
const hiddenInput = document.createElement("input");
|
|
4901
|
+
hiddenInput.type = "hidden";
|
|
4902
|
+
hiddenInput.name = pathKey;
|
|
4903
|
+
hiddenInput.value = JSON.stringify({ cells, merges });
|
|
4904
|
+
wrapper.appendChild(hiddenInput);
|
|
4905
|
+
function persistValue() {
|
|
4906
|
+
hiddenInput.value = JSON.stringify({ cells, merges });
|
|
4907
|
+
if (instance) {
|
|
4908
|
+
instance.triggerOnChange(pathKey, { cells, merges });
|
|
4909
|
+
}
|
|
4910
|
+
}
|
|
4911
|
+
hiddenInput._applyExternalUpdate = (data) => {
|
|
4912
|
+
cells.length = 0;
|
|
4913
|
+
data.cells.forEach((row) => cells.push([...row]));
|
|
4914
|
+
merges.length = 0;
|
|
4915
|
+
if (data.merges) {
|
|
4916
|
+
data.merges.forEach((m) => merges.push({ ...m }));
|
|
4917
|
+
}
|
|
4918
|
+
sel.anchor = null;
|
|
4919
|
+
sel.focus = null;
|
|
4920
|
+
persistValue();
|
|
4921
|
+
rebuild();
|
|
4922
|
+
};
|
|
4923
|
+
const tableWrapper = document.createElement("div");
|
|
4924
|
+
tableWrapper.style.cssText = "position: relative; padding: 20px 20px 20px 24px; overflow-x: auto; max-width: 100%;";
|
|
4925
|
+
const tableEl = document.createElement("table");
|
|
4926
|
+
tableEl.style.cssText = `
|
|
4927
|
+
border-collapse: collapse;
|
|
4928
|
+
font-size: var(--fb-font-size);
|
|
4929
|
+
font-family: var(--fb-font-family);
|
|
4930
|
+
color: var(--fb-text-color);
|
|
4931
|
+
table-layout: fixed;
|
|
4932
|
+
`;
|
|
4933
|
+
tableWrapper.appendChild(tableEl);
|
|
4934
|
+
wrapper.appendChild(tableWrapper);
|
|
4935
|
+
const contextMenu = document.createElement("div");
|
|
4936
|
+
contextMenu.style.cssText = `
|
|
4937
|
+
position: fixed;
|
|
4938
|
+
display: none;
|
|
4939
|
+
background: white;
|
|
4940
|
+
border: 1px solid var(--fb-border-color);
|
|
4941
|
+
border-radius: var(--fb-border-radius);
|
|
4942
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
|
4943
|
+
padding: 4px;
|
|
4944
|
+
z-index: 1000;
|
|
4945
|
+
gap: 4px;
|
|
4946
|
+
flex-direction: column;
|
|
4947
|
+
`;
|
|
4948
|
+
wrapper.appendChild(contextMenu);
|
|
4949
|
+
function makeContextMenuBtn(label, onClick) {
|
|
4950
|
+
const btn = document.createElement("button");
|
|
4951
|
+
btn.type = "button";
|
|
4952
|
+
btn.textContent = label;
|
|
4953
|
+
btn.style.cssText = `
|
|
4954
|
+
padding: 4px 10px;
|
|
4955
|
+
font-size: var(--fb-font-size-small);
|
|
4956
|
+
color: var(--fb-text-color);
|
|
4957
|
+
border: 1px solid var(--fb-border-color);
|
|
4958
|
+
border-radius: var(--fb-border-radius);
|
|
4959
|
+
background: transparent;
|
|
4960
|
+
cursor: pointer;
|
|
4961
|
+
white-space: nowrap;
|
|
4962
|
+
text-align: left;
|
|
4963
|
+
`;
|
|
4964
|
+
btn.addEventListener("mouseenter", () => {
|
|
4965
|
+
btn.style.background = "var(--fb-background-hover-color)";
|
|
4966
|
+
});
|
|
4967
|
+
btn.addEventListener("mouseleave", () => {
|
|
4968
|
+
btn.style.background = "transparent";
|
|
4969
|
+
});
|
|
4970
|
+
btn.addEventListener("click", () => {
|
|
4971
|
+
hideContextMenu();
|
|
4972
|
+
onClick();
|
|
4973
|
+
});
|
|
4974
|
+
return btn;
|
|
4975
|
+
}
|
|
4976
|
+
function showContextMenu(x, y) {
|
|
4977
|
+
contextMenu.innerHTML = "";
|
|
4978
|
+
contextMenu.style.display = "flex";
|
|
4979
|
+
const range = selectionRange(sel);
|
|
4980
|
+
const isMultiCell = range && (range.r1 !== range.r2 || range.c1 !== range.c2);
|
|
4981
|
+
const isSingleMerged = sel.anchor && getMergeAt(sel.anchor.row, sel.anchor.col, merges);
|
|
4982
|
+
if (isMultiCell) {
|
|
4983
|
+
contextMenu.appendChild(
|
|
4984
|
+
makeContextMenuBtn(t("tableMergeCells", state), mergeCells)
|
|
4803
4985
|
);
|
|
4804
|
-
return checked ? checked.value : void 0;
|
|
4805
|
-
} else {
|
|
4806
|
-
return input.value;
|
|
4807
4986
|
}
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
return void 0;
|
|
4812
|
-
}
|
|
4813
|
-
function reevaluateEnableIf(wrapper, element, ctx) {
|
|
4814
|
-
if (!element.enableIf) {
|
|
4815
|
-
return;
|
|
4816
|
-
}
|
|
4817
|
-
const formRoot = ctx.state.formRoot;
|
|
4818
|
-
if (!formRoot) {
|
|
4819
|
-
console.error(`Cannot re-evaluate enableIf: formRoot is null`);
|
|
4820
|
-
return;
|
|
4821
|
-
}
|
|
4822
|
-
const condition = element.enableIf;
|
|
4823
|
-
const scope = condition.scope ?? "relative";
|
|
4824
|
-
let rootFormData = {};
|
|
4825
|
-
const containerData = {};
|
|
4826
|
-
const effectiveScope = !ctx.path || ctx.path === "" ? "absolute" : scope;
|
|
4827
|
-
if (effectiveScope === "relative" && ctx.path) {
|
|
4828
|
-
const containerMatch = ctx.path.match(/^(.+)\[(\d+)\]$/);
|
|
4829
|
-
if (containerMatch) {
|
|
4830
|
-
const containerKey = containerMatch[1];
|
|
4831
|
-
const containerIndex = parseInt(containerMatch[2], 10);
|
|
4832
|
-
const containerItemElement = formRoot.querySelector(
|
|
4833
|
-
`[data-container-item="${containerKey}[${containerIndex}]"]`
|
|
4987
|
+
if (isSingleMerged) {
|
|
4988
|
+
contextMenu.appendChild(
|
|
4989
|
+
makeContextMenuBtn(t("tableSplitCell", state), splitCell)
|
|
4834
4990
|
);
|
|
4835
|
-
if (containerItemElement) {
|
|
4836
|
-
const inputs = containerItemElement.querySelectorAll(
|
|
4837
|
-
"input, select, textarea"
|
|
4838
|
-
);
|
|
4839
|
-
inputs.forEach((input) => {
|
|
4840
|
-
const fieldName = input.getAttribute("name");
|
|
4841
|
-
if (fieldName) {
|
|
4842
|
-
const fieldKeyMatch = fieldName.match(/\.([^.[\]]+)$/);
|
|
4843
|
-
if (fieldKeyMatch) {
|
|
4844
|
-
const fieldKey = fieldKeyMatch[1];
|
|
4845
|
-
if (input instanceof HTMLSelectElement) {
|
|
4846
|
-
containerData[fieldKey] = input.value;
|
|
4847
|
-
} else if (input instanceof HTMLInputElement) {
|
|
4848
|
-
if (input.type === "checkbox") {
|
|
4849
|
-
containerData[fieldKey] = input.checked;
|
|
4850
|
-
} else if (input.type === "radio") {
|
|
4851
|
-
if (input.checked) {
|
|
4852
|
-
containerData[fieldKey] = input.value;
|
|
4853
|
-
}
|
|
4854
|
-
} else {
|
|
4855
|
-
containerData[fieldKey] = input.value;
|
|
4856
|
-
}
|
|
4857
|
-
} else if (input instanceof HTMLTextAreaElement) {
|
|
4858
|
-
containerData[fieldKey] = input.value;
|
|
4859
|
-
}
|
|
4860
|
-
}
|
|
4861
|
-
}
|
|
4862
|
-
});
|
|
4863
|
-
}
|
|
4864
4991
|
}
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
if (dependencyValue !== void 0) {
|
|
4869
|
-
rootFormData[dependencyKey] = dependencyValue;
|
|
4870
|
-
} else {
|
|
4871
|
-
rootFormData = ctx.formData ?? ctx.prefill;
|
|
4992
|
+
if (!contextMenu.firstChild) {
|
|
4993
|
+
hideContextMenu();
|
|
4994
|
+
return;
|
|
4872
4995
|
}
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
const
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4996
|
+
const menuWidth = 140;
|
|
4997
|
+
const menuHeight = contextMenu.children.length * 32 + 8;
|
|
4998
|
+
const vw = window.innerWidth;
|
|
4999
|
+
const vh = window.innerHeight;
|
|
5000
|
+
const left = x + menuWidth > vw ? x - menuWidth : x;
|
|
5001
|
+
const top = y + menuHeight > vh ? y - menuHeight : y;
|
|
5002
|
+
contextMenu.style.left = `${left}px`;
|
|
5003
|
+
contextMenu.style.top = `${top}px`;
|
|
5004
|
+
}
|
|
5005
|
+
function hideContextMenu() {
|
|
5006
|
+
contextMenu.style.display = "none";
|
|
5007
|
+
}
|
|
5008
|
+
const menuDismissCtrl = new AbortController();
|
|
5009
|
+
document.addEventListener("mousedown", (e) => {
|
|
5010
|
+
if (!wrapper.isConnected) {
|
|
5011
|
+
menuDismissCtrl.abort();
|
|
5012
|
+
return;
|
|
4888
5013
|
}
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
}
|
|
4893
|
-
function
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
5014
|
+
if (!contextMenu.contains(e.target)) {
|
|
5015
|
+
hideContextMenu();
|
|
5016
|
+
}
|
|
5017
|
+
}, { signal: menuDismissCtrl.signal });
|
|
5018
|
+
function applySelectionStyles() {
|
|
5019
|
+
const range = selectionRange(sel);
|
|
5020
|
+
const allTds = tableEl.querySelectorAll("td[data-row]");
|
|
5021
|
+
allTds.forEach((td) => {
|
|
5022
|
+
const r = parseInt(td.getAttribute("data-row") || "0", 10);
|
|
5023
|
+
const c = parseInt(td.getAttribute("data-col") || "0", 10);
|
|
5024
|
+
const isAnchor = sel.anchor !== null && sel.anchor.row === r && sel.anchor.col === c;
|
|
5025
|
+
const inRange = range !== null && r >= range.r1 && r <= range.r2 && c >= range.c1 && c <= range.c2;
|
|
5026
|
+
td.style.outline = isAnchor ? "2px solid var(--fb-primary-color, #0066cc)" : "";
|
|
5027
|
+
td.style.outlineOffset = isAnchor ? "-2px" : "";
|
|
5028
|
+
if (r === 0) {
|
|
5029
|
+
td.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
5030
|
+
} else {
|
|
5031
|
+
td.style.backgroundColor = inRange && !isAnchor ? "rgba(0,102,204,0.08)" : "";
|
|
5032
|
+
}
|
|
5033
|
+
});
|
|
4901
5034
|
}
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
dependencyFieldPath = `${ctx.path}.${dependencyKey}`;
|
|
4908
|
-
} else {
|
|
4909
|
-
dependencyFieldPath = dependencyKey;
|
|
5035
|
+
function selectCell(row, col) {
|
|
5036
|
+
sel.anchor = { row, col };
|
|
5037
|
+
sel.focus = null;
|
|
5038
|
+
applySelectionStyles();
|
|
5039
|
+
tableEl.focus();
|
|
4910
5040
|
}
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
5041
|
+
function editCell(row, col) {
|
|
5042
|
+
const td = tableEl.querySelector(
|
|
5043
|
+
`td[data-row="${row}"][data-col="${col}"]`
|
|
5044
|
+
);
|
|
5045
|
+
if (!td) return;
|
|
5046
|
+
const span = td.querySelector("span");
|
|
5047
|
+
if (!span) return;
|
|
5048
|
+
sel.anchor = { row, col };
|
|
5049
|
+
sel.focus = null;
|
|
5050
|
+
applySelectionStyles();
|
|
5051
|
+
startCellEditing(span, row, col, () => cells, persistValue, selectCell);
|
|
5052
|
+
}
|
|
5053
|
+
function addRow(afterIndex) {
|
|
5054
|
+
const numCols = cells.length > 0 ? cells[0].length : element.columns ?? 3;
|
|
5055
|
+
const newRow = Array(numCols).fill("");
|
|
5056
|
+
const insertAt = afterIndex !== void 0 ? afterIndex + 1 : cells.length;
|
|
5057
|
+
cells.splice(insertAt, 0, newRow);
|
|
5058
|
+
merges = merges.map((m) => {
|
|
5059
|
+
if (m.row >= insertAt) {
|
|
5060
|
+
return { ...m, row: m.row + 1 };
|
|
5061
|
+
}
|
|
5062
|
+
if (m.row < insertAt && m.row + m.rowspan > insertAt) {
|
|
5063
|
+
return { ...m, rowspan: m.rowspan + 1 };
|
|
5064
|
+
}
|
|
5065
|
+
return m;
|
|
5066
|
+
});
|
|
5067
|
+
persistValue();
|
|
5068
|
+
rebuild();
|
|
5069
|
+
}
|
|
5070
|
+
function removeRow(targetRow) {
|
|
5071
|
+
if (cells.length <= 1) return;
|
|
5072
|
+
const rowToRemove = targetRow !== void 0 ? targetRow : sel.anchor ? sel.anchor.row : cells.length - 1;
|
|
5073
|
+
merges = merges.map((m) => {
|
|
5074
|
+
const mEndRow = m.row + m.rowspan - 1;
|
|
5075
|
+
if (m.row === rowToRemove && m.rowspan === 1) return null;
|
|
5076
|
+
if (m.row === rowToRemove) {
|
|
5077
|
+
return { ...m, row: m.row + 1, rowspan: m.rowspan - 1 };
|
|
5078
|
+
}
|
|
5079
|
+
if (mEndRow === rowToRemove) {
|
|
5080
|
+
return { ...m, rowspan: m.rowspan - 1 };
|
|
5081
|
+
}
|
|
5082
|
+
if (m.row < rowToRemove && mEndRow > rowToRemove) {
|
|
5083
|
+
return { ...m, rowspan: m.rowspan - 1 };
|
|
5084
|
+
}
|
|
5085
|
+
if (m.row > rowToRemove) {
|
|
5086
|
+
return { ...m, row: m.row - 1 };
|
|
5087
|
+
}
|
|
5088
|
+
return m;
|
|
5089
|
+
}).filter((m) => m !== null);
|
|
5090
|
+
cells.splice(rowToRemove, 1);
|
|
5091
|
+
if (sel.anchor && sel.anchor.row >= cells.length) {
|
|
5092
|
+
sel.anchor = { row: cells.length - 1, col: sel.anchor.col };
|
|
5093
|
+
}
|
|
5094
|
+
persistValue();
|
|
5095
|
+
rebuild();
|
|
5096
|
+
}
|
|
5097
|
+
function addColumn(afterIndex) {
|
|
5098
|
+
const insertAt = afterIndex !== void 0 ? afterIndex + 1 : cells[0]?.length ?? 0;
|
|
5099
|
+
cells.forEach((row) => row.splice(insertAt, 0, ""));
|
|
5100
|
+
merges = merges.map((m) => {
|
|
5101
|
+
if (m.col >= insertAt) {
|
|
5102
|
+
return { ...m, col: m.col + 1 };
|
|
5103
|
+
}
|
|
5104
|
+
if (m.col < insertAt && m.col + m.colspan > insertAt) {
|
|
5105
|
+
return { ...m, colspan: m.colspan + 1 };
|
|
5106
|
+
}
|
|
5107
|
+
return m;
|
|
5108
|
+
});
|
|
5109
|
+
persistValue();
|
|
5110
|
+
rebuild();
|
|
5111
|
+
}
|
|
5112
|
+
function removeColumn(targetCol) {
|
|
5113
|
+
if (cells.length === 0 || cells[0].length <= 1) return;
|
|
5114
|
+
const colToRemove = targetCol !== void 0 ? targetCol : sel.anchor ? sel.anchor.col : cells[0].length - 1;
|
|
5115
|
+
merges = merges.map((m) => {
|
|
5116
|
+
const mEndCol = m.col + m.colspan - 1;
|
|
5117
|
+
if (m.col === colToRemove && m.colspan === 1) return null;
|
|
5118
|
+
if (m.col === colToRemove) {
|
|
5119
|
+
return { ...m, col: m.col + 1, colspan: m.colspan - 1 };
|
|
5120
|
+
}
|
|
5121
|
+
if (mEndCol === colToRemove) {
|
|
5122
|
+
return { ...m, colspan: m.colspan - 1 };
|
|
5123
|
+
}
|
|
5124
|
+
if (m.col < colToRemove && mEndCol > colToRemove) {
|
|
5125
|
+
return { ...m, colspan: m.colspan - 1 };
|
|
5126
|
+
}
|
|
5127
|
+
if (m.col > colToRemove) {
|
|
5128
|
+
return { ...m, col: m.col - 1 };
|
|
5129
|
+
}
|
|
5130
|
+
return m;
|
|
5131
|
+
}).filter((m) => m !== null);
|
|
5132
|
+
cells.forEach((row) => row.splice(colToRemove, 1));
|
|
5133
|
+
if (sel.anchor && sel.anchor.col >= cells[0].length) {
|
|
5134
|
+
sel.anchor = { row: sel.anchor.row, col: cells[0].length - 1 };
|
|
5135
|
+
}
|
|
5136
|
+
persistValue();
|
|
5137
|
+
rebuild();
|
|
5138
|
+
}
|
|
5139
|
+
function mergeCells() {
|
|
5140
|
+
const range = selectionRange(sel);
|
|
5141
|
+
if (!range) return;
|
|
5142
|
+
const { r1, c1, r2, c2 } = range;
|
|
5143
|
+
if (r1 === r2 && c1 === c2) return;
|
|
5144
|
+
merges = merges.filter((m) => {
|
|
5145
|
+
const mEndRow = m.row + m.rowspan - 1;
|
|
5146
|
+
const mEndCol = m.col + m.colspan - 1;
|
|
5147
|
+
const overlaps = m.row <= r2 && mEndRow >= r1 && m.col <= c2 && mEndCol >= c1;
|
|
5148
|
+
return !overlaps;
|
|
5149
|
+
});
|
|
5150
|
+
const anchorText = cells[r1][c1];
|
|
5151
|
+
for (let r = r1; r <= r2; r++) {
|
|
5152
|
+
for (let c = c1; c <= c2; c++) {
|
|
5153
|
+
if (r !== r1 || c !== c1) {
|
|
5154
|
+
cells[r][c] = "";
|
|
5155
|
+
}
|
|
5156
|
+
}
|
|
5157
|
+
}
|
|
5158
|
+
cells[r1][c1] = anchorText;
|
|
5159
|
+
merges.push({ row: r1, col: c1, rowspan: r2 - r1 + 1, colspan: c2 - c1 + 1 });
|
|
5160
|
+
sel.anchor = { row: r1, col: c1 };
|
|
5161
|
+
sel.focus = null;
|
|
5162
|
+
persistValue();
|
|
5163
|
+
rebuild();
|
|
5164
|
+
}
|
|
5165
|
+
function splitCell() {
|
|
5166
|
+
if (!sel.anchor) return;
|
|
5167
|
+
const { row, col } = sel.anchor;
|
|
5168
|
+
const mIdx = merges.findIndex((m) => m.row === row && m.col === col);
|
|
5169
|
+
if (mIdx === -1) return;
|
|
5170
|
+
merges.splice(mIdx, 1);
|
|
5171
|
+
sel.focus = null;
|
|
5172
|
+
persistValue();
|
|
5173
|
+
rebuild();
|
|
5174
|
+
}
|
|
5175
|
+
function rebuild() {
|
|
5176
|
+
tableEl.innerHTML = "";
|
|
5177
|
+
const numRows = cells.length;
|
|
5178
|
+
const numCols = numRows > 0 ? cells[0].length : 0;
|
|
5179
|
+
const range = selectionRange(sel);
|
|
5180
|
+
for (let rIdx = 0; rIdx < numRows; rIdx++) {
|
|
5181
|
+
const section = rIdx === 0 ? tableEl.tHead ?? tableEl.createTHead() : tableEl.tBodies[0] ?? tableEl.createTBody();
|
|
5182
|
+
const tr = section.insertRow();
|
|
5183
|
+
for (let cIdx = 0; cIdx < numCols; cIdx++) {
|
|
5184
|
+
if (getShadowingMerge(rIdx, cIdx, merges)) {
|
|
5185
|
+
continue;
|
|
5186
|
+
}
|
|
5187
|
+
const merge = getMergeAt(rIdx, cIdx, merges);
|
|
5188
|
+
const td = document.createElement("td");
|
|
5189
|
+
td.setAttribute("data-row", String(rIdx));
|
|
5190
|
+
td.setAttribute("data-col", String(cIdx));
|
|
5191
|
+
if (merge) {
|
|
5192
|
+
if (merge.rowspan > 1) td.rowSpan = merge.rowspan;
|
|
5193
|
+
if (merge.colspan > 1) td.colSpan = merge.colspan;
|
|
5194
|
+
}
|
|
5195
|
+
const inRange = range !== null && rIdx >= range.r1 && rIdx <= range.r2 && cIdx >= range.c1 && cIdx <= range.c2;
|
|
5196
|
+
const isAnchor = sel.anchor !== null && sel.anchor.row === rIdx && sel.anchor.col === cIdx;
|
|
5197
|
+
td.style.cssText = [
|
|
5198
|
+
"border: var(--fb-border-width) solid var(--fb-border-color);",
|
|
5199
|
+
"padding: 4px 8px;",
|
|
5200
|
+
"min-width: 80px;",
|
|
5201
|
+
"vertical-align: top;",
|
|
5202
|
+
"cursor: text;",
|
|
5203
|
+
"position: relative;",
|
|
5204
|
+
rIdx === 0 ? "background-color: var(--fb-background-hover-color); font-weight: 600;" : "",
|
|
5205
|
+
inRange && !isAnchor ? "background-color: rgba(0,102,204,0.08);" : "",
|
|
5206
|
+
isAnchor ? "outline: 2px solid var(--fb-primary-color, #0066cc); outline-offset: -2px;" : ""
|
|
5207
|
+
].join(" ");
|
|
5208
|
+
const content = document.createElement("span");
|
|
5209
|
+
content.textContent = cells[rIdx][cIdx];
|
|
5210
|
+
content.style.cssText = "display: block; min-height: 1.4em; white-space: pre-wrap; word-break: break-word; outline: none;";
|
|
5211
|
+
td.appendChild(content);
|
|
5212
|
+
const capturedR = rIdx;
|
|
5213
|
+
const capturedC = cIdx;
|
|
5214
|
+
td.addEventListener("mousedown", (e) => {
|
|
5215
|
+
if (e.target.tagName === "BUTTON") return;
|
|
5216
|
+
if (e.target.contentEditable === "true") return;
|
|
5217
|
+
if (e.button === 2) {
|
|
5218
|
+
const range2 = selectionRange(sel);
|
|
5219
|
+
if (range2 && capturedR >= range2.r1 && capturedR <= range2.r2 && capturedC >= range2.c1 && capturedC <= range2.c2) {
|
|
5220
|
+
return;
|
|
5221
|
+
}
|
|
5222
|
+
}
|
|
5223
|
+
if (e.shiftKey && sel.anchor) {
|
|
5224
|
+
e.preventDefault();
|
|
5225
|
+
sel.focus = { row: capturedR, col: capturedC };
|
|
5226
|
+
sel.dragging = false;
|
|
5227
|
+
applySelectionStyles();
|
|
5228
|
+
} else {
|
|
5229
|
+
sel.anchor = { row: capturedR, col: capturedC };
|
|
5230
|
+
sel.focus = null;
|
|
5231
|
+
sel.dragging = true;
|
|
5232
|
+
applySelectionStyles();
|
|
5233
|
+
}
|
|
5234
|
+
});
|
|
5235
|
+
td.addEventListener("mouseup", (e) => {
|
|
5236
|
+
if (e.target.tagName === "BUTTON") return;
|
|
5237
|
+
if (sel.dragging) {
|
|
5238
|
+
sel.dragging = false;
|
|
5239
|
+
const currentRange = selectionRange(sel);
|
|
5240
|
+
const isSingleCell = !currentRange || currentRange.r1 === currentRange.r2 && currentRange.c1 === currentRange.c2;
|
|
5241
|
+
if (isSingleCell) {
|
|
5242
|
+
editCell(capturedR, capturedC);
|
|
5243
|
+
}
|
|
5244
|
+
}
|
|
5245
|
+
});
|
|
5246
|
+
td.addEventListener("mousemove", (e) => {
|
|
5247
|
+
if (sel.dragging && e.buttons === 1) {
|
|
5248
|
+
const currentAnchor = sel.anchor;
|
|
5249
|
+
if (currentAnchor && (currentAnchor.row !== capturedR || currentAnchor.col !== capturedC)) {
|
|
5250
|
+
sel.focus = { row: capturedR, col: capturedC };
|
|
5251
|
+
applySelectionStyles();
|
|
5252
|
+
}
|
|
5253
|
+
}
|
|
5254
|
+
});
|
|
5255
|
+
td.addEventListener("contextmenu", (e) => {
|
|
5256
|
+
const currentRange = selectionRange(sel);
|
|
5257
|
+
const isMulti = currentRange && (currentRange.r1 !== currentRange.r2 || currentRange.c1 !== currentRange.c2);
|
|
5258
|
+
const isMerged = sel.anchor && getMergeAt(sel.anchor.row, sel.anchor.col, merges);
|
|
5259
|
+
if (isMulti || isMerged) {
|
|
5260
|
+
e.preventDefault();
|
|
5261
|
+
showContextMenu(e.clientX, e.clientY);
|
|
5262
|
+
}
|
|
5263
|
+
});
|
|
5264
|
+
tr.appendChild(td);
|
|
5265
|
+
}
|
|
5266
|
+
}
|
|
5267
|
+
buildInsertOverlays();
|
|
5268
|
+
tableEl.setAttribute("tabindex", "0");
|
|
5269
|
+
tableEl.onkeydown = (e) => {
|
|
5270
|
+
const editing = tableEl.querySelector("[contenteditable='true']");
|
|
5271
|
+
if (editing) return;
|
|
5272
|
+
const anchor = sel.anchor;
|
|
5273
|
+
if (!anchor) return;
|
|
5274
|
+
const numRows2 = cells.length;
|
|
5275
|
+
const numCols2 = numRows2 > 0 ? cells[0].length : 0;
|
|
5276
|
+
const navDeltas = {
|
|
5277
|
+
ArrowUp: [-1, 0],
|
|
5278
|
+
ArrowDown: [1, 0],
|
|
5279
|
+
ArrowLeft: [0, -1],
|
|
5280
|
+
ArrowRight: [0, 1]
|
|
5281
|
+
};
|
|
5282
|
+
if (navDeltas[e.key]) {
|
|
5283
|
+
e.preventDefault();
|
|
5284
|
+
const [dr, dc] = navDeltas[e.key];
|
|
5285
|
+
const newRow = Math.max(0, Math.min(numRows2 - 1, anchor.row + dr));
|
|
5286
|
+
const newCol = Math.max(0, Math.min(numCols2 - 1, anchor.col + dc));
|
|
5287
|
+
if (e.shiftKey) {
|
|
5288
|
+
sel.focus = { row: newRow, col: newCol };
|
|
5289
|
+
applySelectionStyles();
|
|
5290
|
+
} else {
|
|
5291
|
+
selectCell(newRow, newCol);
|
|
5292
|
+
}
|
|
5293
|
+
return;
|
|
5294
|
+
}
|
|
5295
|
+
if (e.key === "Enter") {
|
|
5296
|
+
e.preventDefault();
|
|
5297
|
+
editCell(anchor.row, anchor.col);
|
|
5298
|
+
return;
|
|
5299
|
+
}
|
|
5300
|
+
if (e.key === "Tab") {
|
|
5301
|
+
e.preventDefault();
|
|
5302
|
+
const numCols3 = cells[0]?.length ?? 0;
|
|
5303
|
+
let nr = anchor.row;
|
|
5304
|
+
let nc = e.shiftKey ? anchor.col - 1 : anchor.col + 1;
|
|
5305
|
+
if (nc < 0) {
|
|
5306
|
+
nc = numCols3 - 1;
|
|
5307
|
+
nr = Math.max(0, nr - 1);
|
|
5308
|
+
}
|
|
5309
|
+
if (nc >= numCols3) {
|
|
5310
|
+
nc = 0;
|
|
5311
|
+
nr = Math.min(cells.length - 1, nr + 1);
|
|
5312
|
+
}
|
|
5313
|
+
selectCell(nr, nc);
|
|
5314
|
+
return;
|
|
5315
|
+
}
|
|
5316
|
+
if (e.key === "m" && e.ctrlKey && !e.shiftKey) {
|
|
5317
|
+
e.preventDefault();
|
|
5318
|
+
mergeCells();
|
|
5319
|
+
return;
|
|
5320
|
+
}
|
|
5321
|
+
if (e.key === "M" && e.ctrlKey && e.shiftKey) {
|
|
5322
|
+
e.preventDefault();
|
|
5323
|
+
splitCell();
|
|
5324
|
+
}
|
|
5325
|
+
};
|
|
5326
|
+
tableEl.oncopy = (e) => {
|
|
5327
|
+
const range2 = selectionRange(sel);
|
|
5328
|
+
if (!range2) return;
|
|
5329
|
+
e.preventDefault();
|
|
5330
|
+
const { r1, c1, r2, c2 } = range2;
|
|
5331
|
+
const tsvRows = [];
|
|
5332
|
+
const htmlRows = [];
|
|
5333
|
+
for (let r = r1; r <= r2; r++) {
|
|
5334
|
+
const tsvCols = [];
|
|
5335
|
+
const htmlCols = [];
|
|
5336
|
+
for (let c = c1; c <= c2; c++) {
|
|
5337
|
+
const val = cells[r]?.[c] ?? "";
|
|
5338
|
+
tsvCols.push(val);
|
|
5339
|
+
const escaped = val.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
5340
|
+
htmlCols.push(`<td>${escaped}</td>`);
|
|
5341
|
+
}
|
|
5342
|
+
tsvRows.push(tsvCols.join(" "));
|
|
5343
|
+
htmlRows.push(`<tr>${htmlCols.join("")}</tr>`);
|
|
5344
|
+
}
|
|
5345
|
+
const tsvText = tsvRows.join("\n");
|
|
5346
|
+
const htmlText = `<table>${htmlRows.join("")}</table>`;
|
|
5347
|
+
e.clipboardData?.setData("text/plain", tsvText);
|
|
5348
|
+
e.clipboardData?.setData("text/html", htmlText);
|
|
5349
|
+
};
|
|
5350
|
+
tableEl.onpaste = (e) => {
|
|
5351
|
+
const anchor = sel.anchor;
|
|
5352
|
+
if (!anchor) return;
|
|
5353
|
+
const text = e.clipboardData?.getData("text/plain") ?? "";
|
|
5354
|
+
const isMultiCell = text.includes(" ") || text.split(/\r?\n/).filter((l) => l).length > 1;
|
|
5355
|
+
const editing = tableEl.querySelector("[contenteditable='true']");
|
|
5356
|
+
if (editing && !isMultiCell) return;
|
|
5357
|
+
e.preventDefault();
|
|
5358
|
+
if (editing) {
|
|
5359
|
+
editing.contentEditable = "inherit";
|
|
5360
|
+
const r = parseInt(
|
|
5361
|
+
editing.closest("td")?.getAttribute("data-row") ?? "0",
|
|
5362
|
+
10
|
|
5363
|
+
);
|
|
5364
|
+
const c = parseInt(
|
|
5365
|
+
editing.closest("td")?.getAttribute("data-col") ?? "0",
|
|
5366
|
+
10
|
|
5367
|
+
);
|
|
5368
|
+
if (cells[r]) {
|
|
5369
|
+
cells[r][c] = editing.textContent ?? "";
|
|
5370
|
+
}
|
|
5371
|
+
}
|
|
5372
|
+
if (!text.trim()) return;
|
|
5373
|
+
const pasteRows = text.split(/\r?\n/).map((line) => line.split(" "));
|
|
5374
|
+
if (pasteRows.length > 1 && pasteRows[pasteRows.length - 1].length === 1 && pasteRows[pasteRows.length - 1][0] === "") {
|
|
5375
|
+
pasteRows.pop();
|
|
5376
|
+
}
|
|
5377
|
+
const startR = anchor.row;
|
|
5378
|
+
const startC = anchor.col;
|
|
5379
|
+
const neededRows = startR + pasteRows.length;
|
|
5380
|
+
while (cells.length < neededRows) {
|
|
5381
|
+
cells.push(Array(cells[0]?.length ?? 1).fill(""));
|
|
5382
|
+
}
|
|
5383
|
+
const maxPasteCols = Math.max(...pasteRows.map((r) => r.length));
|
|
5384
|
+
const neededCols = startC + maxPasteCols;
|
|
5385
|
+
if (cells[0] && neededCols > cells[0].length) {
|
|
5386
|
+
const extraCols = neededCols - cells[0].length;
|
|
5387
|
+
cells.forEach((row) => {
|
|
5388
|
+
for (let i = 0; i < extraCols; i++) row.push("");
|
|
5389
|
+
});
|
|
5390
|
+
}
|
|
5391
|
+
for (let pr = 0; pr < pasteRows.length; pr++) {
|
|
5392
|
+
for (let pc = 0; pc < pasteRows[pr].length; pc++) {
|
|
5393
|
+
const tr = startR + pr;
|
|
5394
|
+
const tc = startC + pc;
|
|
5395
|
+
if (cells[tr]) {
|
|
5396
|
+
cells[tr][tc] = pasteRows[pr][pc];
|
|
5397
|
+
}
|
|
5398
|
+
}
|
|
5399
|
+
}
|
|
5400
|
+
persistValue();
|
|
5401
|
+
rebuild();
|
|
5402
|
+
};
|
|
5403
|
+
}
|
|
5404
|
+
const insertColBtn = makeOverlayCircleBtn({
|
|
5405
|
+
label: "+",
|
|
5406
|
+
title: t("tableAddColumn", state),
|
|
5407
|
+
size: 20,
|
|
5408
|
+
color: "var(--fb-primary-color, #0066cc)",
|
|
5409
|
+
textColor: "white",
|
|
5410
|
+
onClick: () => {
|
|
5411
|
+
const afterIdx = parseInt(insertColBtn.dataset.afterCol ?? "0", 10);
|
|
5412
|
+
addColumn(afterIdx);
|
|
5413
|
+
}
|
|
5414
|
+
});
|
|
5415
|
+
insertColBtn.style.position = "absolute";
|
|
5416
|
+
insertColBtn.style.display = "none";
|
|
5417
|
+
tableWrapper.appendChild(insertColBtn);
|
|
5418
|
+
const insertRowBtn = makeOverlayCircleBtn({
|
|
5419
|
+
label: "+",
|
|
5420
|
+
title: t("tableAddRow", state),
|
|
5421
|
+
size: 20,
|
|
5422
|
+
color: "var(--fb-primary-color, #0066cc)",
|
|
5423
|
+
textColor: "white",
|
|
5424
|
+
onClick: () => {
|
|
5425
|
+
const afterIdx = parseInt(insertRowBtn.dataset.afterRow ?? "0", 10);
|
|
5426
|
+
addRow(afterIdx);
|
|
5427
|
+
}
|
|
5428
|
+
});
|
|
5429
|
+
insertRowBtn.style.position = "absolute";
|
|
5430
|
+
insertRowBtn.style.display = "none";
|
|
5431
|
+
tableWrapper.appendChild(insertRowBtn);
|
|
5432
|
+
let colRemoveBtns = [];
|
|
5433
|
+
let rowRemoveBtns = [];
|
|
5434
|
+
const addLastColBtn = makeOverlayCircleBtn({
|
|
5435
|
+
label: "+",
|
|
5436
|
+
title: t("tableAddColumn", state),
|
|
5437
|
+
size: 20,
|
|
5438
|
+
color: "var(--fb-primary-color, #0066cc)",
|
|
5439
|
+
textColor: "white",
|
|
5440
|
+
onClick: () => addColumn()
|
|
5441
|
+
});
|
|
5442
|
+
addLastColBtn.style.position = "absolute";
|
|
5443
|
+
addLastColBtn.style.display = "none";
|
|
5444
|
+
tableWrapper.appendChild(addLastColBtn);
|
|
5445
|
+
const addLastRowBtn = makeOverlayCircleBtn({
|
|
5446
|
+
label: "+",
|
|
5447
|
+
title: t("tableAddRow", state),
|
|
5448
|
+
size: 20,
|
|
5449
|
+
color: "var(--fb-primary-color, #0066cc)",
|
|
5450
|
+
textColor: "white",
|
|
5451
|
+
onClick: () => addRow()
|
|
5452
|
+
});
|
|
5453
|
+
addLastRowBtn.style.position = "absolute";
|
|
5454
|
+
addLastRowBtn.style.display = "none";
|
|
5455
|
+
tableWrapper.appendChild(addLastRowBtn);
|
|
5456
|
+
function buildInsertOverlays() {
|
|
5457
|
+
colRemoveBtns.forEach((b) => b.remove());
|
|
5458
|
+
colRemoveBtns = [];
|
|
5459
|
+
rowRemoveBtns.forEach((b) => b.remove());
|
|
5460
|
+
rowRemoveBtns = [];
|
|
5461
|
+
const numCols = cells.length > 0 ? cells[0].length : 0;
|
|
5462
|
+
const numRows = cells.length;
|
|
5463
|
+
if (numCols > 1) {
|
|
5464
|
+
const headerCells = Array.from(
|
|
5465
|
+
tableEl.querySelectorAll("thead td[data-col]")
|
|
5466
|
+
);
|
|
5467
|
+
for (const hc of headerCells) {
|
|
5468
|
+
const colIdx = parseInt(hc.getAttribute("data-col") ?? "0", 10);
|
|
5469
|
+
const btn = makeOverlayCircleBtn({
|
|
5470
|
+
label: "\xD7",
|
|
5471
|
+
title: t("tableRemoveColumn", state),
|
|
5472
|
+
size: 16,
|
|
5473
|
+
color: "var(--fb-error-color, #dc3545)",
|
|
5474
|
+
textColor: "white",
|
|
5475
|
+
onClick: () => removeColumn(colIdx)
|
|
5476
|
+
});
|
|
5477
|
+
btn.setAttribute("data-action", "remove-col");
|
|
5478
|
+
btn.setAttribute("data-col", String(colIdx));
|
|
5479
|
+
btn.style.position = "absolute";
|
|
5480
|
+
btn.style.display = "none";
|
|
5481
|
+
tableWrapper.appendChild(btn);
|
|
5482
|
+
colRemoveBtns.push(btn);
|
|
5483
|
+
}
|
|
5484
|
+
}
|
|
5485
|
+
if (numRows > 1) {
|
|
5486
|
+
const allRowElements = [
|
|
5487
|
+
...tableEl.tHead ? Array.from(tableEl.tHead.rows) : [],
|
|
5488
|
+
...tableEl.tBodies[0] ? Array.from(tableEl.tBodies[0].rows) : []
|
|
5489
|
+
].filter((r) => r.querySelector("td[data-row]"));
|
|
5490
|
+
for (const rowEl of allRowElements) {
|
|
5491
|
+
const firstTd = rowEl.querySelector("td[data-row]");
|
|
5492
|
+
if (!firstTd) continue;
|
|
5493
|
+
const rowIdx = parseInt(firstTd.getAttribute("data-row") ?? "0", 10);
|
|
5494
|
+
const btn = makeOverlayCircleBtn({
|
|
5495
|
+
label: "\xD7",
|
|
5496
|
+
title: t("tableRemoveRow", state),
|
|
5497
|
+
size: 16,
|
|
5498
|
+
color: "var(--fb-error-color, #dc3545)",
|
|
5499
|
+
textColor: "white",
|
|
5500
|
+
onClick: () => removeRow(rowIdx)
|
|
5501
|
+
});
|
|
5502
|
+
btn.setAttribute("data-action", "remove-row");
|
|
5503
|
+
btn.setAttribute("data-row", String(rowIdx));
|
|
5504
|
+
btn.style.position = "absolute";
|
|
5505
|
+
btn.style.display = "none";
|
|
5506
|
+
tableWrapper.appendChild(btn);
|
|
5507
|
+
rowRemoveBtns.push(btn);
|
|
5508
|
+
}
|
|
5509
|
+
}
|
|
5510
|
+
function updateTopZoneOverlays(mx, wr, tblR, scrollL, active) {
|
|
5511
|
+
const headerCells = active ? Array.from(tableEl.querySelectorAll("thead td[data-col]")) : [];
|
|
5512
|
+
let closestColIdx = -1;
|
|
5513
|
+
let closestColDist = Infinity;
|
|
5514
|
+
let closestBorderX = -1;
|
|
5515
|
+
let closestAfterCol = -1;
|
|
5516
|
+
let closestBorderDist = Infinity;
|
|
5517
|
+
for (let i = 0; i < headerCells.length; i++) {
|
|
5518
|
+
const cellRect = headerCells[i].getBoundingClientRect();
|
|
5519
|
+
const centerX = (cellRect.left + cellRect.right) / 2;
|
|
5520
|
+
const dist = Math.abs(mx - centerX);
|
|
5521
|
+
if (dist < closestColDist) {
|
|
5522
|
+
closestColDist = dist;
|
|
5523
|
+
closestColIdx = i;
|
|
5524
|
+
}
|
|
5525
|
+
const borderDist = Math.abs(mx - cellRect.right);
|
|
5526
|
+
if (borderDist < closestBorderDist && borderDist < 20) {
|
|
5527
|
+
closestBorderDist = borderDist;
|
|
5528
|
+
closestBorderX = cellRect.right - wr.left + scrollL;
|
|
5529
|
+
closestAfterCol = parseInt(headerCells[i].getAttribute("data-col") ?? "0", 10);
|
|
5530
|
+
}
|
|
5531
|
+
}
|
|
5532
|
+
colRemoveBtns.forEach((btn, idx) => {
|
|
5533
|
+
if (!active || idx !== closestColIdx) {
|
|
5534
|
+
btn.style.display = "none";
|
|
5535
|
+
return;
|
|
5536
|
+
}
|
|
5537
|
+
const cellRect = headerCells[idx].getBoundingClientRect();
|
|
5538
|
+
const centerX = (cellRect.left + cellRect.right) / 2 - wr.left + scrollL;
|
|
5539
|
+
btn.style.left = `${centerX - 8}px`;
|
|
5540
|
+
btn.style.top = "2px";
|
|
5541
|
+
btn.style.display = "flex";
|
|
5542
|
+
});
|
|
5543
|
+
if (active && closestAfterCol >= 0) {
|
|
5544
|
+
insertColBtn.style.display = "flex";
|
|
5545
|
+
insertColBtn.style.left = `${closestBorderX - 10}px`;
|
|
5546
|
+
insertColBtn.style.top = `${tblR.top - wr.top - 10}px`;
|
|
5547
|
+
insertColBtn.dataset.afterCol = String(closestAfterCol);
|
|
5548
|
+
} else {
|
|
5549
|
+
insertColBtn.style.display = "none";
|
|
5550
|
+
}
|
|
5551
|
+
}
|
|
5552
|
+
function updateLeftZoneOverlays(my, wr, tblR, scrollL, active) {
|
|
5553
|
+
const allRowEls = [];
|
|
5554
|
+
if (active) {
|
|
5555
|
+
if (tableEl.tHead) {
|
|
5556
|
+
for (const row of Array.from(tableEl.tHead.rows)) {
|
|
5557
|
+
if (row.querySelector("td[data-row]")) allRowEls.push(row);
|
|
5558
|
+
}
|
|
5559
|
+
}
|
|
5560
|
+
if (tableEl.tBodies[0]) {
|
|
5561
|
+
for (const row of Array.from(tableEl.tBodies[0].rows)) {
|
|
5562
|
+
if (row.querySelector("td[data-row]")) allRowEls.push(row);
|
|
5563
|
+
}
|
|
5564
|
+
}
|
|
5565
|
+
}
|
|
5566
|
+
let closestRowIdx = -1;
|
|
5567
|
+
let closestRowDist = Infinity;
|
|
5568
|
+
let closestBorderY = -1;
|
|
5569
|
+
let closestAfterRow = -1;
|
|
5570
|
+
let closestRowBorderDist = Infinity;
|
|
5571
|
+
for (let i = 0; i < allRowEls.length; i++) {
|
|
5572
|
+
const trRect = allRowEls[i].getBoundingClientRect();
|
|
5573
|
+
const centerY = (trRect.top + trRect.bottom) / 2;
|
|
5574
|
+
const dist = Math.abs(my - centerY);
|
|
5575
|
+
if (dist < closestRowDist) {
|
|
5576
|
+
closestRowDist = dist;
|
|
5577
|
+
closestRowIdx = i;
|
|
5578
|
+
}
|
|
5579
|
+
const borderDist = Math.abs(my - trRect.bottom);
|
|
5580
|
+
if (borderDist < closestRowBorderDist && borderDist < 14) {
|
|
5581
|
+
closestRowBorderDist = borderDist;
|
|
5582
|
+
closestBorderY = trRect.bottom - wr.top;
|
|
5583
|
+
const firstTd = allRowEls[i].querySelector("td[data-row]");
|
|
5584
|
+
closestAfterRow = parseInt(firstTd?.getAttribute("data-row") ?? "0", 10);
|
|
5585
|
+
}
|
|
5586
|
+
}
|
|
5587
|
+
rowRemoveBtns.forEach((btn, idx) => {
|
|
5588
|
+
if (!active || idx !== closestRowIdx) {
|
|
5589
|
+
btn.style.display = "none";
|
|
5590
|
+
return;
|
|
5591
|
+
}
|
|
5592
|
+
const trRect = allRowEls[idx].getBoundingClientRect();
|
|
5593
|
+
const centerY = (trRect.top + trRect.bottom) / 2 - wr.top;
|
|
5594
|
+
btn.style.left = "4px";
|
|
5595
|
+
btn.style.top = `${centerY - 8}px`;
|
|
5596
|
+
btn.style.display = "flex";
|
|
5597
|
+
});
|
|
5598
|
+
if (active && closestAfterRow >= 0) {
|
|
5599
|
+
insertRowBtn.style.display = "flex";
|
|
5600
|
+
insertRowBtn.style.top = `${closestBorderY - 10}px`;
|
|
5601
|
+
insertRowBtn.style.left = `${tblR.left - wr.left + scrollL - 10}px`;
|
|
5602
|
+
insertRowBtn.dataset.afterRow = String(closestAfterRow);
|
|
5603
|
+
} else {
|
|
5604
|
+
insertRowBtn.style.display = "none";
|
|
5605
|
+
}
|
|
5606
|
+
}
|
|
5607
|
+
let rafPending = false;
|
|
5608
|
+
tableWrapper.onmousemove = (e) => {
|
|
5609
|
+
const target = e.target;
|
|
5610
|
+
if (target.tagName === "BUTTON" && target.parentElement === tableWrapper) return;
|
|
5611
|
+
if (rafPending) return;
|
|
5612
|
+
rafPending = true;
|
|
5613
|
+
const mx = e.clientX;
|
|
5614
|
+
const my = e.clientY;
|
|
5615
|
+
requestAnimationFrame(() => {
|
|
5616
|
+
rafPending = false;
|
|
5617
|
+
const wr = tableWrapper.getBoundingClientRect();
|
|
5618
|
+
const tblR = tableEl.getBoundingClientRect();
|
|
5619
|
+
const scrollL = tableWrapper.scrollLeft;
|
|
5620
|
+
const inTopZone = my >= wr.top && my < tblR.top + 4;
|
|
5621
|
+
const inLeftZone = mx >= wr.left && mx < tblR.left + 4;
|
|
5622
|
+
const visibleRight = Math.min(tblR.right, wr.right);
|
|
5623
|
+
const inRightZone = mx > visibleRight - 20 && mx <= wr.right;
|
|
5624
|
+
const inBottomZone = my > tblR.bottom - 4 && my <= wr.bottom + 20;
|
|
5625
|
+
updateTopZoneOverlays(mx, wr, tblR, scrollL, inTopZone);
|
|
5626
|
+
updateLeftZoneOverlays(my, wr, tblR, scrollL, inLeftZone);
|
|
5627
|
+
addLastColBtn.style.display = inRightZone ? "flex" : "none";
|
|
5628
|
+
if (inRightZone) {
|
|
5629
|
+
addLastColBtn.style.left = `${wr.right - wr.left + scrollL - 20}px`;
|
|
5630
|
+
addLastColBtn.style.top = `${(tblR.top + tblR.bottom) / 2 - wr.top - 10}px`;
|
|
5631
|
+
}
|
|
5632
|
+
addLastRowBtn.style.display = inBottomZone ? "flex" : "none";
|
|
5633
|
+
if (inBottomZone) {
|
|
5634
|
+
const visibleCenterX = (wr.left + wr.right) / 2 - wr.left + scrollL;
|
|
5635
|
+
addLastRowBtn.style.left = `${visibleCenterX - 10}px`;
|
|
5636
|
+
addLastRowBtn.style.top = `${tblR.bottom - wr.top - 10}px`;
|
|
5637
|
+
}
|
|
5638
|
+
});
|
|
5639
|
+
};
|
|
5640
|
+
tableWrapper.onmouseleave = () => {
|
|
5641
|
+
colRemoveBtns.forEach((btn) => {
|
|
5642
|
+
btn.style.display = "none";
|
|
5643
|
+
});
|
|
5644
|
+
rowRemoveBtns.forEach((btn) => {
|
|
5645
|
+
btn.style.display = "none";
|
|
5646
|
+
});
|
|
5647
|
+
insertColBtn.style.display = "none";
|
|
5648
|
+
insertRowBtn.style.display = "none";
|
|
5649
|
+
addLastColBtn.style.display = "none";
|
|
5650
|
+
addLastRowBtn.style.display = "none";
|
|
5651
|
+
};
|
|
5652
|
+
}
|
|
5653
|
+
rebuild();
|
|
5654
|
+
}
|
|
5655
|
+
function defaultTableData(element) {
|
|
5656
|
+
return {
|
|
5657
|
+
cells: createEmptyCells(element.rows ?? 3, element.columns ?? 3),
|
|
5658
|
+
merges: []
|
|
5659
|
+
};
|
|
5660
|
+
}
|
|
5661
|
+
function isTableData(v) {
|
|
5662
|
+
return v !== null && typeof v === "object" && "cells" in v && Array.isArray(v.cells);
|
|
5663
|
+
}
|
|
5664
|
+
function renderTableElement(element, ctx, wrapper, pathKey) {
|
|
5665
|
+
const state = ctx.state;
|
|
5666
|
+
const rawPrefill = ctx.prefill[element.key];
|
|
5667
|
+
const initialData = isTableData(rawPrefill) ? rawPrefill : isTableData(element.default) ? element.default : defaultTableData(element);
|
|
5668
|
+
if (state.config.readonly) {
|
|
5669
|
+
renderReadonlyTable(initialData, wrapper);
|
|
5670
|
+
} else {
|
|
5671
|
+
renderEditTable(element, initialData, pathKey, ctx, wrapper);
|
|
5672
|
+
}
|
|
5673
|
+
}
|
|
5674
|
+
function validateTableElement(element, key, context) {
|
|
5675
|
+
const { scopeRoot, skipValidation } = context;
|
|
5676
|
+
const errors = [];
|
|
5677
|
+
const hiddenInput = scopeRoot.querySelector(
|
|
5678
|
+
`[name="${key}"]`
|
|
5679
|
+
);
|
|
5680
|
+
if (!hiddenInput) {
|
|
5681
|
+
return { value: null, errors };
|
|
5682
|
+
}
|
|
5683
|
+
let value = null;
|
|
5684
|
+
try {
|
|
5685
|
+
value = JSON.parse(hiddenInput.value);
|
|
5686
|
+
} catch {
|
|
5687
|
+
errors.push(`${key}: invalid table data`);
|
|
5688
|
+
return { value: null, errors };
|
|
5689
|
+
}
|
|
5690
|
+
if (!skipValidation && element.required) {
|
|
5691
|
+
const hasContent = value.cells.some(
|
|
5692
|
+
(row) => row.some((cell) => cell.trim() !== "")
|
|
5693
|
+
);
|
|
5694
|
+
if (!hasContent) {
|
|
5695
|
+
errors.push(`${key}: ${t("required", context.state)}`);
|
|
5696
|
+
}
|
|
5697
|
+
}
|
|
5698
|
+
return { value, errors };
|
|
5699
|
+
}
|
|
5700
|
+
function updateTableField(_element, fieldPath, value, context) {
|
|
5701
|
+
const { scopeRoot } = context;
|
|
5702
|
+
const hiddenInput = scopeRoot.querySelector(
|
|
5703
|
+
`[name="${fieldPath}"]`
|
|
5704
|
+
);
|
|
5705
|
+
if (!hiddenInput) {
|
|
5706
|
+
console.warn(
|
|
5707
|
+
`updateTableField: no hidden input found for "${fieldPath}". Re-render to reflect new data.`
|
|
5708
|
+
);
|
|
5709
|
+
return;
|
|
5710
|
+
}
|
|
5711
|
+
if (isTableData(value) && hiddenInput._applyExternalUpdate) {
|
|
5712
|
+
hiddenInput._applyExternalUpdate(value);
|
|
5713
|
+
} else {
|
|
5714
|
+
hiddenInput.value = JSON.stringify(value);
|
|
5715
|
+
}
|
|
5716
|
+
}
|
|
5717
|
+
|
|
5718
|
+
// src/components/richinput.ts
|
|
5719
|
+
function applyAutoExpand2(textarea, backdrop) {
|
|
5720
|
+
textarea.style.overflow = "hidden";
|
|
5721
|
+
textarea.style.resize = "none";
|
|
5722
|
+
const lineCount = (textarea.value.match(/\n/g) || []).length + 1;
|
|
5723
|
+
textarea.rows = Math.max(3, lineCount);
|
|
5724
|
+
const resize = () => {
|
|
5725
|
+
if (!textarea.isConnected) return;
|
|
5726
|
+
textarea.style.height = "0";
|
|
5727
|
+
textarea.style.height = `${textarea.scrollHeight}px`;
|
|
5728
|
+
if (backdrop) {
|
|
5729
|
+
backdrop.style.height = `${textarea.scrollHeight}px`;
|
|
5730
|
+
}
|
|
5731
|
+
};
|
|
5732
|
+
textarea.addEventListener("input", resize);
|
|
5733
|
+
setTimeout(() => {
|
|
5734
|
+
if (textarea.isConnected) resize();
|
|
5735
|
+
}, 0);
|
|
5736
|
+
}
|
|
5737
|
+
function buildFileLabels(files, state) {
|
|
5738
|
+
const labels = /* @__PURE__ */ new Map();
|
|
5739
|
+
const nameCount = /* @__PURE__ */ new Map();
|
|
5740
|
+
for (const rid of files) {
|
|
5741
|
+
const meta = state.resourceIndex.get(rid);
|
|
5742
|
+
const name = meta?.name ?? rid;
|
|
5743
|
+
nameCount.set(name, (nameCount.get(name) ?? 0) + 1);
|
|
5744
|
+
}
|
|
5745
|
+
for (const rid of files) {
|
|
5746
|
+
const meta = state.resourceIndex.get(rid);
|
|
5747
|
+
const name = meta?.name ?? rid;
|
|
5748
|
+
if ((nameCount.get(name) ?? 1) > 1 && meta) {
|
|
5749
|
+
labels.set(rid, `${name} (${formatFileSize(meta.size)})`);
|
|
5750
|
+
} else {
|
|
5751
|
+
labels.set(rid, name);
|
|
5752
|
+
}
|
|
5753
|
+
}
|
|
5754
|
+
return labels;
|
|
5755
|
+
}
|
|
5756
|
+
function isImageMeta(meta) {
|
|
5757
|
+
if (!meta) return false;
|
|
5758
|
+
return meta.type.startsWith("image/");
|
|
5759
|
+
}
|
|
5760
|
+
function buildNameToRid(files, state) {
|
|
5761
|
+
const labels = buildFileLabels(files, state);
|
|
5762
|
+
const map = /* @__PURE__ */ new Map();
|
|
5763
|
+
for (const rid of files) {
|
|
5764
|
+
const label = labels.get(rid);
|
|
5765
|
+
if (label) map.set(label, rid);
|
|
5766
|
+
}
|
|
5767
|
+
return map;
|
|
5768
|
+
}
|
|
5769
|
+
function formatMention(name) {
|
|
5770
|
+
if (/\s/.test(name) || name.includes('"')) {
|
|
5771
|
+
return `@"${name.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
5772
|
+
}
|
|
5773
|
+
return `@${name}`;
|
|
5774
|
+
}
|
|
5775
|
+
function findAtTokens(text) {
|
|
5776
|
+
const tokens = [];
|
|
5777
|
+
const len = text.length;
|
|
5778
|
+
let i = 0;
|
|
5779
|
+
while (i < len) {
|
|
5780
|
+
if (text[i] === "@") {
|
|
5781
|
+
if (i > 0 && !/\s/.test(text[i - 1])) {
|
|
5782
|
+
i++;
|
|
5783
|
+
continue;
|
|
5784
|
+
}
|
|
5785
|
+
const start = i;
|
|
5786
|
+
i++;
|
|
5787
|
+
if (i < len && text[i] === '"') {
|
|
5788
|
+
i++;
|
|
5789
|
+
let name = "";
|
|
5790
|
+
while (i < len && text[i] !== '"') {
|
|
5791
|
+
if (text[i] === "\\" && i + 1 < len) {
|
|
5792
|
+
name += text[i + 1];
|
|
5793
|
+
i += 2;
|
|
5794
|
+
} else {
|
|
5795
|
+
name += text[i];
|
|
5796
|
+
i++;
|
|
5797
|
+
}
|
|
5798
|
+
}
|
|
5799
|
+
if (i < len && text[i] === '"') {
|
|
5800
|
+
i++;
|
|
5801
|
+
if (i >= len || /\s/.test(text[i])) {
|
|
5802
|
+
tokens.push({ start, end: i, raw: text.slice(start, i), name });
|
|
5803
|
+
}
|
|
5804
|
+
}
|
|
5805
|
+
} else if (i < len && !/\s/.test(text[i])) {
|
|
5806
|
+
const wordStart = i;
|
|
5807
|
+
while (i < len && !/\s/.test(text[i])) {
|
|
5808
|
+
i++;
|
|
5809
|
+
}
|
|
5810
|
+
const name = text.slice(wordStart, i);
|
|
5811
|
+
tokens.push({ start, end: i, raw: text.slice(start, i), name });
|
|
5812
|
+
}
|
|
5813
|
+
} else {
|
|
5814
|
+
i++;
|
|
5815
|
+
}
|
|
5816
|
+
}
|
|
5817
|
+
return tokens;
|
|
5818
|
+
}
|
|
5819
|
+
function findMentions(text, nameToRid) {
|
|
5820
|
+
if (nameToRid.size === 0) return [];
|
|
5821
|
+
const tokens = findAtTokens(text);
|
|
5822
|
+
const results = [];
|
|
5823
|
+
for (const token of tokens) {
|
|
5824
|
+
const rid = nameToRid.get(token.name);
|
|
5825
|
+
if (rid) {
|
|
5826
|
+
results.push({
|
|
5827
|
+
start: token.start,
|
|
5828
|
+
end: token.end,
|
|
5829
|
+
name: token.name,
|
|
5830
|
+
rid
|
|
5831
|
+
});
|
|
5832
|
+
}
|
|
5833
|
+
}
|
|
5834
|
+
return results;
|
|
5835
|
+
}
|
|
5836
|
+
function replaceFilenamesWithRids(text, nameToRid) {
|
|
5837
|
+
const mentions = findMentions(text, nameToRid);
|
|
5838
|
+
if (mentions.length === 0) return text;
|
|
5839
|
+
let result = "";
|
|
5840
|
+
let lastIdx = 0;
|
|
5841
|
+
for (const m of mentions) {
|
|
5842
|
+
result += text.slice(lastIdx, m.start);
|
|
5843
|
+
result += `@${m.rid}`;
|
|
5844
|
+
lastIdx = m.end;
|
|
5845
|
+
}
|
|
5846
|
+
result += text.slice(lastIdx);
|
|
5847
|
+
return result;
|
|
5848
|
+
}
|
|
5849
|
+
function replaceRidsWithFilenames(text, files, state) {
|
|
5850
|
+
const labels = buildFileLabels(files, state);
|
|
5851
|
+
const ridToLabel = /* @__PURE__ */ new Map();
|
|
5852
|
+
for (const rid of files) {
|
|
5853
|
+
const label = labels.get(rid);
|
|
5854
|
+
if (label) ridToLabel.set(rid, label);
|
|
5855
|
+
}
|
|
5856
|
+
const tokens = findAtTokens(text);
|
|
5857
|
+
if (tokens.length === 0) return text;
|
|
5858
|
+
let result = "";
|
|
5859
|
+
let lastIdx = 0;
|
|
5860
|
+
for (const token of tokens) {
|
|
5861
|
+
result += text.slice(lastIdx, token.start);
|
|
5862
|
+
const label = ridToLabel.get(token.name);
|
|
5863
|
+
if (label) {
|
|
5864
|
+
result += formatMention(label);
|
|
5865
|
+
} else {
|
|
5866
|
+
result += token.raw;
|
|
5867
|
+
}
|
|
5868
|
+
lastIdx = token.end;
|
|
5869
|
+
}
|
|
5870
|
+
result += text.slice(lastIdx);
|
|
5871
|
+
return result;
|
|
5872
|
+
}
|
|
5873
|
+
function renderThumbContent(thumb, rid, meta, state) {
|
|
5874
|
+
clear(thumb);
|
|
5875
|
+
if (meta?.file && isImageMeta(meta)) {
|
|
5876
|
+
const img = document.createElement("img");
|
|
5877
|
+
img.alt = meta.name;
|
|
5878
|
+
img.style.cssText = "width: 100%; height: 100%; object-fit: cover; display: block;";
|
|
5879
|
+
const reader = new FileReader();
|
|
5880
|
+
reader.onload = (e) => {
|
|
5881
|
+
img.src = e.target?.result || "";
|
|
5882
|
+
};
|
|
5883
|
+
reader.readAsDataURL(meta.file);
|
|
5884
|
+
thumb.appendChild(img);
|
|
5885
|
+
} else if (state.config.getThumbnail) {
|
|
5886
|
+
state.config.getThumbnail(rid).then((url) => {
|
|
5887
|
+
if (!url || !thumb.isConnected) return;
|
|
5888
|
+
const img = document.createElement("img");
|
|
5889
|
+
img.alt = meta?.name ?? rid;
|
|
5890
|
+
img.src = url;
|
|
5891
|
+
img.style.cssText = "width: 100%; height: 100%; object-fit: cover; display: block;";
|
|
5892
|
+
clear(thumb);
|
|
5893
|
+
thumb.appendChild(img);
|
|
5894
|
+
}).catch((err) => {
|
|
5895
|
+
state.config.onThumbnailError?.(err, rid);
|
|
5896
|
+
});
|
|
5897
|
+
const placeholder = document.createElement("div");
|
|
5898
|
+
placeholder.style.cssText = "width: 100%; height: 100%; background: var(--fb-background-hover-color, #f3f4f6); display: flex; align-items: center; justify-content: center;";
|
|
5899
|
+
placeholder.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>';
|
|
5900
|
+
thumb.appendChild(placeholder);
|
|
5901
|
+
} else {
|
|
5902
|
+
const icon = document.createElement("div");
|
|
5903
|
+
icon.style.cssText = "width: 100%; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; padding: 2px; box-sizing: border-box;";
|
|
5904
|
+
icon.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>';
|
|
5905
|
+
if (meta?.name) {
|
|
5906
|
+
const nameEl = document.createElement("span");
|
|
5907
|
+
nameEl.style.cssText = "font-size: 9px; text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 44px; color: var(--fb-text-color, #111827);";
|
|
5908
|
+
nameEl.textContent = meta.name;
|
|
5909
|
+
icon.appendChild(nameEl);
|
|
5910
|
+
}
|
|
5911
|
+
thumb.appendChild(icon);
|
|
5912
|
+
}
|
|
5913
|
+
}
|
|
5914
|
+
function renderImagePreview(hoverEl, rid, meta, state) {
|
|
5915
|
+
clear(hoverEl);
|
|
5916
|
+
if (meta?.file && isImageMeta(meta)) {
|
|
5917
|
+
const img = document.createElement("img");
|
|
5918
|
+
img.alt = meta.name;
|
|
5919
|
+
img.style.cssText = "max-width: 120px; max-height: 120px; object-fit: contain; display: block;";
|
|
5920
|
+
const reader = new FileReader();
|
|
5921
|
+
reader.onload = (e) => {
|
|
5922
|
+
img.src = e.target?.result || "";
|
|
5923
|
+
};
|
|
5924
|
+
reader.readAsDataURL(meta.file);
|
|
5925
|
+
hoverEl.appendChild(img);
|
|
5926
|
+
} else if (state.config.getThumbnail) {
|
|
5927
|
+
state.config.getThumbnail(rid).then((url) => {
|
|
5928
|
+
if (!url || !hoverEl.isConnected) return;
|
|
5929
|
+
const img = document.createElement("img");
|
|
5930
|
+
img.alt = meta?.name ?? rid;
|
|
5931
|
+
img.src = url;
|
|
5932
|
+
img.style.cssText = "max-width: 120px; max-height: 120px; object-fit: contain; display: block;";
|
|
5933
|
+
clear(hoverEl);
|
|
5934
|
+
hoverEl.appendChild(img);
|
|
5935
|
+
}).catch((err) => {
|
|
5936
|
+
state.config.onThumbnailError?.(err, rid);
|
|
5937
|
+
});
|
|
5938
|
+
}
|
|
5939
|
+
}
|
|
5940
|
+
function positionPortalTooltip(tooltip, anchor) {
|
|
5941
|
+
const rect = anchor.getBoundingClientRect();
|
|
5942
|
+
const ttRect = tooltip.getBoundingClientRect();
|
|
5943
|
+
const left = Math.max(
|
|
5944
|
+
4,
|
|
5945
|
+
Math.min(
|
|
5946
|
+
rect.left + rect.width / 2 - ttRect.width / 2,
|
|
5947
|
+
window.innerWidth - ttRect.width - 4
|
|
5948
|
+
)
|
|
5949
|
+
);
|
|
5950
|
+
const topAbove = rect.top - ttRect.height - 8;
|
|
5951
|
+
const topBelow = rect.bottom + 8;
|
|
5952
|
+
const top = topAbove >= 4 ? topAbove : topBelow;
|
|
5953
|
+
tooltip.style.left = `${left}px`;
|
|
5954
|
+
tooltip.style.top = `${Math.max(4, top)}px`;
|
|
5955
|
+
}
|
|
5956
|
+
function showMentionTooltip(anchor, rid, state) {
|
|
5957
|
+
const meta = state.resourceIndex.get(rid);
|
|
5958
|
+
const tooltip = document.createElement("div");
|
|
5959
|
+
tooltip.className = "fb-richinput-portal-tooltip fb-richinput-mention-tooltip";
|
|
5960
|
+
tooltip.style.cssText = `
|
|
5961
|
+
position: fixed;
|
|
5962
|
+
z-index: 99999;
|
|
5963
|
+
background: #fff;
|
|
5964
|
+
border: 1px solid var(--fb-border-color, #d1d5db);
|
|
5965
|
+
border-radius: 8px;
|
|
5966
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
5967
|
+
padding: 4px;
|
|
5968
|
+
pointer-events: none;
|
|
5969
|
+
min-width: 60px;
|
|
5970
|
+
max-width: 140px;
|
|
5971
|
+
`;
|
|
5972
|
+
const preview = document.createElement("div");
|
|
5973
|
+
preview.style.cssText = "min-height: 60px; max-height: 120px; display: flex; align-items: center; justify-content: center;";
|
|
5974
|
+
renderImagePreview(preview, rid, meta, state);
|
|
5975
|
+
tooltip.appendChild(preview);
|
|
5976
|
+
document.body.appendChild(tooltip);
|
|
5977
|
+
positionPortalTooltip(tooltip, anchor);
|
|
5978
|
+
return tooltip;
|
|
5979
|
+
}
|
|
5980
|
+
function showFileTooltip(anchor, opts) {
|
|
5981
|
+
const { rid, state, isReadonly, onMention, onRemove } = opts;
|
|
5982
|
+
const meta = state.resourceIndex.get(rid);
|
|
5983
|
+
const filename = meta?.name ?? rid;
|
|
5984
|
+
const tooltip = document.createElement("div");
|
|
5985
|
+
tooltip.className = "fb-richinput-portal-tooltip fb-richinput-file-tooltip";
|
|
5986
|
+
tooltip.style.cssText = `
|
|
5987
|
+
position: fixed;
|
|
5988
|
+
z-index: 99999;
|
|
5989
|
+
background: #fff;
|
|
5990
|
+
border: 1px solid var(--fb-border-color, #d1d5db);
|
|
5991
|
+
border-radius: 8px;
|
|
5992
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
5993
|
+
padding: 4px;
|
|
5994
|
+
pointer-events: auto;
|
|
5995
|
+
min-width: 80px;
|
|
5996
|
+
max-width: 260px;
|
|
5997
|
+
`;
|
|
5998
|
+
const preview = document.createElement("div");
|
|
5999
|
+
preview.style.cssText = "min-height: 60px; max-height: 120px; display: flex; align-items: center; justify-content: center;";
|
|
6000
|
+
renderImagePreview(preview, rid, meta, state);
|
|
6001
|
+
tooltip.appendChild(preview);
|
|
6002
|
+
const nameEl = document.createElement("div");
|
|
6003
|
+
nameEl.style.cssText = "padding: 4px 6px 2px; font-size: 12px; color: var(--fb-text-color, #111827); word-break: break-word; border-top: 1px solid var(--fb-border-color, #d1d5db);";
|
|
6004
|
+
nameEl.textContent = filename;
|
|
6005
|
+
tooltip.appendChild(nameEl);
|
|
6006
|
+
const actionsRow = document.createElement("div");
|
|
6007
|
+
actionsRow.style.cssText = "display: flex; align-items: center; gap: 2px; padding: 3px 4px 2px; border-top: 1px solid var(--fb-border-color, #d1d5db); justify-content: center;";
|
|
6008
|
+
const btnStyle = "background: none; border: none; cursor: pointer; padding: 3px 5px; border-radius: 4px; color: var(--fb-text-muted-color, #6b7280); display: flex; align-items: center; transition: background 0.1s, color 0.1s;";
|
|
6009
|
+
const btnHoverIn = (btn) => {
|
|
6010
|
+
btn.style.background = "var(--fb-background-hover-color, #f3f4f6)";
|
|
6011
|
+
btn.style.color = "var(--fb-text-color, #111827)";
|
|
6012
|
+
};
|
|
6013
|
+
const btnHoverOut = (btn) => {
|
|
6014
|
+
btn.style.background = "none";
|
|
6015
|
+
btn.style.color = "var(--fb-text-muted-color, #6b7280)";
|
|
6016
|
+
};
|
|
6017
|
+
if (!isReadonly && onMention) {
|
|
6018
|
+
const mentionBtn = document.createElement("button");
|
|
6019
|
+
mentionBtn.type = "button";
|
|
6020
|
+
mentionBtn.title = "Mention";
|
|
6021
|
+
mentionBtn.style.cssText = btnStyle;
|
|
6022
|
+
mentionBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="12" cy="12" r="4"/><path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-3.92 7.94"/></svg>';
|
|
6023
|
+
mentionBtn.addEventListener("mouseenter", () => btnHoverIn(mentionBtn));
|
|
6024
|
+
mentionBtn.addEventListener("mouseleave", () => btnHoverOut(mentionBtn));
|
|
6025
|
+
mentionBtn.addEventListener("click", (e) => {
|
|
6026
|
+
e.stopPropagation();
|
|
6027
|
+
onMention();
|
|
6028
|
+
tooltip.remove();
|
|
6029
|
+
});
|
|
6030
|
+
actionsRow.appendChild(mentionBtn);
|
|
6031
|
+
}
|
|
6032
|
+
if (state.config.downloadFile) {
|
|
6033
|
+
const dlBtn = document.createElement("button");
|
|
6034
|
+
dlBtn.type = "button";
|
|
6035
|
+
dlBtn.title = "Download";
|
|
6036
|
+
dlBtn.style.cssText = btnStyle;
|
|
6037
|
+
dlBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>';
|
|
6038
|
+
dlBtn.addEventListener("mouseenter", () => btnHoverIn(dlBtn));
|
|
6039
|
+
dlBtn.addEventListener("mouseleave", () => btnHoverOut(dlBtn));
|
|
6040
|
+
dlBtn.addEventListener("click", (e) => {
|
|
6041
|
+
e.stopPropagation();
|
|
6042
|
+
state.config.downloadFile?.(rid, filename);
|
|
6043
|
+
});
|
|
6044
|
+
actionsRow.appendChild(dlBtn);
|
|
6045
|
+
}
|
|
6046
|
+
const hasOpenUrl = !!(state.config.getDownloadUrl ?? state.config.getThumbnail);
|
|
6047
|
+
if (hasOpenUrl) {
|
|
6048
|
+
const openBtn = document.createElement("button");
|
|
6049
|
+
openBtn.type = "button";
|
|
6050
|
+
openBtn.title = "Open in new window";
|
|
6051
|
+
openBtn.style.cssText = btnStyle;
|
|
6052
|
+
openBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>';
|
|
6053
|
+
openBtn.addEventListener("mouseenter", () => btnHoverIn(openBtn));
|
|
6054
|
+
openBtn.addEventListener("mouseleave", () => btnHoverOut(openBtn));
|
|
6055
|
+
openBtn.addEventListener("click", (e) => {
|
|
6056
|
+
e.stopPropagation();
|
|
6057
|
+
if (state.config.getDownloadUrl) {
|
|
6058
|
+
const url = state.config.getDownloadUrl(rid);
|
|
6059
|
+
if (url) {
|
|
6060
|
+
window.open(url, "_blank");
|
|
6061
|
+
} else {
|
|
6062
|
+
state.config.getThumbnail?.(rid).then((thumbUrl) => {
|
|
6063
|
+
if (thumbUrl) window.open(thumbUrl, "_blank");
|
|
6064
|
+
}).catch(() => {
|
|
6065
|
+
});
|
|
6066
|
+
}
|
|
6067
|
+
} else {
|
|
6068
|
+
state.config.getThumbnail?.(rid).then((url) => {
|
|
6069
|
+
if (url) window.open(url, "_blank");
|
|
6070
|
+
}).catch(() => {
|
|
6071
|
+
});
|
|
6072
|
+
}
|
|
6073
|
+
});
|
|
6074
|
+
actionsRow.appendChild(openBtn);
|
|
6075
|
+
}
|
|
6076
|
+
if (!isReadonly && onRemove) {
|
|
6077
|
+
const removeBtn = document.createElement("button");
|
|
6078
|
+
removeBtn.type = "button";
|
|
6079
|
+
removeBtn.title = "Remove";
|
|
6080
|
+
removeBtn.style.cssText = btnStyle;
|
|
6081
|
+
removeBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>';
|
|
6082
|
+
removeBtn.addEventListener("mouseenter", () => {
|
|
6083
|
+
removeBtn.style.background = "var(--fb-background-hover-color, #f3f4f6)";
|
|
6084
|
+
removeBtn.style.color = "var(--fb-error-color, #ef4444)";
|
|
6085
|
+
});
|
|
6086
|
+
removeBtn.addEventListener("mouseleave", () => btnHoverOut(removeBtn));
|
|
6087
|
+
removeBtn.addEventListener("click", (e) => {
|
|
6088
|
+
e.stopPropagation();
|
|
6089
|
+
tooltip.remove();
|
|
6090
|
+
onRemove();
|
|
6091
|
+
});
|
|
6092
|
+
actionsRow.appendChild(removeBtn);
|
|
6093
|
+
}
|
|
6094
|
+
const hasActions = actionsRow.children.length > 0;
|
|
6095
|
+
if (hasActions) {
|
|
6096
|
+
tooltip.appendChild(actionsRow);
|
|
6097
|
+
}
|
|
6098
|
+
document.body.appendChild(tooltip);
|
|
6099
|
+
positionPortalTooltip(tooltip, anchor);
|
|
6100
|
+
return tooltip;
|
|
6101
|
+
}
|
|
6102
|
+
function createTooltipHandle() {
|
|
6103
|
+
return { element: null, hideTimer: null };
|
|
6104
|
+
}
|
|
6105
|
+
function scheduleHideTooltip(handle, delayMs = 150) {
|
|
6106
|
+
if (handle.hideTimer !== null) return;
|
|
6107
|
+
handle.hideTimer = setTimeout(() => {
|
|
6108
|
+
handle.hideTimer = null;
|
|
6109
|
+
if (handle.element) {
|
|
6110
|
+
handle.element.remove();
|
|
6111
|
+
handle.element = null;
|
|
6112
|
+
}
|
|
6113
|
+
}, delayMs);
|
|
6114
|
+
}
|
|
6115
|
+
function cancelHideTooltip(handle) {
|
|
6116
|
+
if (handle.hideTimer !== null) {
|
|
6117
|
+
clearTimeout(handle.hideTimer);
|
|
6118
|
+
handle.hideTimer = null;
|
|
6119
|
+
}
|
|
6120
|
+
}
|
|
6121
|
+
function removePortalTooltip(tooltip) {
|
|
6122
|
+
if (tooltip) tooltip.remove();
|
|
6123
|
+
return null;
|
|
6124
|
+
}
|
|
6125
|
+
function getAtTrigger(textarea) {
|
|
6126
|
+
const cursorPos = textarea.selectionStart ?? 0;
|
|
6127
|
+
const textBefore = textarea.value.slice(0, cursorPos);
|
|
6128
|
+
for (let i = textBefore.length - 1; i >= 0; i--) {
|
|
6129
|
+
if (textBefore[i] === "@") {
|
|
6130
|
+
if (i === 0 || /\s/.test(textBefore[i - 1])) {
|
|
6131
|
+
let query = textBefore.slice(i + 1);
|
|
6132
|
+
if (query.startsWith('"')) {
|
|
6133
|
+
query = query.slice(1);
|
|
6134
|
+
}
|
|
6135
|
+
return { query, pos: i };
|
|
6136
|
+
}
|
|
6137
|
+
return null;
|
|
6138
|
+
}
|
|
6139
|
+
}
|
|
6140
|
+
return null;
|
|
6141
|
+
}
|
|
6142
|
+
function filterFilesForDropdown(query, files, labels) {
|
|
6143
|
+
const lq = query.toLowerCase();
|
|
6144
|
+
return files.filter((rid) => {
|
|
6145
|
+
const label = labels.get(rid) ?? rid;
|
|
6146
|
+
return label.toLowerCase().includes(lq);
|
|
6147
|
+
});
|
|
6148
|
+
}
|
|
6149
|
+
var TEXTAREA_FONT = "font-size: var(--fb-font-size, 14px); font-family: var(--fb-font-family, inherit); line-height: 1.6;";
|
|
6150
|
+
var TEXTAREA_PADDING = "padding: 12px 52px 12px 14px;";
|
|
6151
|
+
function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
|
|
6152
|
+
const state = ctx.state;
|
|
6153
|
+
const files = [...initialValue.files];
|
|
6154
|
+
const dropdownState = {
|
|
6155
|
+
open: false,
|
|
6156
|
+
query: "",
|
|
6157
|
+
triggerPos: -1,
|
|
6158
|
+
selectedIndex: 0
|
|
6159
|
+
};
|
|
6160
|
+
const docListenerCtrl = new AbortController();
|
|
6161
|
+
const hiddenInput = document.createElement("input");
|
|
6162
|
+
hiddenInput.type = "hidden";
|
|
6163
|
+
hiddenInput.name = pathKey;
|
|
6164
|
+
function getCurrentValue() {
|
|
6165
|
+
const rawText = textarea.value;
|
|
6166
|
+
const nameToRid = buildNameToRid(files, state);
|
|
6167
|
+
const submissionText = rawText ? replaceFilenamesWithRids(rawText, nameToRid) : null;
|
|
6168
|
+
const textKey = element.textKey ?? "text";
|
|
6169
|
+
const filesKey = element.filesKey ?? "files";
|
|
6170
|
+
return {
|
|
6171
|
+
[textKey]: rawText === "" ? null : submissionText,
|
|
6172
|
+
[filesKey]: [...files]
|
|
6173
|
+
};
|
|
6174
|
+
}
|
|
6175
|
+
function writeHidden() {
|
|
6176
|
+
hiddenInput.value = JSON.stringify(getCurrentValue());
|
|
6177
|
+
}
|
|
6178
|
+
const outerDiv = document.createElement("div");
|
|
6179
|
+
outerDiv.className = "fb-richinput-wrapper";
|
|
6180
|
+
outerDiv.style.cssText = `
|
|
6181
|
+
position: relative;
|
|
6182
|
+
border: 1px solid var(--fb-border-color, #d1d5db);
|
|
6183
|
+
border-radius: 16px;
|
|
6184
|
+
background: var(--fb-background-color, #f9fafb);
|
|
6185
|
+
transition: box-shadow 0.15s, border-color 0.15s;
|
|
6186
|
+
`;
|
|
6187
|
+
outerDiv.addEventListener("focusin", () => {
|
|
6188
|
+
outerDiv.style.borderColor = "var(--fb-primary-color, #0066cc)";
|
|
6189
|
+
outerDiv.style.boxShadow = "0 0 0 2px color-mix(in srgb, var(--fb-primary-color, #0066cc) 25%, transparent)";
|
|
6190
|
+
});
|
|
6191
|
+
outerDiv.addEventListener("focusout", () => {
|
|
6192
|
+
outerDiv.style.borderColor = "var(--fb-border-color, #d1d5db)";
|
|
6193
|
+
outerDiv.style.boxShadow = "none";
|
|
6194
|
+
});
|
|
6195
|
+
let dragCounter = 0;
|
|
6196
|
+
outerDiv.addEventListener("dragenter", (e) => {
|
|
6197
|
+
e.preventDefault();
|
|
6198
|
+
dragCounter++;
|
|
6199
|
+
outerDiv.style.borderColor = "var(--fb-primary-color, #0066cc)";
|
|
6200
|
+
outerDiv.style.boxShadow = "0 0 0 2px color-mix(in srgb, var(--fb-primary-color, #0066cc) 25%, transparent)";
|
|
6201
|
+
});
|
|
6202
|
+
outerDiv.addEventListener("dragover", (e) => {
|
|
6203
|
+
e.preventDefault();
|
|
6204
|
+
});
|
|
6205
|
+
outerDiv.addEventListener("dragleave", (e) => {
|
|
6206
|
+
e.preventDefault();
|
|
6207
|
+
dragCounter--;
|
|
6208
|
+
if (dragCounter <= 0) {
|
|
6209
|
+
dragCounter = 0;
|
|
6210
|
+
outerDiv.style.borderColor = "var(--fb-border-color, #d1d5db)";
|
|
6211
|
+
outerDiv.style.boxShadow = "none";
|
|
6212
|
+
}
|
|
6213
|
+
});
|
|
6214
|
+
outerDiv.addEventListener("drop", (e) => {
|
|
6215
|
+
e.preventDefault();
|
|
6216
|
+
dragCounter = 0;
|
|
6217
|
+
outerDiv.style.borderColor = "var(--fb-border-color, #d1d5db)";
|
|
6218
|
+
outerDiv.style.boxShadow = "none";
|
|
6219
|
+
const droppedFiles = e.dataTransfer?.files;
|
|
6220
|
+
if (!droppedFiles || !state.config.uploadFile) return;
|
|
6221
|
+
const maxFiles = element.maxFiles ?? Infinity;
|
|
6222
|
+
for (let i = 0; i < droppedFiles.length && files.length < maxFiles; i++) {
|
|
6223
|
+
uploadFile(droppedFiles[i]);
|
|
6224
|
+
}
|
|
6225
|
+
});
|
|
6226
|
+
const filesRow = document.createElement("div");
|
|
6227
|
+
filesRow.className = "fb-richinput-files";
|
|
6228
|
+
filesRow.style.cssText = "display: none; flex-wrap: wrap; gap: 6px; padding: 10px 14px 0; align-items: center;";
|
|
6229
|
+
const fileInput = document.createElement("input");
|
|
6230
|
+
fileInput.type = "file";
|
|
6231
|
+
fileInput.multiple = true;
|
|
6232
|
+
fileInput.style.display = "none";
|
|
6233
|
+
if (element.accept) {
|
|
6234
|
+
if (typeof element.accept === "string") {
|
|
6235
|
+
fileInput.accept = element.accept;
|
|
6236
|
+
} else {
|
|
6237
|
+
fileInput.accept = element.accept.extensions.map((ext) => ext.startsWith(".") ? ext : `.${ext}`).join(",");
|
|
6238
|
+
}
|
|
6239
|
+
}
|
|
6240
|
+
const textareaArea = document.createElement("div");
|
|
6241
|
+
textareaArea.style.cssText = "position: relative;";
|
|
6242
|
+
const backdrop = document.createElement("div");
|
|
6243
|
+
backdrop.className = "fb-richinput-backdrop";
|
|
6244
|
+
backdrop.style.cssText = `
|
|
6245
|
+
position: absolute;
|
|
6246
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
6247
|
+
${TEXTAREA_PADDING}
|
|
6248
|
+
${TEXTAREA_FONT}
|
|
6249
|
+
white-space: pre-wrap;
|
|
6250
|
+
word-break: break-word;
|
|
6251
|
+
color: transparent;
|
|
6252
|
+
pointer-events: none;
|
|
6253
|
+
overflow: hidden;
|
|
6254
|
+
border-radius: inherit;
|
|
6255
|
+
box-sizing: border-box;
|
|
6256
|
+
z-index: 2;
|
|
6257
|
+
`;
|
|
6258
|
+
const textarea = document.createElement("textarea");
|
|
6259
|
+
textarea.name = `${pathKey}__text`;
|
|
6260
|
+
textarea.placeholder = element.placeholder || t("richinputPlaceholder", state);
|
|
6261
|
+
const rawInitialText = initialValue.text ?? "";
|
|
6262
|
+
textarea.value = rawInitialText ? replaceRidsWithFilenames(rawInitialText, files, state) : "";
|
|
6263
|
+
textarea.style.cssText = `
|
|
6264
|
+
width: 100%;
|
|
6265
|
+
${TEXTAREA_PADDING}
|
|
6266
|
+
${TEXTAREA_FONT}
|
|
6267
|
+
background: transparent;
|
|
6268
|
+
border: none;
|
|
6269
|
+
outline: none;
|
|
6270
|
+
resize: none;
|
|
6271
|
+
color: var(--fb-text-color, #111827);
|
|
6272
|
+
box-sizing: border-box;
|
|
6273
|
+
position: relative;
|
|
6274
|
+
z-index: 1;
|
|
6275
|
+
caret-color: var(--fb-text-color, #111827);
|
|
6276
|
+
`;
|
|
6277
|
+
applyAutoExpand2(textarea, backdrop);
|
|
6278
|
+
textarea.addEventListener("scroll", () => {
|
|
6279
|
+
backdrop.scrollTop = textarea.scrollTop;
|
|
6280
|
+
});
|
|
6281
|
+
let mentionTooltip = null;
|
|
6282
|
+
backdrop.addEventListener("mouseover", (e) => {
|
|
6283
|
+
const mark = e.target.closest?.("mark");
|
|
6284
|
+
if (!mark?.dataset.rid) return;
|
|
6285
|
+
mentionTooltip = removePortalTooltip(mentionTooltip);
|
|
6286
|
+
mentionTooltip = showMentionTooltip(mark, mark.dataset.rid, state);
|
|
6287
|
+
});
|
|
6288
|
+
backdrop.addEventListener("mouseout", (e) => {
|
|
6289
|
+
const mark = e.target.closest?.("mark");
|
|
6290
|
+
if (!mark) return;
|
|
6291
|
+
const related = e.relatedTarget;
|
|
6292
|
+
if (related?.closest?.("mark")) return;
|
|
6293
|
+
mentionTooltip = removePortalTooltip(mentionTooltip);
|
|
6294
|
+
});
|
|
6295
|
+
backdrop.addEventListener("mousedown", (e) => {
|
|
6296
|
+
const mark = e.target.closest?.("mark");
|
|
6297
|
+
if (!mark) return;
|
|
6298
|
+
mentionTooltip = removePortalTooltip(mentionTooltip);
|
|
6299
|
+
const marks = backdrop.querySelectorAll("mark");
|
|
6300
|
+
marks.forEach((m) => m.style.pointerEvents = "none");
|
|
6301
|
+
const under = document.elementFromPoint(e.clientX, e.clientY);
|
|
6302
|
+
if (under) {
|
|
6303
|
+
under.dispatchEvent(
|
|
6304
|
+
new MouseEvent("mousedown", {
|
|
6305
|
+
bubbles: true,
|
|
6306
|
+
cancelable: true,
|
|
6307
|
+
view: window,
|
|
6308
|
+
clientX: e.clientX,
|
|
6309
|
+
clientY: e.clientY,
|
|
6310
|
+
button: e.button,
|
|
6311
|
+
buttons: e.buttons,
|
|
6312
|
+
detail: e.detail
|
|
6313
|
+
})
|
|
6314
|
+
);
|
|
6315
|
+
}
|
|
6316
|
+
document.addEventListener(
|
|
6317
|
+
"mouseup",
|
|
6318
|
+
() => {
|
|
6319
|
+
marks.forEach((m) => m.style.pointerEvents = "auto");
|
|
6320
|
+
},
|
|
6321
|
+
{ once: true }
|
|
6322
|
+
);
|
|
6323
|
+
});
|
|
6324
|
+
function updateBackdrop() {
|
|
6325
|
+
const text = textarea.value;
|
|
6326
|
+
const nameToRid = buildNameToRid(files, state);
|
|
6327
|
+
const tokens = findAtTokens(text);
|
|
6328
|
+
if (tokens.length === 0) {
|
|
6329
|
+
backdrop.innerHTML = escapeHtml(text) + "\n";
|
|
6330
|
+
return;
|
|
6331
|
+
}
|
|
6332
|
+
let html = "";
|
|
6333
|
+
let lastIdx = 0;
|
|
6334
|
+
for (const token of tokens) {
|
|
6335
|
+
html += escapeHtml(text.slice(lastIdx, token.start));
|
|
6336
|
+
const rid = nameToRid.get(token.name);
|
|
6337
|
+
if (rid) {
|
|
6338
|
+
html += `<mark data-rid="${escapeHtml(rid)}" style="background: color-mix(in srgb, var(--fb-primary-color, #0066cc) 15%, transparent); color: transparent; border-radius: 8px; padding: 0; border: none; box-shadow: 0 0 0 2px color-mix(in srgb, var(--fb-primary-color, #0066cc) 15%, transparent), 0 0 0 3px color-mix(in srgb, var(--fb-primary-color, #0066cc) 30%, transparent); box-decoration-break: clone; -webkit-box-decoration-break: clone; pointer-events: auto; cursor: text;">${escapeHtml(text.slice(token.start, token.end))}</mark>`;
|
|
6339
|
+
} else {
|
|
6340
|
+
html += `<mark style="color: transparent; background: none; padding: 0; border: none; text-decoration-line: underline; text-decoration-style: wavy; text-decoration-color: rgba(239, 68, 68, 0.45); text-underline-offset: 2px;">${escapeHtml(text.slice(token.start, token.end))}</mark>`;
|
|
6341
|
+
}
|
|
6342
|
+
lastIdx = token.end;
|
|
6343
|
+
}
|
|
6344
|
+
html += escapeHtml(text.slice(lastIdx));
|
|
6345
|
+
backdrop.innerHTML = html + "\n";
|
|
6346
|
+
}
|
|
6347
|
+
const paperclipBtn = document.createElement("button");
|
|
6348
|
+
paperclipBtn.type = "button";
|
|
6349
|
+
paperclipBtn.title = t("richinputAttachFile", state);
|
|
6350
|
+
paperclipBtn.style.cssText = `
|
|
6351
|
+
position: absolute;
|
|
6352
|
+
right: 10px;
|
|
6353
|
+
bottom: 10px;
|
|
6354
|
+
z-index: 2;
|
|
6355
|
+
width: 32px;
|
|
6356
|
+
height: 32px;
|
|
6357
|
+
border: none;
|
|
6358
|
+
border-radius: 8px;
|
|
6359
|
+
background: transparent;
|
|
6360
|
+
cursor: pointer;
|
|
6361
|
+
display: flex;
|
|
6362
|
+
align-items: center;
|
|
6363
|
+
justify-content: center;
|
|
6364
|
+
color: var(--fb-text-muted-color, #9ca3af);
|
|
6365
|
+
transition: color 0.15s, background 0.15s;
|
|
6366
|
+
`;
|
|
6367
|
+
paperclipBtn.innerHTML = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>';
|
|
6368
|
+
paperclipBtn.addEventListener("mouseenter", () => {
|
|
6369
|
+
paperclipBtn.style.color = "var(--fb-primary-color, #0066cc)";
|
|
6370
|
+
paperclipBtn.style.background = "var(--fb-background-hover-color, #f3f4f6)";
|
|
6371
|
+
});
|
|
6372
|
+
paperclipBtn.addEventListener("mouseleave", () => {
|
|
6373
|
+
paperclipBtn.style.color = "var(--fb-text-muted-color, #9ca3af)";
|
|
6374
|
+
paperclipBtn.style.background = "transparent";
|
|
6375
|
+
});
|
|
6376
|
+
paperclipBtn.addEventListener("click", () => {
|
|
6377
|
+
const maxFiles = element.maxFiles ?? Infinity;
|
|
6378
|
+
if (files.length < maxFiles) {
|
|
6379
|
+
fileInput.click();
|
|
6380
|
+
}
|
|
6381
|
+
});
|
|
6382
|
+
const dropdown = document.createElement("div");
|
|
6383
|
+
dropdown.className = "fb-richinput-dropdown";
|
|
6384
|
+
dropdown.style.cssText = `
|
|
6385
|
+
display: none;
|
|
6386
|
+
position: absolute;
|
|
6387
|
+
bottom: 100%;
|
|
6388
|
+
left: 0;
|
|
6389
|
+
z-index: 1000;
|
|
6390
|
+
background: #fff;
|
|
6391
|
+
border: 1px solid var(--fb-border-color, #d1d5db);
|
|
6392
|
+
border-radius: var(--fb-border-radius, 6px);
|
|
6393
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.12);
|
|
6394
|
+
min-width: 180px;
|
|
6395
|
+
max-width: 320px;
|
|
6396
|
+
max-height: 200px;
|
|
6397
|
+
overflow-y: auto;
|
|
6398
|
+
margin-bottom: 4px;
|
|
6399
|
+
${TEXTAREA_FONT}
|
|
6400
|
+
`;
|
|
6401
|
+
function buildFileLabelsFromClosure() {
|
|
6402
|
+
return buildFileLabels(files, state);
|
|
6403
|
+
}
|
|
6404
|
+
function renderDropdownItems(filtered) {
|
|
6405
|
+
clear(dropdown);
|
|
6406
|
+
const labels = buildFileLabelsFromClosure();
|
|
6407
|
+
if (filtered.length === 0) {
|
|
6408
|
+
dropdown.style.display = "none";
|
|
6409
|
+
dropdownState.open = false;
|
|
6410
|
+
return;
|
|
6411
|
+
}
|
|
6412
|
+
filtered.forEach((rid, idx) => {
|
|
6413
|
+
const meta = state.resourceIndex.get(rid);
|
|
6414
|
+
const item = document.createElement("div");
|
|
6415
|
+
item.className = "fb-richinput-dropdown-item";
|
|
6416
|
+
item.dataset.rid = rid;
|
|
6417
|
+
item.style.cssText = `
|
|
6418
|
+
padding: 5px 10px;
|
|
6419
|
+
cursor: pointer;
|
|
6420
|
+
color: var(--fb-text-color, #111827);
|
|
6421
|
+
background: ${idx === dropdownState.selectedIndex ? "var(--fb-background-hover-color, #f3f4f6)" : "transparent"};
|
|
6422
|
+
display: flex;
|
|
6423
|
+
align-items: center;
|
|
6424
|
+
gap: 8px;
|
|
6425
|
+
`;
|
|
6426
|
+
const thumb = document.createElement("div");
|
|
6427
|
+
thumb.style.cssText = "width: 24px; height: 24px; border-radius: 4px; overflow: hidden; flex-shrink: 0; background: var(--fb-background-hover-color, #f3f4f6); display: flex; align-items: center; justify-content: center;";
|
|
6428
|
+
if (meta?.file && isImageMeta(meta)) {
|
|
6429
|
+
const img = document.createElement("img");
|
|
6430
|
+
img.style.cssText = "width: 100%; height: 100%; object-fit: cover; display: block;";
|
|
6431
|
+
const reader = new FileReader();
|
|
6432
|
+
reader.onload = (ev) => {
|
|
6433
|
+
img.src = ev.target?.result || "";
|
|
6434
|
+
};
|
|
6435
|
+
reader.readAsDataURL(meta.file);
|
|
6436
|
+
thumb.appendChild(img);
|
|
6437
|
+
} else if (state.config.getThumbnail) {
|
|
6438
|
+
state.config.getThumbnail(rid).then((url) => {
|
|
6439
|
+
if (!url || !thumb.isConnected) return;
|
|
6440
|
+
const img = document.createElement("img");
|
|
6441
|
+
img.style.cssText = "width: 100%; height: 100%; object-fit: cover; display: block;";
|
|
6442
|
+
img.src = url;
|
|
6443
|
+
clear(thumb);
|
|
6444
|
+
thumb.appendChild(img);
|
|
6445
|
+
}).catch(() => {
|
|
6446
|
+
});
|
|
6447
|
+
thumb.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>';
|
|
6448
|
+
} else {
|
|
6449
|
+
thumb.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>';
|
|
6450
|
+
}
|
|
6451
|
+
item.appendChild(thumb);
|
|
6452
|
+
const nameSpan = document.createElement("span");
|
|
6453
|
+
nameSpan.style.cssText = "overflow: hidden; text-overflow: ellipsis; white-space: nowrap;";
|
|
6454
|
+
nameSpan.textContent = labels.get(rid) ?? rid;
|
|
6455
|
+
item.appendChild(nameSpan);
|
|
6456
|
+
dropdown.appendChild(item);
|
|
6457
|
+
});
|
|
6458
|
+
dropdown.onmousemove = (e) => {
|
|
6459
|
+
const target = e.target.closest?.(
|
|
6460
|
+
".fb-richinput-dropdown-item"
|
|
6461
|
+
);
|
|
6462
|
+
if (!target) return;
|
|
6463
|
+
const newIdx = filtered.indexOf(target.dataset.rid ?? "");
|
|
6464
|
+
if (newIdx === -1 || newIdx === dropdownState.selectedIndex) return;
|
|
6465
|
+
const items = dropdown.querySelectorAll(
|
|
6466
|
+
".fb-richinput-dropdown-item"
|
|
6467
|
+
);
|
|
6468
|
+
items.forEach((el, i) => {
|
|
6469
|
+
el.style.background = i === newIdx ? "var(--fb-background-hover-color, #f3f4f6)" : "transparent";
|
|
6470
|
+
});
|
|
6471
|
+
dropdownState.selectedIndex = newIdx;
|
|
6472
|
+
};
|
|
6473
|
+
dropdown.onmousedown = (e) => {
|
|
6474
|
+
e.preventDefault();
|
|
6475
|
+
e.stopPropagation();
|
|
6476
|
+
const target = e.target.closest?.(
|
|
6477
|
+
".fb-richinput-dropdown-item"
|
|
6478
|
+
);
|
|
6479
|
+
if (!target?.dataset.rid) return;
|
|
6480
|
+
insertMention(target.dataset.rid);
|
|
6481
|
+
};
|
|
6482
|
+
dropdown.style.display = "block";
|
|
6483
|
+
dropdownState.open = true;
|
|
6484
|
+
}
|
|
6485
|
+
function openDropdown() {
|
|
6486
|
+
const trigger = getAtTrigger(textarea);
|
|
6487
|
+
if (!trigger) {
|
|
6488
|
+
closeDropdown();
|
|
6489
|
+
return;
|
|
6490
|
+
}
|
|
6491
|
+
dropdownState.query = trigger.query;
|
|
6492
|
+
dropdownState.triggerPos = trigger.pos;
|
|
6493
|
+
dropdownState.selectedIndex = 0;
|
|
6494
|
+
const labels = buildFileLabelsFromClosure();
|
|
6495
|
+
const filtered = filterFilesForDropdown(trigger.query, files, labels);
|
|
6496
|
+
renderDropdownItems(filtered);
|
|
6497
|
+
}
|
|
6498
|
+
function closeDropdown() {
|
|
6499
|
+
dropdown.style.display = "none";
|
|
6500
|
+
dropdownState.open = false;
|
|
6501
|
+
}
|
|
6502
|
+
function insertMention(rid) {
|
|
6503
|
+
const labels = buildFileLabelsFromClosure();
|
|
6504
|
+
const label = labels.get(rid) ?? state.resourceIndex.get(rid)?.name ?? rid;
|
|
6505
|
+
const cursorPos = textarea.selectionStart ?? 0;
|
|
6506
|
+
const before = textarea.value.slice(0, dropdownState.triggerPos);
|
|
6507
|
+
const after = textarea.value.slice(cursorPos);
|
|
6508
|
+
const mention = `${formatMention(label)} `;
|
|
6509
|
+
textarea.value = `${before}${mention}${after}`;
|
|
6510
|
+
const newPos = before.length + mention.length;
|
|
6511
|
+
textarea.setSelectionRange(newPos, newPos);
|
|
6512
|
+
textarea.dispatchEvent(new Event("input"));
|
|
6513
|
+
closeDropdown();
|
|
6514
|
+
}
|
|
6515
|
+
textarea.addEventListener("input", () => {
|
|
6516
|
+
updateBackdrop();
|
|
6517
|
+
writeHidden();
|
|
6518
|
+
ctx.instance?.triggerOnChange(pathKey, getCurrentValue());
|
|
6519
|
+
if (files.length > 0) {
|
|
6520
|
+
openDropdown();
|
|
6521
|
+
} else {
|
|
6522
|
+
closeDropdown();
|
|
6523
|
+
}
|
|
6524
|
+
});
|
|
6525
|
+
function updateDropdownHighlight() {
|
|
6526
|
+
const items = dropdown.querySelectorAll(
|
|
6527
|
+
".fb-richinput-dropdown-item"
|
|
6528
|
+
);
|
|
6529
|
+
items.forEach((el, i) => {
|
|
6530
|
+
el.style.background = i === dropdownState.selectedIndex ? "var(--fb-background-hover-color, #f3f4f6)" : "transparent";
|
|
6531
|
+
});
|
|
6532
|
+
}
|
|
6533
|
+
textarea.addEventListener("keydown", (e) => {
|
|
6534
|
+
if (!dropdownState.open) return;
|
|
6535
|
+
const labels = buildFileLabelsFromClosure();
|
|
6536
|
+
const filtered = filterFilesForDropdown(
|
|
6537
|
+
dropdownState.query,
|
|
6538
|
+
files,
|
|
6539
|
+
labels
|
|
6540
|
+
);
|
|
6541
|
+
if (e.key === "ArrowDown") {
|
|
6542
|
+
e.preventDefault();
|
|
6543
|
+
dropdownState.selectedIndex = Math.min(
|
|
6544
|
+
dropdownState.selectedIndex + 1,
|
|
6545
|
+
filtered.length - 1
|
|
6546
|
+
);
|
|
6547
|
+
updateDropdownHighlight();
|
|
6548
|
+
} else if (e.key === "ArrowUp") {
|
|
6549
|
+
e.preventDefault();
|
|
6550
|
+
dropdownState.selectedIndex = Math.max(
|
|
6551
|
+
dropdownState.selectedIndex - 1,
|
|
6552
|
+
0
|
|
6553
|
+
);
|
|
6554
|
+
updateDropdownHighlight();
|
|
6555
|
+
} else if (e.key === "Enter" && filtered.length > 0) {
|
|
6556
|
+
e.preventDefault();
|
|
6557
|
+
insertMention(filtered[dropdownState.selectedIndex]);
|
|
6558
|
+
} else if (e.key === "Escape") {
|
|
6559
|
+
closeDropdown();
|
|
6560
|
+
}
|
|
6561
|
+
});
|
|
6562
|
+
document.addEventListener(
|
|
6563
|
+
"click",
|
|
6564
|
+
(e) => {
|
|
6565
|
+
if (!outerDiv.contains(e.target) && !dropdown.contains(e.target)) {
|
|
6566
|
+
closeDropdown();
|
|
6567
|
+
}
|
|
6568
|
+
},
|
|
6569
|
+
{ signal: docListenerCtrl.signal }
|
|
6570
|
+
);
|
|
6571
|
+
function renderFilesRow() {
|
|
6572
|
+
clear(filesRow);
|
|
6573
|
+
if (files.length === 0) {
|
|
6574
|
+
filesRow.style.display = "none";
|
|
6575
|
+
return;
|
|
6576
|
+
}
|
|
6577
|
+
filesRow.style.display = "flex";
|
|
6578
|
+
files.forEach((rid) => {
|
|
6579
|
+
const meta = state.resourceIndex.get(rid);
|
|
6580
|
+
const thumbWrapper = document.createElement("div");
|
|
6581
|
+
thumbWrapper.className = "fb-richinput-file-thumb";
|
|
6582
|
+
thumbWrapper.style.cssText = `
|
|
6583
|
+
position: relative;
|
|
6584
|
+
width: 48px;
|
|
6585
|
+
height: 48px;
|
|
6586
|
+
border: 1px solid var(--fb-border-color, #d1d5db);
|
|
6587
|
+
border-radius: 8px;
|
|
6588
|
+
overflow: hidden;
|
|
6589
|
+
flex-shrink: 0;
|
|
6590
|
+
cursor: pointer;
|
|
6591
|
+
background: #fff;
|
|
6592
|
+
`;
|
|
6593
|
+
const thumbInner = document.createElement("div");
|
|
6594
|
+
thumbInner.style.cssText = "width: 48px; height: 48px; border-radius: inherit; overflow: hidden;";
|
|
6595
|
+
renderThumbContent(thumbInner, rid, meta, state);
|
|
6596
|
+
thumbWrapper.appendChild(thumbInner);
|
|
6597
|
+
const tooltipHandle = createTooltipHandle();
|
|
6598
|
+
const doMention = () => {
|
|
6599
|
+
const cursorPos = textarea.selectionStart ?? textarea.value.length;
|
|
6600
|
+
const labels = buildFileLabelsFromClosure();
|
|
6601
|
+
const label = labels.get(rid) ?? meta?.name ?? rid;
|
|
6602
|
+
const before = textarea.value.slice(0, cursorPos);
|
|
6603
|
+
const after = textarea.value.slice(cursorPos);
|
|
6604
|
+
const prefix = before.length > 0 && !/[\s\n]$/.test(before) ? "\n" : "";
|
|
6605
|
+
const mention = `${prefix}${formatMention(label)} `;
|
|
6606
|
+
textarea.value = `${before}${mention}${after}`;
|
|
6607
|
+
const newPos = cursorPos + mention.length;
|
|
6608
|
+
textarea.setSelectionRange(newPos, newPos);
|
|
6609
|
+
textarea.focus();
|
|
6610
|
+
textarea.dispatchEvent(new Event("input"));
|
|
6611
|
+
};
|
|
6612
|
+
const doRemove = () => {
|
|
6613
|
+
const idx = files.indexOf(rid);
|
|
6614
|
+
if (idx !== -1) files.splice(idx, 1);
|
|
6615
|
+
renderFilesRow();
|
|
6616
|
+
updateBackdrop();
|
|
6617
|
+
writeHidden();
|
|
6618
|
+
ctx.instance?.triggerOnChange(pathKey, getCurrentValue());
|
|
6619
|
+
};
|
|
6620
|
+
thumbWrapper.addEventListener("mouseenter", () => {
|
|
6621
|
+
cancelHideTooltip(tooltipHandle);
|
|
6622
|
+
if (!tooltipHandle.element) {
|
|
6623
|
+
tooltipHandle.element = showFileTooltip(thumbWrapper, {
|
|
6624
|
+
rid,
|
|
6625
|
+
state,
|
|
6626
|
+
isReadonly: false,
|
|
6627
|
+
onMention: doMention,
|
|
6628
|
+
onRemove: doRemove
|
|
6629
|
+
});
|
|
6630
|
+
tooltipHandle.element.addEventListener("mouseenter", () => {
|
|
6631
|
+
cancelHideTooltip(tooltipHandle);
|
|
6632
|
+
});
|
|
6633
|
+
tooltipHandle.element.addEventListener("mouseleave", () => {
|
|
6634
|
+
scheduleHideTooltip(tooltipHandle);
|
|
6635
|
+
});
|
|
6636
|
+
}
|
|
6637
|
+
});
|
|
6638
|
+
thumbWrapper.addEventListener("mouseleave", () => {
|
|
6639
|
+
scheduleHideTooltip(tooltipHandle);
|
|
6640
|
+
});
|
|
6641
|
+
filesRow.appendChild(thumbWrapper);
|
|
6642
|
+
});
|
|
6643
|
+
}
|
|
6644
|
+
function uploadFile(file) {
|
|
6645
|
+
if (!state.config.uploadFile) return;
|
|
6646
|
+
const tempId = `temp-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
6647
|
+
state.resourceIndex.set(tempId, {
|
|
6648
|
+
name: file.name,
|
|
6649
|
+
type: file.type,
|
|
6650
|
+
size: file.size,
|
|
6651
|
+
uploadedAt: /* @__PURE__ */ new Date(),
|
|
6652
|
+
file
|
|
6653
|
+
});
|
|
6654
|
+
files.push(tempId);
|
|
6655
|
+
renderFilesRow();
|
|
6656
|
+
const thumbs = filesRow.querySelectorAll(
|
|
6657
|
+
".fb-richinput-file-thumb"
|
|
6658
|
+
);
|
|
6659
|
+
const loadingThumb = thumbs[thumbs.length - 1];
|
|
6660
|
+
if (loadingThumb) loadingThumb.style.opacity = "0.5";
|
|
6661
|
+
state.config.uploadFile(file).then((resourceId) => {
|
|
6662
|
+
const idx = files.indexOf(tempId);
|
|
6663
|
+
if (idx !== -1) files[idx] = resourceId;
|
|
6664
|
+
state.resourceIndex.delete(tempId);
|
|
6665
|
+
state.resourceIndex.set(resourceId, {
|
|
6666
|
+
name: file.name,
|
|
6667
|
+
type: file.type,
|
|
6668
|
+
size: file.size,
|
|
6669
|
+
uploadedAt: /* @__PURE__ */ new Date(),
|
|
6670
|
+
file
|
|
6671
|
+
});
|
|
6672
|
+
renderFilesRow();
|
|
6673
|
+
updateBackdrop();
|
|
6674
|
+
writeHidden();
|
|
6675
|
+
ctx.instance?.triggerOnChange(pathKey, getCurrentValue());
|
|
6676
|
+
}).catch((err) => {
|
|
6677
|
+
const idx = files.indexOf(tempId);
|
|
6678
|
+
if (idx !== -1) files.splice(idx, 1);
|
|
6679
|
+
state.resourceIndex.delete(tempId);
|
|
6680
|
+
renderFilesRow();
|
|
6681
|
+
state.config.onUploadError?.(err, file);
|
|
6682
|
+
});
|
|
6683
|
+
}
|
|
6684
|
+
fileInput.addEventListener("change", () => {
|
|
6685
|
+
const selected = fileInput.files;
|
|
6686
|
+
if (!selected || selected.length === 0) return;
|
|
6687
|
+
const maxFiles = element.maxFiles ?? Infinity;
|
|
6688
|
+
for (let i = 0; i < selected.length && files.length < maxFiles; i++) {
|
|
6689
|
+
uploadFile(selected[i]);
|
|
6690
|
+
}
|
|
6691
|
+
fileInput.value = "";
|
|
6692
|
+
});
|
|
6693
|
+
textareaArea.appendChild(backdrop);
|
|
6694
|
+
textareaArea.appendChild(textarea);
|
|
6695
|
+
textareaArea.appendChild(paperclipBtn);
|
|
6696
|
+
textareaArea.appendChild(dropdown);
|
|
6697
|
+
outerDiv.appendChild(filesRow);
|
|
6698
|
+
outerDiv.appendChild(textareaArea);
|
|
6699
|
+
if (element.minLength != null || element.maxLength != null) {
|
|
6700
|
+
const counterRow = document.createElement("div");
|
|
6701
|
+
counterRow.style.cssText = "position: relative; padding: 2px 14px 6px; text-align: right;";
|
|
6702
|
+
const counter = createCharCounter(element, textarea, false);
|
|
6703
|
+
counter.style.cssText = `
|
|
6704
|
+
position: static;
|
|
6705
|
+
display: inline-block;
|
|
6706
|
+
font-size: var(--fb-font-size-small);
|
|
6707
|
+
color: var(--fb-text-secondary-color);
|
|
6708
|
+
pointer-events: none;
|
|
6709
|
+
`;
|
|
6710
|
+
counterRow.appendChild(counter);
|
|
6711
|
+
outerDiv.appendChild(counterRow);
|
|
6712
|
+
}
|
|
6713
|
+
outerDiv.appendChild(hiddenInput);
|
|
6714
|
+
outerDiv.appendChild(fileInput);
|
|
6715
|
+
writeHidden();
|
|
6716
|
+
updateBackdrop();
|
|
6717
|
+
hiddenInput._applyExternalUpdate = (value) => {
|
|
6718
|
+
const rawText = value.text ?? "";
|
|
6719
|
+
textarea.value = rawText ? replaceRidsWithFilenames(rawText, files, state) : "";
|
|
6720
|
+
textarea.dispatchEvent(new Event("input"));
|
|
6721
|
+
files.length = 0;
|
|
6722
|
+
for (const rid of value.files) files.push(rid);
|
|
6723
|
+
renderFilesRow();
|
|
6724
|
+
updateBackdrop();
|
|
6725
|
+
writeHidden();
|
|
6726
|
+
};
|
|
6727
|
+
wrapper.appendChild(outerDiv);
|
|
6728
|
+
renderFilesRow();
|
|
6729
|
+
const observer = new MutationObserver(() => {
|
|
6730
|
+
if (!outerDiv.isConnected) {
|
|
6731
|
+
docListenerCtrl.abort();
|
|
6732
|
+
mentionTooltip = removePortalTooltip(mentionTooltip);
|
|
6733
|
+
observer.disconnect();
|
|
6734
|
+
}
|
|
6735
|
+
});
|
|
6736
|
+
if (outerDiv.parentElement) {
|
|
6737
|
+
observer.observe(outerDiv.parentElement, { childList: true });
|
|
6738
|
+
}
|
|
6739
|
+
}
|
|
6740
|
+
function renderReadonlyMode(_element, ctx, wrapper, _pathKey, value) {
|
|
6741
|
+
const state = ctx.state;
|
|
6742
|
+
const { text, files } = value;
|
|
6743
|
+
const ridToName = /* @__PURE__ */ new Map();
|
|
6744
|
+
for (const rid of files) {
|
|
6745
|
+
const meta = state.resourceIndex.get(rid);
|
|
6746
|
+
if (meta?.name) ridToName.set(rid, meta.name);
|
|
6747
|
+
}
|
|
6748
|
+
if (files.length > 0) {
|
|
6749
|
+
const filesRow = document.createElement("div");
|
|
6750
|
+
filesRow.style.cssText = "display: flex; flex-wrap: wrap; gap: 6px; padding-bottom: 8px;";
|
|
6751
|
+
files.forEach((rid) => {
|
|
6752
|
+
const meta = state.resourceIndex.get(rid);
|
|
6753
|
+
const thumbWrapper = document.createElement("div");
|
|
6754
|
+
thumbWrapper.style.cssText = `
|
|
6755
|
+
position: relative;
|
|
6756
|
+
width: 48px; height: 48px;
|
|
6757
|
+
border: 1px solid var(--fb-border-color, #d1d5db);
|
|
6758
|
+
border-radius: 8px;
|
|
6759
|
+
overflow: hidden;
|
|
6760
|
+
flex-shrink: 0;
|
|
6761
|
+
background: #fff;
|
|
6762
|
+
cursor: default;
|
|
6763
|
+
`;
|
|
6764
|
+
const thumbInner = document.createElement("div");
|
|
6765
|
+
thumbInner.style.cssText = "width: 48px; height: 48px; border-radius: inherit; overflow: hidden;";
|
|
6766
|
+
renderThumbContent(thumbInner, rid, meta, state);
|
|
6767
|
+
thumbWrapper.appendChild(thumbInner);
|
|
6768
|
+
const tooltipHandle = createTooltipHandle();
|
|
6769
|
+
thumbWrapper.addEventListener("mouseenter", () => {
|
|
6770
|
+
cancelHideTooltip(tooltipHandle);
|
|
6771
|
+
if (!tooltipHandle.element) {
|
|
6772
|
+
tooltipHandle.element = showFileTooltip(thumbWrapper, {
|
|
6773
|
+
rid,
|
|
6774
|
+
state,
|
|
6775
|
+
isReadonly: true
|
|
6776
|
+
});
|
|
6777
|
+
tooltipHandle.element.addEventListener("mouseenter", () => {
|
|
6778
|
+
cancelHideTooltip(tooltipHandle);
|
|
6779
|
+
});
|
|
6780
|
+
tooltipHandle.element.addEventListener("mouseleave", () => {
|
|
6781
|
+
scheduleHideTooltip(tooltipHandle);
|
|
6782
|
+
});
|
|
6783
|
+
}
|
|
6784
|
+
});
|
|
6785
|
+
thumbWrapper.addEventListener("mouseleave", () => {
|
|
6786
|
+
scheduleHideTooltip(tooltipHandle);
|
|
6787
|
+
});
|
|
6788
|
+
filesRow.appendChild(thumbWrapper);
|
|
6789
|
+
});
|
|
6790
|
+
wrapper.appendChild(filesRow);
|
|
6791
|
+
}
|
|
6792
|
+
if (text) {
|
|
6793
|
+
const textDiv = document.createElement("div");
|
|
6794
|
+
textDiv.style.cssText = `
|
|
6795
|
+
${TEXTAREA_FONT}
|
|
6796
|
+
color: var(--fb-text-color, #111827);
|
|
6797
|
+
white-space: pre-wrap;
|
|
6798
|
+
word-break: break-word;
|
|
6799
|
+
`;
|
|
6800
|
+
const tokens = findAtTokens(text);
|
|
6801
|
+
const resolvedTokens = tokens.filter(
|
|
6802
|
+
(tok) => ridToName.has(tok.name) || [...ridToName.values()].includes(tok.name)
|
|
6803
|
+
);
|
|
6804
|
+
if (resolvedTokens.length === 0) {
|
|
6805
|
+
textDiv.textContent = text;
|
|
6806
|
+
} else {
|
|
6807
|
+
let lastIndex = 0;
|
|
6808
|
+
for (const token of resolvedTokens) {
|
|
6809
|
+
if (token.start > lastIndex) {
|
|
6810
|
+
textDiv.appendChild(
|
|
6811
|
+
document.createTextNode(text.slice(lastIndex, token.start))
|
|
6812
|
+
);
|
|
6813
|
+
}
|
|
6814
|
+
const span = document.createElement("span");
|
|
6815
|
+
span.style.cssText = `
|
|
6816
|
+
display: inline;
|
|
6817
|
+
background: color-mix(in srgb, var(--fb-primary-color, #0066cc) 15%, transparent);
|
|
6818
|
+
color: var(--fb-primary-color, #0066cc);
|
|
6819
|
+
border-radius: 8px;
|
|
6820
|
+
padding: 1px 6px;
|
|
6821
|
+
font-weight: 500;
|
|
6822
|
+
cursor: default;
|
|
6823
|
+
`;
|
|
6824
|
+
const rid = ridToName.has(token.name) ? token.name : [...ridToName.entries()].find(([, n]) => n === token.name)?.[0];
|
|
6825
|
+
const displayName = ridToName.get(token.name) ?? token.name;
|
|
6826
|
+
span.textContent = `@${displayName}`;
|
|
6827
|
+
if (rid) {
|
|
6828
|
+
let spanTooltip = null;
|
|
6829
|
+
const mentionRid = rid;
|
|
6830
|
+
span.addEventListener("mouseenter", () => {
|
|
6831
|
+
spanTooltip = removePortalTooltip(spanTooltip);
|
|
6832
|
+
spanTooltip = showMentionTooltip(span, mentionRid, state);
|
|
6833
|
+
});
|
|
6834
|
+
span.addEventListener("mouseleave", () => {
|
|
6835
|
+
spanTooltip = removePortalTooltip(spanTooltip);
|
|
6836
|
+
});
|
|
6837
|
+
}
|
|
6838
|
+
textDiv.appendChild(span);
|
|
6839
|
+
lastIndex = token.end;
|
|
6840
|
+
}
|
|
6841
|
+
if (lastIndex < text.length) {
|
|
6842
|
+
textDiv.appendChild(document.createTextNode(text.slice(lastIndex)));
|
|
6843
|
+
}
|
|
6844
|
+
}
|
|
6845
|
+
wrapper.appendChild(textDiv);
|
|
6846
|
+
}
|
|
6847
|
+
if (!text && files.length === 0) {
|
|
6848
|
+
const empty = document.createElement("div");
|
|
6849
|
+
empty.style.cssText = "color: var(--fb-text-muted-color, #6b7280); font-size: var(--fb-font-size, 14px);";
|
|
6850
|
+
empty.textContent = "\u2014";
|
|
6851
|
+
wrapper.appendChild(empty);
|
|
6852
|
+
}
|
|
6853
|
+
}
|
|
6854
|
+
function renderRichInputElement(element, ctx, wrapper, pathKey) {
|
|
6855
|
+
const state = ctx.state;
|
|
6856
|
+
const textKey = element.textKey ?? "text";
|
|
6857
|
+
const filesKey = element.filesKey ?? "files";
|
|
6858
|
+
const rawPrefill = ctx.prefill[element.key];
|
|
6859
|
+
let initialValue;
|
|
6860
|
+
if (rawPrefill && typeof rawPrefill === "object" && !Array.isArray(rawPrefill)) {
|
|
6861
|
+
const obj = rawPrefill;
|
|
6862
|
+
const textVal = obj[textKey] ?? obj["text"];
|
|
6863
|
+
const filesVal = obj[filesKey] ?? obj["files"];
|
|
6864
|
+
initialValue = {
|
|
6865
|
+
text: typeof textVal === "string" ? textVal : null,
|
|
6866
|
+
files: Array.isArray(filesVal) ? filesVal : []
|
|
6867
|
+
};
|
|
6868
|
+
} else if (typeof rawPrefill === "string") {
|
|
6869
|
+
initialValue = { text: rawPrefill || null, files: [] };
|
|
6870
|
+
} else {
|
|
6871
|
+
initialValue = { text: null, files: [] };
|
|
6872
|
+
}
|
|
6873
|
+
for (const rid of initialValue.files) {
|
|
6874
|
+
if (!state.resourceIndex.has(rid)) {
|
|
6875
|
+
state.resourceIndex.set(rid, {
|
|
6876
|
+
name: rid,
|
|
6877
|
+
type: "application/octet-stream",
|
|
6878
|
+
size: 0,
|
|
6879
|
+
uploadedAt: /* @__PURE__ */ new Date(),
|
|
6880
|
+
file: void 0
|
|
6881
|
+
});
|
|
6882
|
+
}
|
|
6883
|
+
}
|
|
6884
|
+
if (state.config.readonly) {
|
|
6885
|
+
renderReadonlyMode(element, ctx, wrapper, pathKey, initialValue);
|
|
6886
|
+
} else {
|
|
6887
|
+
if (!state.config.uploadFile) {
|
|
6888
|
+
throw new Error(
|
|
6889
|
+
`RichInput field "${element.key}" requires uploadFile handler in config`
|
|
6890
|
+
);
|
|
6891
|
+
}
|
|
6892
|
+
renderEditMode(element, ctx, wrapper, pathKey, initialValue);
|
|
6893
|
+
}
|
|
6894
|
+
}
|
|
6895
|
+
function validateRichInputElement(element, key, context) {
|
|
6896
|
+
const { scopeRoot, state, skipValidation } = context;
|
|
6897
|
+
const errors = [];
|
|
6898
|
+
const textKey = element.textKey ?? "text";
|
|
6899
|
+
const filesKey = element.filesKey ?? "files";
|
|
6900
|
+
const hiddenInput = scopeRoot.querySelector(
|
|
6901
|
+
`[name="${key}"]`
|
|
6902
|
+
);
|
|
6903
|
+
if (!hiddenInput) {
|
|
6904
|
+
return { value: null, errors };
|
|
6905
|
+
}
|
|
6906
|
+
let rawValue = {};
|
|
6907
|
+
try {
|
|
6908
|
+
const parsed = JSON.parse(hiddenInput.value);
|
|
6909
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
6910
|
+
rawValue = parsed;
|
|
6911
|
+
} else {
|
|
6912
|
+
errors.push(`${key}: invalid richinput data`);
|
|
6913
|
+
return { value: null, errors };
|
|
6914
|
+
}
|
|
6915
|
+
} catch {
|
|
6916
|
+
errors.push(`${key}: invalid richinput data`);
|
|
6917
|
+
return { value: null, errors };
|
|
6918
|
+
}
|
|
6919
|
+
const textVal = rawValue[textKey];
|
|
6920
|
+
const filesVal = rawValue[filesKey];
|
|
6921
|
+
const text = textVal === null || typeof textVal === "string" ? textVal : null;
|
|
6922
|
+
const files = Array.isArray(filesVal) ? filesVal : [];
|
|
6923
|
+
const value = {
|
|
6924
|
+
[textKey]: text ?? null,
|
|
6925
|
+
[filesKey]: files
|
|
6926
|
+
};
|
|
6927
|
+
if (!skipValidation) {
|
|
6928
|
+
const textEmpty = !text || text.trim() === "";
|
|
6929
|
+
const filesEmpty = files.length === 0;
|
|
6930
|
+
if (element.required && textEmpty && filesEmpty) {
|
|
6931
|
+
errors.push(`${key}: ${t("required", state)}`);
|
|
6932
|
+
}
|
|
6933
|
+
if (!textEmpty && text) {
|
|
6934
|
+
if (element.minLength != null && text.length < element.minLength) {
|
|
6935
|
+
errors.push(
|
|
6936
|
+
`${key}: ${t("minLength", state, { min: element.minLength })}`
|
|
6937
|
+
);
|
|
6938
|
+
}
|
|
6939
|
+
if (element.maxLength != null && text.length > element.maxLength) {
|
|
6940
|
+
errors.push(
|
|
6941
|
+
`${key}: ${t("maxLength", state, { max: element.maxLength })}`
|
|
6942
|
+
);
|
|
6943
|
+
}
|
|
6944
|
+
}
|
|
6945
|
+
if (element.maxFiles != null && files.length > element.maxFiles) {
|
|
6946
|
+
errors.push(
|
|
6947
|
+
`${key}: ${t("maxFiles", state, { max: element.maxFiles })}`
|
|
6948
|
+
);
|
|
6949
|
+
}
|
|
6950
|
+
}
|
|
6951
|
+
return { value, errors };
|
|
6952
|
+
}
|
|
6953
|
+
function updateRichInputField(element, fieldPath, value, context) {
|
|
6954
|
+
const { scopeRoot } = context;
|
|
6955
|
+
const hiddenInput = scopeRoot.querySelector(
|
|
6956
|
+
`[name="${fieldPath}"]`
|
|
6957
|
+
);
|
|
6958
|
+
if (!hiddenInput) {
|
|
6959
|
+
console.warn(
|
|
6960
|
+
`updateRichInputField: no hidden input found for "${fieldPath}". Re-render to reflect new data.`
|
|
6961
|
+
);
|
|
6962
|
+
return;
|
|
6963
|
+
}
|
|
6964
|
+
let normalized = null;
|
|
6965
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
6966
|
+
const obj = value;
|
|
6967
|
+
const textKey = element.textKey ?? "text";
|
|
6968
|
+
const filesKey = element.filesKey ?? "files";
|
|
6969
|
+
const textVal = obj[textKey] ?? obj["text"];
|
|
6970
|
+
const filesVal = obj[filesKey] ?? obj["files"];
|
|
6971
|
+
if (textVal !== void 0 || filesVal !== void 0) {
|
|
6972
|
+
normalized = {
|
|
6973
|
+
text: typeof textVal === "string" ? textVal : null,
|
|
6974
|
+
files: Array.isArray(filesVal) ? filesVal : []
|
|
6975
|
+
};
|
|
6976
|
+
}
|
|
6977
|
+
}
|
|
6978
|
+
if (normalized && hiddenInput._applyExternalUpdate) {
|
|
6979
|
+
hiddenInput._applyExternalUpdate(normalized);
|
|
6980
|
+
} else if (normalized) {
|
|
6981
|
+
hiddenInput.value = JSON.stringify(normalized);
|
|
6982
|
+
}
|
|
6983
|
+
}
|
|
6984
|
+
|
|
6985
|
+
// src/components/index.ts
|
|
6986
|
+
function showTooltip(tooltipId, button) {
|
|
6987
|
+
const tooltip = document.getElementById(tooltipId);
|
|
6988
|
+
if (!tooltip) return;
|
|
6989
|
+
const isCurrentlyVisible = !tooltip.classList.contains("hidden");
|
|
6990
|
+
document.querySelectorAll('[id^="tooltip-"]').forEach((t2) => {
|
|
6991
|
+
t2.classList.add("hidden");
|
|
6992
|
+
});
|
|
6993
|
+
if (isCurrentlyVisible) {
|
|
6994
|
+
return;
|
|
6995
|
+
}
|
|
6996
|
+
const rect = button.getBoundingClientRect();
|
|
6997
|
+
const viewportWidth = window.innerWidth;
|
|
6998
|
+
const viewportHeight = window.innerHeight;
|
|
6999
|
+
if (tooltip && tooltip.parentElement !== document.body) {
|
|
7000
|
+
document.body.appendChild(tooltip);
|
|
7001
|
+
}
|
|
7002
|
+
tooltip.style.visibility = "hidden";
|
|
7003
|
+
tooltip.style.position = "fixed";
|
|
7004
|
+
tooltip.classList.remove("hidden");
|
|
7005
|
+
const tooltipRect = tooltip.getBoundingClientRect();
|
|
7006
|
+
tooltip.classList.add("hidden");
|
|
7007
|
+
tooltip.style.visibility = "visible";
|
|
7008
|
+
let left = rect.left;
|
|
7009
|
+
let top = rect.bottom + 5;
|
|
7010
|
+
if (left + tooltipRect.width > viewportWidth) {
|
|
7011
|
+
left = rect.right - tooltipRect.width;
|
|
7012
|
+
}
|
|
7013
|
+
if (top + tooltipRect.height > viewportHeight) {
|
|
7014
|
+
top = rect.top - tooltipRect.height - 5;
|
|
7015
|
+
}
|
|
7016
|
+
if (left < 10) {
|
|
7017
|
+
left = 10;
|
|
7018
|
+
}
|
|
7019
|
+
if (top < 10) {
|
|
7020
|
+
top = rect.bottom + 5;
|
|
7021
|
+
}
|
|
7022
|
+
tooltip.style.left = `${left}px`;
|
|
7023
|
+
tooltip.style.top = `${top}px`;
|
|
7024
|
+
tooltip.classList.remove("hidden");
|
|
7025
|
+
setTimeout(() => {
|
|
7026
|
+
tooltip.classList.add("hidden");
|
|
7027
|
+
}, 25e3);
|
|
7028
|
+
}
|
|
7029
|
+
if (typeof document !== "undefined") {
|
|
7030
|
+
document.addEventListener("click", (e) => {
|
|
7031
|
+
const target = e.target;
|
|
7032
|
+
const isInfoButton = target.closest("button") && target.closest("button").onclick;
|
|
7033
|
+
const isTooltip = target.closest('[id^="tooltip-"]');
|
|
7034
|
+
if (!isInfoButton && !isTooltip) {
|
|
7035
|
+
document.querySelectorAll('[id^="tooltip-"]').forEach((tooltip) => {
|
|
7036
|
+
tooltip.classList.add("hidden");
|
|
7037
|
+
});
|
|
7038
|
+
}
|
|
7039
|
+
});
|
|
7040
|
+
}
|
|
7041
|
+
function shouldDisableElement(element, ctx) {
|
|
7042
|
+
if (!element.enableIf) {
|
|
7043
|
+
return false;
|
|
7044
|
+
}
|
|
7045
|
+
try {
|
|
7046
|
+
const rootFormData = ctx.formData ?? ctx.prefill ?? {};
|
|
7047
|
+
const scope = element.enableIf.scope ?? "relative";
|
|
7048
|
+
const containerData = scope === "relative" && ctx.path ? ctx.prefill : void 0;
|
|
7049
|
+
const shouldEnable = evaluateEnableCondition(
|
|
7050
|
+
element.enableIf,
|
|
7051
|
+
rootFormData,
|
|
7052
|
+
containerData
|
|
7053
|
+
);
|
|
7054
|
+
return !shouldEnable;
|
|
7055
|
+
} catch (error) {
|
|
7056
|
+
console.error(
|
|
7057
|
+
`Error evaluating enableIf for field "${element.key}":`,
|
|
7058
|
+
error
|
|
7059
|
+
);
|
|
7060
|
+
}
|
|
7061
|
+
return false;
|
|
7062
|
+
}
|
|
7063
|
+
function extractDOMValue(fieldPath, formRoot) {
|
|
7064
|
+
const input = formRoot.querySelector(
|
|
7065
|
+
`[name="${fieldPath}"]`
|
|
7066
|
+
);
|
|
7067
|
+
if (!input) {
|
|
7068
|
+
return void 0;
|
|
7069
|
+
}
|
|
7070
|
+
if (input instanceof HTMLSelectElement) {
|
|
7071
|
+
return input.value;
|
|
7072
|
+
} else if (input instanceof HTMLInputElement) {
|
|
7073
|
+
if (input.type === "checkbox") {
|
|
7074
|
+
return input.checked;
|
|
7075
|
+
} else if (input.type === "radio") {
|
|
7076
|
+
const checked = formRoot.querySelector(
|
|
7077
|
+
`[name="${fieldPath}"]:checked`
|
|
7078
|
+
);
|
|
7079
|
+
return checked ? checked.value : void 0;
|
|
7080
|
+
} else {
|
|
7081
|
+
return input.value;
|
|
7082
|
+
}
|
|
7083
|
+
} else if (input instanceof HTMLTextAreaElement) {
|
|
7084
|
+
return input.value;
|
|
7085
|
+
}
|
|
7086
|
+
return void 0;
|
|
7087
|
+
}
|
|
7088
|
+
function reevaluateEnableIf(wrapper, element, ctx) {
|
|
7089
|
+
if (!element.enableIf) {
|
|
7090
|
+
return;
|
|
7091
|
+
}
|
|
7092
|
+
const formRoot = ctx.state.formRoot;
|
|
7093
|
+
if (!formRoot) {
|
|
7094
|
+
console.error(`Cannot re-evaluate enableIf: formRoot is null`);
|
|
7095
|
+
return;
|
|
7096
|
+
}
|
|
7097
|
+
const condition = element.enableIf;
|
|
7098
|
+
const scope = condition.scope ?? "relative";
|
|
7099
|
+
let rootFormData = {};
|
|
7100
|
+
const containerData = {};
|
|
7101
|
+
const effectiveScope = !ctx.path || ctx.path === "" ? "absolute" : scope;
|
|
7102
|
+
if (effectiveScope === "relative" && ctx.path) {
|
|
7103
|
+
const containerMatch = ctx.path.match(/^(.+)\[(\d+)\]$/);
|
|
7104
|
+
if (containerMatch) {
|
|
7105
|
+
const containerKey = containerMatch[1];
|
|
7106
|
+
const containerIndex = parseInt(containerMatch[2], 10);
|
|
7107
|
+
const containerItemElement = formRoot.querySelector(
|
|
7108
|
+
`[data-container-item="${containerKey}[${containerIndex}]"]`
|
|
7109
|
+
);
|
|
7110
|
+
if (containerItemElement) {
|
|
7111
|
+
const inputs = containerItemElement.querySelectorAll(
|
|
7112
|
+
"input, select, textarea"
|
|
7113
|
+
);
|
|
7114
|
+
inputs.forEach((input) => {
|
|
7115
|
+
const fieldName = input.getAttribute("name");
|
|
7116
|
+
if (fieldName) {
|
|
7117
|
+
const fieldKeyMatch = fieldName.match(/\.([^.[\]]+)$/);
|
|
7118
|
+
if (fieldKeyMatch) {
|
|
7119
|
+
const fieldKey = fieldKeyMatch[1];
|
|
7120
|
+
if (input instanceof HTMLSelectElement) {
|
|
7121
|
+
containerData[fieldKey] = input.value;
|
|
7122
|
+
} else if (input instanceof HTMLInputElement) {
|
|
7123
|
+
if (input.type === "checkbox") {
|
|
7124
|
+
containerData[fieldKey] = input.checked;
|
|
7125
|
+
} else if (input.type === "radio") {
|
|
7126
|
+
if (input.checked) {
|
|
7127
|
+
containerData[fieldKey] = input.value;
|
|
7128
|
+
}
|
|
7129
|
+
} else {
|
|
7130
|
+
containerData[fieldKey] = input.value;
|
|
7131
|
+
}
|
|
7132
|
+
} else if (input instanceof HTMLTextAreaElement) {
|
|
7133
|
+
containerData[fieldKey] = input.value;
|
|
7134
|
+
}
|
|
7135
|
+
}
|
|
7136
|
+
}
|
|
7137
|
+
});
|
|
7138
|
+
}
|
|
7139
|
+
}
|
|
7140
|
+
} else {
|
|
7141
|
+
const dependencyKey = condition.key;
|
|
7142
|
+
const dependencyValue = extractDOMValue(dependencyKey, formRoot);
|
|
7143
|
+
if (dependencyValue !== void 0) {
|
|
7144
|
+
rootFormData[dependencyKey] = dependencyValue;
|
|
7145
|
+
} else {
|
|
7146
|
+
rootFormData = ctx.formData ?? ctx.prefill;
|
|
7147
|
+
}
|
|
7148
|
+
}
|
|
7149
|
+
try {
|
|
7150
|
+
const shouldEnable = evaluateEnableCondition(
|
|
7151
|
+
condition,
|
|
7152
|
+
rootFormData,
|
|
7153
|
+
containerData
|
|
7154
|
+
);
|
|
7155
|
+
if (shouldEnable) {
|
|
7156
|
+
wrapper.style.display = "";
|
|
7157
|
+
wrapper.classList.remove("fb-field-wrapper-disabled");
|
|
7158
|
+
wrapper.removeAttribute("data-conditionally-disabled");
|
|
7159
|
+
} else {
|
|
7160
|
+
wrapper.style.display = "none";
|
|
7161
|
+
wrapper.classList.add("fb-field-wrapper-disabled");
|
|
7162
|
+
wrapper.setAttribute("data-conditionally-disabled", "true");
|
|
7163
|
+
}
|
|
7164
|
+
} catch (error) {
|
|
7165
|
+
console.error(`Error re-evaluating enableIf for field "${element.key}":`, error);
|
|
7166
|
+
}
|
|
7167
|
+
}
|
|
7168
|
+
function setupEnableIfListeners(wrapper, element, ctx) {
|
|
7169
|
+
if (!element.enableIf) {
|
|
7170
|
+
return;
|
|
7171
|
+
}
|
|
7172
|
+
const formRoot = ctx.state.formRoot;
|
|
7173
|
+
if (!formRoot) {
|
|
7174
|
+
console.error(`Cannot setup enableIf listeners: formRoot is null`);
|
|
7175
|
+
return;
|
|
7176
|
+
}
|
|
7177
|
+
const condition = element.enableIf;
|
|
7178
|
+
const scope = condition.scope ?? "relative";
|
|
7179
|
+
const dependencyKey = condition.key;
|
|
7180
|
+
let dependencyFieldPath;
|
|
7181
|
+
if (scope === "relative" && ctx.path) {
|
|
7182
|
+
dependencyFieldPath = `${ctx.path}.${dependencyKey}`;
|
|
7183
|
+
} else {
|
|
7184
|
+
dependencyFieldPath = dependencyKey;
|
|
7185
|
+
}
|
|
7186
|
+
const dependencyInput = formRoot.querySelector(
|
|
7187
|
+
`[name="${dependencyFieldPath}"]`
|
|
7188
|
+
);
|
|
7189
|
+
if (!dependencyInput) {
|
|
4915
7190
|
const observer = new MutationObserver(() => {
|
|
4916
7191
|
const input = formRoot.querySelector(
|
|
4917
7192
|
`[name="${dependencyFieldPath}"]`
|
|
@@ -5053,6 +7328,12 @@ function dispatchToRenderer(element, ctx, wrapper, pathKey) {
|
|
|
5053
7328
|
renderSingleContainerElement(element, ctx, wrapper, pathKey);
|
|
5054
7329
|
}
|
|
5055
7330
|
break;
|
|
7331
|
+
case "table":
|
|
7332
|
+
renderTableElement(element, ctx, wrapper, pathKey);
|
|
7333
|
+
break;
|
|
7334
|
+
case "richinput":
|
|
7335
|
+
renderRichInputElement(element, ctx, wrapper, pathKey);
|
|
7336
|
+
break;
|
|
5056
7337
|
default: {
|
|
5057
7338
|
const unsupported = document.createElement("div");
|
|
5058
7339
|
unsupported.className = "text-red-500 text-sm";
|
|
@@ -5143,7 +7424,17 @@ var defaultConfig = {
|
|
|
5143
7424
|
minFiles: "Minimum {min} files required",
|
|
5144
7425
|
maxFiles: "Maximum {max} files allowed",
|
|
5145
7426
|
unsupportedFieldType: "Unsupported field type: {type}",
|
|
5146
|
-
invalidOption: "Invalid option"
|
|
7427
|
+
invalidOption: "Invalid option",
|
|
7428
|
+
tableAddRow: "Add row",
|
|
7429
|
+
tableAddColumn: "Add column",
|
|
7430
|
+
tableRemoveRow: "Remove row",
|
|
7431
|
+
tableRemoveColumn: "Remove column",
|
|
7432
|
+
tableMergeCells: "Merge cells (Ctrl+M)",
|
|
7433
|
+
tableSplitCell: "Split cell (Ctrl+Shift+M)",
|
|
7434
|
+
richinputPlaceholder: "Type text...",
|
|
7435
|
+
richinputAttachFile: "Attach file",
|
|
7436
|
+
richinputMention: "Mention",
|
|
7437
|
+
richinputRemoveFile: "Remove"
|
|
5147
7438
|
},
|
|
5148
7439
|
ru: {
|
|
5149
7440
|
// UI texts
|
|
@@ -5189,7 +7480,17 @@ var defaultConfig = {
|
|
|
5189
7480
|
minFiles: "\u041C\u0438\u043D\u0438\u043C\u0443\u043C {min} \u0444\u0430\u0439\u043B\u043E\u0432",
|
|
5190
7481
|
maxFiles: "\u041C\u0430\u043A\u0441\u0438\u043C\u0443\u043C {max} \u0444\u0430\u0439\u043B\u043E\u0432",
|
|
5191
7482
|
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"
|
|
7483
|
+
invalidOption: "\u041D\u0435\u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435",
|
|
7484
|
+
tableAddRow: "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u0441\u0442\u0440\u043E\u043A\u0443",
|
|
7485
|
+
tableAddColumn: "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u0441\u0442\u043E\u043B\u0431\u0435\u0446",
|
|
7486
|
+
tableRemoveRow: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0441\u0442\u0440\u043E\u043A\u0443",
|
|
7487
|
+
tableRemoveColumn: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0441\u0442\u043E\u043B\u0431\u0435\u0446",
|
|
7488
|
+
tableMergeCells: "\u041E\u0431\u044A\u0435\u0434\u0438\u043D\u0438\u0442\u044C \u044F\u0447\u0435\u0439\u043A\u0438 (Ctrl+M)",
|
|
7489
|
+
tableSplitCell: "\u0420\u0430\u0437\u0434\u0435\u043B\u0438\u0442\u044C \u044F\u0447\u0435\u0439\u043A\u0443 (Ctrl+Shift+M)",
|
|
7490
|
+
richinputPlaceholder: "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442...",
|
|
7491
|
+
richinputAttachFile: "\u041F\u0440\u0438\u043A\u0440\u0435\u043F\u0438\u0442\u044C \u0444\u0430\u0439\u043B",
|
|
7492
|
+
richinputMention: "\u0423\u043F\u043E\u043C\u044F\u043D\u0443\u0442\u044C",
|
|
7493
|
+
richinputRemoveFile: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C"
|
|
5193
7494
|
}
|
|
5194
7495
|
},
|
|
5195
7496
|
theme: {}
|
|
@@ -5463,6 +7764,14 @@ var componentRegistry = {
|
|
|
5463
7764
|
// Deprecated type - delegates to container
|
|
5464
7765
|
validate: validateGroupElement,
|
|
5465
7766
|
update: updateGroupField
|
|
7767
|
+
},
|
|
7768
|
+
table: {
|
|
7769
|
+
validate: validateTableElement,
|
|
7770
|
+
update: updateTableField
|
|
7771
|
+
},
|
|
7772
|
+
richinput: {
|
|
7773
|
+
validate: validateRichInputElement,
|
|
7774
|
+
update: updateRichInputField
|
|
5466
7775
|
}
|
|
5467
7776
|
};
|
|
5468
7777
|
function getComponentOperations(elementType) {
|