@dmitryvim/form-builder 0.2.19 → 0.2.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/formbuilder.min.js +251 -97
- package/dist/browser/formbuilder.v0.2.21.min.js +602 -0
- package/dist/cjs/index.cjs +1644 -62
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/index.js +1590 -52
- package/dist/esm/index.js.map +1 -1
- package/dist/form-builder.js +251 -97
- package/dist/types/components/index.d.ts +2 -1
- package/dist/types/components/richinput.d.ts +4 -0
- package/dist/types/components/table.d.ts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/types/config.d.ts +12 -0
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/types/schema.d.ts +31 -9
- package/dist/types/utils/helpers.d.ts +5 -0
- package/package.json +1 -1
- package/dist/browser/formbuilder.v0.2.19.min.js +0 -448
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) {
|
|
@@ -4708,25 +4713,61 @@ function updateGroupField(element, fieldPath, value, context) {
|
|
|
4708
4713
|
}
|
|
4709
4714
|
|
|
4710
4715
|
// src/components/table.ts
|
|
4716
|
+
function isLegacyMerge(m) {
|
|
4717
|
+
return m !== null && typeof m === "object" && "row" in m && "col" in m && "rowspan" in m && "colspan" in m;
|
|
4718
|
+
}
|
|
4719
|
+
function isValidMergeShape(m) {
|
|
4720
|
+
if (m === null || typeof m !== "object") return false;
|
|
4721
|
+
const o = m;
|
|
4722
|
+
return typeof o.top === "number" && typeof o.left === "number" && typeof o.bottom === "number" && typeof o.right === "number";
|
|
4723
|
+
}
|
|
4724
|
+
function migrateMerge(m) {
|
|
4725
|
+
if (isLegacyMerge(m)) {
|
|
4726
|
+
return {
|
|
4727
|
+
top: m.row,
|
|
4728
|
+
left: m.col,
|
|
4729
|
+
bottom: m.row + m.rowspan - 1,
|
|
4730
|
+
right: m.col + m.colspan - 1
|
|
4731
|
+
};
|
|
4732
|
+
}
|
|
4733
|
+
if (isValidMergeShape(m)) return m;
|
|
4734
|
+
return null;
|
|
4735
|
+
}
|
|
4736
|
+
function migrateMerges(merges) {
|
|
4737
|
+
return merges.map(migrateMerge).filter((m) => m !== null);
|
|
4738
|
+
}
|
|
4711
4739
|
function createEmptyCells(rows, cols) {
|
|
4712
4740
|
return Array.from(
|
|
4713
4741
|
{ length: rows },
|
|
4714
4742
|
() => Array.from({ length: cols }, () => "")
|
|
4715
4743
|
);
|
|
4716
4744
|
}
|
|
4745
|
+
function validateMerges(merges, rows, cols) {
|
|
4746
|
+
for (let i = 0; i < merges.length; i++) {
|
|
4747
|
+
const a = merges[i];
|
|
4748
|
+
if (a.top < 0 || a.left < 0 || a.bottom >= rows || a.right >= cols || a.top > a.bottom || a.left > a.right)
|
|
4749
|
+
return `Merge ${i} out of bounds`;
|
|
4750
|
+
for (let j = i + 1; j < merges.length; j++) {
|
|
4751
|
+
const b = merges[j];
|
|
4752
|
+
if (a.top <= b.bottom && a.bottom >= b.top && a.left <= b.right && a.right >= b.left)
|
|
4753
|
+
return `Merges ${i} and ${j} overlap`;
|
|
4754
|
+
}
|
|
4755
|
+
}
|
|
4756
|
+
return null;
|
|
4757
|
+
}
|
|
4717
4758
|
function getShadowingMerge(row, col, merges) {
|
|
4718
4759
|
for (const m of merges) {
|
|
4719
|
-
if (m.
|
|
4760
|
+
if (m.top === row && m.left === col) {
|
|
4720
4761
|
return null;
|
|
4721
4762
|
}
|
|
4722
|
-
if (row >= m.
|
|
4763
|
+
if (row >= m.top && row <= m.bottom && col >= m.left && col <= m.right) {
|
|
4723
4764
|
return m;
|
|
4724
4765
|
}
|
|
4725
4766
|
}
|
|
4726
4767
|
return null;
|
|
4727
4768
|
}
|
|
4728
4769
|
function getMergeAt(row, col, merges) {
|
|
4729
|
-
return merges.find((m) => m.
|
|
4770
|
+
return merges.find((m) => m.top === row && m.left === col) ?? null;
|
|
4730
4771
|
}
|
|
4731
4772
|
function selectionRange(sel) {
|
|
4732
4773
|
if (!sel.anchor) return null;
|
|
@@ -4800,8 +4841,10 @@ function renderReadonlyTable(data, wrapper) {
|
|
|
4800
4841
|
const merge = getMergeAt(rIdx, cIdx, merges);
|
|
4801
4842
|
const td = document.createElement(rIdx === 0 ? "th" : "td");
|
|
4802
4843
|
if (merge) {
|
|
4803
|
-
|
|
4804
|
-
|
|
4844
|
+
const rowspan = merge.bottom - merge.top + 1;
|
|
4845
|
+
const colspan = merge.right - merge.left + 1;
|
|
4846
|
+
if (rowspan > 1) td.rowSpan = rowspan;
|
|
4847
|
+
if (colspan > 1) td.colSpan = colspan;
|
|
4805
4848
|
}
|
|
4806
4849
|
td.textContent = rowData[cIdx] ?? "";
|
|
4807
4850
|
td.style.cssText = `
|
|
@@ -4886,21 +4929,55 @@ function startCellEditing(span, r, c, getCells, persistValue, selectCell) {
|
|
|
4886
4929
|
span.addEventListener("keydown", onKeyDown);
|
|
4887
4930
|
span.addEventListener("blur", onBlur);
|
|
4888
4931
|
}
|
|
4932
|
+
function ensureSpinKeyframes() {
|
|
4933
|
+
if (document.getElementById("fb-spin-keyframes")) return;
|
|
4934
|
+
const style = document.createElement("style");
|
|
4935
|
+
style.id = "fb-spin-keyframes";
|
|
4936
|
+
style.textContent = `@keyframes fb-spin { to { transform: rotate(360deg); } }`;
|
|
4937
|
+
document.head.appendChild(style);
|
|
4938
|
+
}
|
|
4939
|
+
function showLoadingOverlay(parent, text) {
|
|
4940
|
+
ensureSpinKeyframes();
|
|
4941
|
+
const overlay = document.createElement("div");
|
|
4942
|
+
overlay.className = "fb-table-loading-overlay";
|
|
4943
|
+
overlay.style.cssText = `
|
|
4944
|
+
position: absolute; top: 0; left: 0; right: 0; bottom: 0;
|
|
4945
|
+
background: rgba(255,255,255,0.8); display: flex;
|
|
4946
|
+
align-items: center; justify-content: center; flex-direction: column;
|
|
4947
|
+
gap: 8px; z-index: 100;
|
|
4948
|
+
`;
|
|
4949
|
+
const spinner = document.createElement("div");
|
|
4950
|
+
spinner.style.cssText = `
|
|
4951
|
+
width: 24px; height: 24px; border: 3px solid var(--fb-border-color, #ccc);
|
|
4952
|
+
border-top-color: var(--fb-primary-color, #0066cc); border-radius: 50%;
|
|
4953
|
+
animation: fb-spin 0.8s linear infinite;
|
|
4954
|
+
`;
|
|
4955
|
+
const label = document.createElement("span");
|
|
4956
|
+
label.textContent = text;
|
|
4957
|
+
label.style.cssText = `font-size: var(--fb-font-size-small, 12px); color: var(--fb-text-color, #333);`;
|
|
4958
|
+
overlay.appendChild(spinner);
|
|
4959
|
+
overlay.appendChild(label);
|
|
4960
|
+
parent.appendChild(overlay);
|
|
4961
|
+
return overlay;
|
|
4962
|
+
}
|
|
4889
4963
|
function renderEditTable(element, initialData, pathKey, ctx, wrapper) {
|
|
4890
4964
|
const state = ctx.state;
|
|
4891
4965
|
const instance = ctx.instance;
|
|
4966
|
+
const mergeAllowed = element.mergeAllowed !== false;
|
|
4967
|
+
const cellsKey = element.fieldNames?.cells ?? "cells";
|
|
4968
|
+
const mergesKey = element.fieldNames?.merges ?? "merges";
|
|
4892
4969
|
const cells = initialData.cells.length > 0 ? initialData.cells.map((r) => [...r]) : createEmptyCells(element.rows ?? 3, element.columns ?? 3);
|
|
4893
4970
|
let merges = initialData.merges ? [...initialData.merges] : [];
|
|
4894
4971
|
const sel = { anchor: null, focus: null, dragging: false };
|
|
4895
4972
|
const hiddenInput = document.createElement("input");
|
|
4896
4973
|
hiddenInput.type = "hidden";
|
|
4897
4974
|
hiddenInput.name = pathKey;
|
|
4898
|
-
hiddenInput.value = JSON.stringify({ cells, merges });
|
|
4975
|
+
hiddenInput.value = JSON.stringify({ [cellsKey]: cells, [mergesKey]: merges });
|
|
4899
4976
|
wrapper.appendChild(hiddenInput);
|
|
4900
4977
|
function persistValue() {
|
|
4901
|
-
hiddenInput.value = JSON.stringify({ cells, merges });
|
|
4978
|
+
hiddenInput.value = JSON.stringify({ [cellsKey]: cells, [mergesKey]: merges });
|
|
4902
4979
|
if (instance) {
|
|
4903
|
-
instance.triggerOnChange(pathKey, { cells, merges });
|
|
4980
|
+
instance.triggerOnChange(pathKey, { [cellsKey]: cells, [mergesKey]: merges });
|
|
4904
4981
|
}
|
|
4905
4982
|
}
|
|
4906
4983
|
hiddenInput._applyExternalUpdate = (data) => {
|
|
@@ -4926,6 +5003,113 @@ function renderEditTable(element, initialData, pathKey, ctx, wrapper) {
|
|
|
4926
5003
|
table-layout: fixed;
|
|
4927
5004
|
`;
|
|
4928
5005
|
tableWrapper.appendChild(tableEl);
|
|
5006
|
+
const acceptExts = element.importAccept?.map((ext) => `.${ext.toLowerCase()}`) ?? [];
|
|
5007
|
+
async function importFile(file) {
|
|
5008
|
+
if (!state.config.parseTableFile) return;
|
|
5009
|
+
if (acceptExts.length > 0) {
|
|
5010
|
+
const ext = file.name.toLowerCase().replace(/^.*(\.[^.]+)$/, "$1");
|
|
5011
|
+
if (!acceptExts.includes(ext)) return;
|
|
5012
|
+
}
|
|
5013
|
+
const overlay = showLoadingOverlay(
|
|
5014
|
+
tableWrapper,
|
|
5015
|
+
t("tableImporting", state)
|
|
5016
|
+
);
|
|
5017
|
+
try {
|
|
5018
|
+
const result = await state.config.parseTableFile(file);
|
|
5019
|
+
const importedCells = result.cells;
|
|
5020
|
+
const importedMerges = result.merges ?? [];
|
|
5021
|
+
if (importedMerges.length > 0) {
|
|
5022
|
+
const err = validateMerges(
|
|
5023
|
+
importedMerges,
|
|
5024
|
+
importedCells.length,
|
|
5025
|
+
importedCells[0]?.length ?? 0
|
|
5026
|
+
);
|
|
5027
|
+
if (err) throw new Error(err);
|
|
5028
|
+
}
|
|
5029
|
+
cells.length = 0;
|
|
5030
|
+
importedCells.forEach((row) => cells.push([...row]));
|
|
5031
|
+
merges.length = 0;
|
|
5032
|
+
importedMerges.forEach((m) => merges.push({ ...m }));
|
|
5033
|
+
sel.anchor = null;
|
|
5034
|
+
sel.focus = null;
|
|
5035
|
+
persistValue();
|
|
5036
|
+
rebuild();
|
|
5037
|
+
} catch (e) {
|
|
5038
|
+
const errMsg = e instanceof Error ? e.message : String(e);
|
|
5039
|
+
console.error(
|
|
5040
|
+
t("tableImportError", state).replace("{error}", errMsg)
|
|
5041
|
+
);
|
|
5042
|
+
} finally {
|
|
5043
|
+
overlay.remove();
|
|
5044
|
+
}
|
|
5045
|
+
}
|
|
5046
|
+
if (element.importAccept && state.config.parseTableFile) {
|
|
5047
|
+
const importBtn = document.createElement("button");
|
|
5048
|
+
importBtn.type = "button";
|
|
5049
|
+
importBtn.title = t("tableImportFile", state);
|
|
5050
|
+
importBtn.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.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48"/></svg>`;
|
|
5051
|
+
importBtn.style.cssText = `
|
|
5052
|
+
position: absolute; top: 2px; left: 2px;
|
|
5053
|
+
width: 20px; height: 20px;
|
|
5054
|
+
padding: 0;
|
|
5055
|
+
display: flex; align-items: center; justify-content: center;
|
|
5056
|
+
color: var(--fb-text-color, #999);
|
|
5057
|
+
border: none;
|
|
5058
|
+
background: transparent;
|
|
5059
|
+
cursor: pointer;
|
|
5060
|
+
z-index: 2;
|
|
5061
|
+
`;
|
|
5062
|
+
importBtn.addEventListener("mouseenter", () => {
|
|
5063
|
+
importBtn.style.color = "var(--fb-primary-color, #0066cc)";
|
|
5064
|
+
});
|
|
5065
|
+
importBtn.addEventListener("mouseleave", () => {
|
|
5066
|
+
importBtn.style.color = "var(--fb-text-color, #999)";
|
|
5067
|
+
});
|
|
5068
|
+
const importInput = document.createElement("input");
|
|
5069
|
+
importInput.type = "file";
|
|
5070
|
+
importInput.accept = acceptExts.join(",");
|
|
5071
|
+
importInput.style.display = "none";
|
|
5072
|
+
tableWrapper.appendChild(importInput);
|
|
5073
|
+
importBtn.addEventListener("click", () => {
|
|
5074
|
+
importInput.click();
|
|
5075
|
+
});
|
|
5076
|
+
importInput.addEventListener("change", () => {
|
|
5077
|
+
const file = importInput.files?.[0];
|
|
5078
|
+
if (file) importFile(file);
|
|
5079
|
+
importInput.value = "";
|
|
5080
|
+
});
|
|
5081
|
+
tableWrapper.appendChild(importBtn);
|
|
5082
|
+
let dragCounter = 0;
|
|
5083
|
+
tableWrapper.addEventListener("dragenter", (e) => {
|
|
5084
|
+
e.preventDefault();
|
|
5085
|
+
dragCounter++;
|
|
5086
|
+
if (dragCounter === 1) {
|
|
5087
|
+
tableWrapper.style.outline = "2px dashed var(--fb-primary-color, #0066cc)";
|
|
5088
|
+
tableWrapper.style.outlineOffset = "-2px";
|
|
5089
|
+
}
|
|
5090
|
+
});
|
|
5091
|
+
tableWrapper.addEventListener("dragover", (e) => {
|
|
5092
|
+
e.preventDefault();
|
|
5093
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = "copy";
|
|
5094
|
+
});
|
|
5095
|
+
tableWrapper.addEventListener("dragleave", (e) => {
|
|
5096
|
+
e.preventDefault();
|
|
5097
|
+
dragCounter--;
|
|
5098
|
+
if (dragCounter <= 0) {
|
|
5099
|
+
dragCounter = 0;
|
|
5100
|
+
tableWrapper.style.outline = "";
|
|
5101
|
+
tableWrapper.style.outlineOffset = "";
|
|
5102
|
+
}
|
|
5103
|
+
});
|
|
5104
|
+
tableWrapper.addEventListener("drop", (e) => {
|
|
5105
|
+
e.preventDefault();
|
|
5106
|
+
dragCounter = 0;
|
|
5107
|
+
tableWrapper.style.outline = "";
|
|
5108
|
+
tableWrapper.style.outlineOffset = "";
|
|
5109
|
+
const file = e.dataTransfer?.files?.[0];
|
|
5110
|
+
if (file) importFile(file);
|
|
5111
|
+
});
|
|
5112
|
+
}
|
|
4929
5113
|
wrapper.appendChild(tableWrapper);
|
|
4930
5114
|
const contextMenu = document.createElement("div");
|
|
4931
5115
|
contextMenu.style.cssText = `
|
|
@@ -4969,6 +5153,7 @@ function renderEditTable(element, initialData, pathKey, ctx, wrapper) {
|
|
|
4969
5153
|
return btn;
|
|
4970
5154
|
}
|
|
4971
5155
|
function showContextMenu(x, y) {
|
|
5156
|
+
if (!mergeAllowed) return;
|
|
4972
5157
|
contextMenu.innerHTML = "";
|
|
4973
5158
|
contextMenu.style.display = "flex";
|
|
4974
5159
|
const range = selectionRange(sel);
|
|
@@ -5051,11 +5236,11 @@ function renderEditTable(element, initialData, pathKey, ctx, wrapper) {
|
|
|
5051
5236
|
const insertAt = afterIndex !== void 0 ? afterIndex + 1 : cells.length;
|
|
5052
5237
|
cells.splice(insertAt, 0, newRow);
|
|
5053
5238
|
merges = merges.map((m) => {
|
|
5054
|
-
if (m.
|
|
5055
|
-
return { ...m,
|
|
5239
|
+
if (m.top >= insertAt) {
|
|
5240
|
+
return { ...m, top: m.top + 1, bottom: m.bottom + 1 };
|
|
5056
5241
|
}
|
|
5057
|
-
if (m.
|
|
5058
|
-
return { ...m,
|
|
5242
|
+
if (m.top < insertAt && m.bottom >= insertAt) {
|
|
5243
|
+
return { ...m, bottom: m.bottom + 1 };
|
|
5059
5244
|
}
|
|
5060
5245
|
return m;
|
|
5061
5246
|
});
|
|
@@ -5066,19 +5251,18 @@ function renderEditTable(element, initialData, pathKey, ctx, wrapper) {
|
|
|
5066
5251
|
if (cells.length <= 1) return;
|
|
5067
5252
|
const rowToRemove = targetRow !== void 0 ? targetRow : sel.anchor ? sel.anchor.row : cells.length - 1;
|
|
5068
5253
|
merges = merges.map((m) => {
|
|
5069
|
-
|
|
5070
|
-
if (m.
|
|
5071
|
-
|
|
5072
|
-
return { ...m, row: m.row + 1, rowspan: m.rowspan - 1 };
|
|
5254
|
+
if (m.top === rowToRemove && m.bottom === rowToRemove) return null;
|
|
5255
|
+
if (m.top === rowToRemove) {
|
|
5256
|
+
return { ...m, bottom: m.bottom - 1 };
|
|
5073
5257
|
}
|
|
5074
|
-
if (
|
|
5075
|
-
return { ...m,
|
|
5258
|
+
if (m.bottom === rowToRemove) {
|
|
5259
|
+
return { ...m, bottom: m.bottom - 1 };
|
|
5076
5260
|
}
|
|
5077
|
-
if (m.
|
|
5078
|
-
return { ...m,
|
|
5261
|
+
if (m.top < rowToRemove && m.bottom > rowToRemove) {
|
|
5262
|
+
return { ...m, bottom: m.bottom - 1 };
|
|
5079
5263
|
}
|
|
5080
|
-
if (m.
|
|
5081
|
-
return { ...m,
|
|
5264
|
+
if (m.top > rowToRemove) {
|
|
5265
|
+
return { ...m, top: m.top - 1, bottom: m.bottom - 1 };
|
|
5082
5266
|
}
|
|
5083
5267
|
return m;
|
|
5084
5268
|
}).filter((m) => m !== null);
|
|
@@ -5093,11 +5277,11 @@ function renderEditTable(element, initialData, pathKey, ctx, wrapper) {
|
|
|
5093
5277
|
const insertAt = afterIndex !== void 0 ? afterIndex + 1 : cells[0]?.length ?? 0;
|
|
5094
5278
|
cells.forEach((row) => row.splice(insertAt, 0, ""));
|
|
5095
5279
|
merges = merges.map((m) => {
|
|
5096
|
-
if (m.
|
|
5097
|
-
return { ...m,
|
|
5280
|
+
if (m.left >= insertAt) {
|
|
5281
|
+
return { ...m, left: m.left + 1, right: m.right + 1 };
|
|
5098
5282
|
}
|
|
5099
|
-
if (m.
|
|
5100
|
-
return { ...m,
|
|
5283
|
+
if (m.left < insertAt && m.right >= insertAt) {
|
|
5284
|
+
return { ...m, right: m.right + 1 };
|
|
5101
5285
|
}
|
|
5102
5286
|
return m;
|
|
5103
5287
|
});
|
|
@@ -5108,19 +5292,18 @@ function renderEditTable(element, initialData, pathKey, ctx, wrapper) {
|
|
|
5108
5292
|
if (cells.length === 0 || cells[0].length <= 1) return;
|
|
5109
5293
|
const colToRemove = targetCol !== void 0 ? targetCol : sel.anchor ? sel.anchor.col : cells[0].length - 1;
|
|
5110
5294
|
merges = merges.map((m) => {
|
|
5111
|
-
|
|
5112
|
-
if (m.
|
|
5113
|
-
|
|
5114
|
-
return { ...m, col: m.col + 1, colspan: m.colspan - 1 };
|
|
5295
|
+
if (m.left === colToRemove && m.right === colToRemove) return null;
|
|
5296
|
+
if (m.left === colToRemove) {
|
|
5297
|
+
return { ...m, right: m.right - 1 };
|
|
5115
5298
|
}
|
|
5116
|
-
if (
|
|
5117
|
-
return { ...m,
|
|
5299
|
+
if (m.right === colToRemove) {
|
|
5300
|
+
return { ...m, right: m.right - 1 };
|
|
5118
5301
|
}
|
|
5119
|
-
if (m.
|
|
5120
|
-
return { ...m,
|
|
5302
|
+
if (m.left < colToRemove && m.right > colToRemove) {
|
|
5303
|
+
return { ...m, right: m.right - 1 };
|
|
5121
5304
|
}
|
|
5122
|
-
if (m.
|
|
5123
|
-
return { ...m,
|
|
5305
|
+
if (m.left > colToRemove) {
|
|
5306
|
+
return { ...m, left: m.left - 1, right: m.right - 1 };
|
|
5124
5307
|
}
|
|
5125
5308
|
return m;
|
|
5126
5309
|
}).filter((m) => m !== null);
|
|
@@ -5132,14 +5315,13 @@ function renderEditTable(element, initialData, pathKey, ctx, wrapper) {
|
|
|
5132
5315
|
rebuild();
|
|
5133
5316
|
}
|
|
5134
5317
|
function mergeCells() {
|
|
5318
|
+
if (!mergeAllowed) return;
|
|
5135
5319
|
const range = selectionRange(sel);
|
|
5136
5320
|
if (!range) return;
|
|
5137
5321
|
const { r1, c1, r2, c2 } = range;
|
|
5138
5322
|
if (r1 === r2 && c1 === c2) return;
|
|
5139
5323
|
merges = merges.filter((m) => {
|
|
5140
|
-
const
|
|
5141
|
-
const mEndCol = m.col + m.colspan - 1;
|
|
5142
|
-
const overlaps = m.row <= r2 && mEndRow >= r1 && m.col <= c2 && mEndCol >= c1;
|
|
5324
|
+
const overlaps = m.top <= r2 && m.bottom >= r1 && m.left <= c2 && m.right >= c1;
|
|
5143
5325
|
return !overlaps;
|
|
5144
5326
|
});
|
|
5145
5327
|
const anchorText = cells[r1][c1];
|
|
@@ -5151,16 +5333,24 @@ function renderEditTable(element, initialData, pathKey, ctx, wrapper) {
|
|
|
5151
5333
|
}
|
|
5152
5334
|
}
|
|
5153
5335
|
cells[r1][c1] = anchorText;
|
|
5154
|
-
|
|
5336
|
+
const newMerge = { top: r1, left: c1, bottom: r2, right: c2 };
|
|
5337
|
+
const testMerges = [...merges, newMerge];
|
|
5338
|
+
const err = validateMerges(testMerges, cells.length, cells[0]?.length ?? 0);
|
|
5339
|
+
if (err) {
|
|
5340
|
+
console.warn("Merge validation failed:", err);
|
|
5341
|
+
return;
|
|
5342
|
+
}
|
|
5343
|
+
merges.push(newMerge);
|
|
5155
5344
|
sel.anchor = { row: r1, col: c1 };
|
|
5156
5345
|
sel.focus = null;
|
|
5157
5346
|
persistValue();
|
|
5158
5347
|
rebuild();
|
|
5159
5348
|
}
|
|
5160
5349
|
function splitCell() {
|
|
5350
|
+
if (!mergeAllowed) return;
|
|
5161
5351
|
if (!sel.anchor) return;
|
|
5162
5352
|
const { row, col } = sel.anchor;
|
|
5163
|
-
const mIdx = merges.findIndex((m) => m.
|
|
5353
|
+
const mIdx = merges.findIndex((m) => m.top === row && m.left === col);
|
|
5164
5354
|
if (mIdx === -1) return;
|
|
5165
5355
|
merges.splice(mIdx, 1);
|
|
5166
5356
|
sel.focus = null;
|
|
@@ -5184,8 +5374,10 @@ function renderEditTable(element, initialData, pathKey, ctx, wrapper) {
|
|
|
5184
5374
|
td.setAttribute("data-row", String(rIdx));
|
|
5185
5375
|
td.setAttribute("data-col", String(cIdx));
|
|
5186
5376
|
if (merge) {
|
|
5187
|
-
|
|
5188
|
-
|
|
5377
|
+
const rowspan = merge.bottom - merge.top + 1;
|
|
5378
|
+
const colspan = merge.right - merge.left + 1;
|
|
5379
|
+
if (rowspan > 1) td.rowSpan = rowspan;
|
|
5380
|
+
if (colspan > 1) td.colSpan = colspan;
|
|
5189
5381
|
}
|
|
5190
5382
|
const inRange = range !== null && rIdx >= range.r1 && rIdx <= range.r2 && cIdx >= range.c1 && cIdx <= range.c2;
|
|
5191
5383
|
const isAnchor = sel.anchor !== null && sel.anchor.row === rIdx && sel.anchor.col === cIdx;
|
|
@@ -5248,6 +5440,7 @@ function renderEditTable(element, initialData, pathKey, ctx, wrapper) {
|
|
|
5248
5440
|
}
|
|
5249
5441
|
});
|
|
5250
5442
|
td.addEventListener("contextmenu", (e) => {
|
|
5443
|
+
if (!mergeAllowed) return;
|
|
5251
5444
|
const currentRange = selectionRange(sel);
|
|
5252
5445
|
const isMulti = currentRange && (currentRange.r1 !== currentRange.r2 || currentRange.c1 !== currentRange.c2);
|
|
5253
5446
|
const isMerged = sel.anchor && getMergeAt(sel.anchor.row, sel.anchor.col, merges);
|
|
@@ -5308,12 +5501,12 @@ function renderEditTable(element, initialData, pathKey, ctx, wrapper) {
|
|
|
5308
5501
|
selectCell(nr, nc);
|
|
5309
5502
|
return;
|
|
5310
5503
|
}
|
|
5311
|
-
if (e.key === "m" && e.ctrlKey && !e.shiftKey) {
|
|
5504
|
+
if (mergeAllowed && e.key === "m" && e.ctrlKey && !e.shiftKey) {
|
|
5312
5505
|
e.preventDefault();
|
|
5313
5506
|
mergeCells();
|
|
5314
5507
|
return;
|
|
5315
5508
|
}
|
|
5316
|
-
if (e.key === "M" && e.ctrlKey && e.shiftKey) {
|
|
5509
|
+
if (mergeAllowed && e.key === "M" && e.ctrlKey && e.shiftKey) {
|
|
5317
5510
|
e.preventDefault();
|
|
5318
5511
|
splitCell();
|
|
5319
5512
|
}
|
|
@@ -5656,10 +5849,49 @@ function defaultTableData(element) {
|
|
|
5656
5849
|
function isTableData(v) {
|
|
5657
5850
|
return v !== null && typeof v === "object" && "cells" in v && Array.isArray(v.cells);
|
|
5658
5851
|
}
|
|
5852
|
+
function isTableDataWithFieldNames(v, cellsKey) {
|
|
5853
|
+
return v !== null && typeof v === "object" && cellsKey in v && Array.isArray(v[cellsKey]);
|
|
5854
|
+
}
|
|
5659
5855
|
function renderTableElement(element, ctx, wrapper, pathKey) {
|
|
5660
5856
|
const state = ctx.state;
|
|
5661
5857
|
const rawPrefill = ctx.prefill[element.key];
|
|
5662
|
-
const
|
|
5858
|
+
const cellsKey = element.fieldNames?.cells ?? "cells";
|
|
5859
|
+
const mergesKey = element.fieldNames?.merges ?? "merges";
|
|
5860
|
+
let initialData;
|
|
5861
|
+
if (isTableData(rawPrefill)) {
|
|
5862
|
+
initialData = {
|
|
5863
|
+
cells: rawPrefill.cells,
|
|
5864
|
+
merges: rawPrefill.merges ? migrateMerges(rawPrefill.merges) : []
|
|
5865
|
+
};
|
|
5866
|
+
} else if (rawPrefill && isTableDataWithFieldNames(rawPrefill, cellsKey)) {
|
|
5867
|
+
const rawMerges = rawPrefill[mergesKey];
|
|
5868
|
+
initialData = {
|
|
5869
|
+
cells: rawPrefill[cellsKey],
|
|
5870
|
+
merges: rawMerges ? migrateMerges(rawMerges) : []
|
|
5871
|
+
};
|
|
5872
|
+
} else if (isTableData(element.default)) {
|
|
5873
|
+
initialData = {
|
|
5874
|
+
cells: element.default.cells,
|
|
5875
|
+
merges: element.default.merges ? migrateMerges(element.default.merges) : []
|
|
5876
|
+
};
|
|
5877
|
+
} else if (element.default && isTableDataWithFieldNames(element.default, cellsKey)) {
|
|
5878
|
+
const rawMerges = element.default[mergesKey];
|
|
5879
|
+
initialData = {
|
|
5880
|
+
cells: element.default[cellsKey],
|
|
5881
|
+
merges: rawMerges ? migrateMerges(rawMerges) : []
|
|
5882
|
+
};
|
|
5883
|
+
} else {
|
|
5884
|
+
initialData = defaultTableData(element);
|
|
5885
|
+
}
|
|
5886
|
+
if (initialData.merges && initialData.merges.length > 0) {
|
|
5887
|
+
const rows = initialData.cells.length;
|
|
5888
|
+
const cols = rows > 0 ? initialData.cells[0].length : 0;
|
|
5889
|
+
const err = validateMerges(initialData.merges, rows, cols);
|
|
5890
|
+
if (err) {
|
|
5891
|
+
console.warn(`Table "${element.key}": invalid prefill merges stripped (${err})`);
|
|
5892
|
+
initialData = { ...initialData, merges: [] };
|
|
5893
|
+
}
|
|
5894
|
+
}
|
|
5663
5895
|
if (state.config.readonly) {
|
|
5664
5896
|
renderReadonlyTable(initialData, wrapper);
|
|
5665
5897
|
} else {
|
|
@@ -5669,6 +5901,7 @@ function renderTableElement(element, ctx, wrapper, pathKey) {
|
|
|
5669
5901
|
function validateTableElement(element, key, context) {
|
|
5670
5902
|
const { scopeRoot, skipValidation } = context;
|
|
5671
5903
|
const errors = [];
|
|
5904
|
+
const cellsKey = element.fieldNames?.cells ?? "cells";
|
|
5672
5905
|
const hiddenInput = scopeRoot.querySelector(
|
|
5673
5906
|
`[name="${key}"]`
|
|
5674
5907
|
);
|
|
@@ -5683,7 +5916,8 @@ function validateTableElement(element, key, context) {
|
|
|
5683
5916
|
return { value: null, errors };
|
|
5684
5917
|
}
|
|
5685
5918
|
if (!skipValidation && element.required) {
|
|
5686
|
-
const
|
|
5919
|
+
const cells = value[cellsKey];
|
|
5920
|
+
const hasContent = cells?.some(
|
|
5687
5921
|
(row) => row.some((cell) => cell.trim() !== "")
|
|
5688
5922
|
);
|
|
5689
5923
|
if (!hasContent) {
|
|
@@ -5692,8 +5926,10 @@ function validateTableElement(element, key, context) {
|
|
|
5692
5926
|
}
|
|
5693
5927
|
return { value, errors };
|
|
5694
5928
|
}
|
|
5695
|
-
function updateTableField(
|
|
5929
|
+
function updateTableField(element, fieldPath, value, context) {
|
|
5696
5930
|
const { scopeRoot } = context;
|
|
5931
|
+
const cellsKey = element.fieldNames?.cells ?? "cells";
|
|
5932
|
+
const mergesKey = element.fieldNames?.merges ?? "merges";
|
|
5697
5933
|
const hiddenInput = scopeRoot.querySelector(
|
|
5698
5934
|
`[name="${fieldPath}"]`
|
|
5699
5935
|
);
|
|
@@ -5703,13 +5939,1293 @@ function updateTableField(_element, fieldPath, value, context) {
|
|
|
5703
5939
|
);
|
|
5704
5940
|
return;
|
|
5705
5941
|
}
|
|
5706
|
-
|
|
5707
|
-
|
|
5942
|
+
let tableData = null;
|
|
5943
|
+
if (isTableData(value)) {
|
|
5944
|
+
tableData = {
|
|
5945
|
+
cells: value.cells,
|
|
5946
|
+
merges: value.merges ? migrateMerges(value.merges) : []
|
|
5947
|
+
};
|
|
5948
|
+
} else if (value && isTableDataWithFieldNames(value, cellsKey)) {
|
|
5949
|
+
const rawMerges = value[mergesKey];
|
|
5950
|
+
tableData = {
|
|
5951
|
+
cells: value[cellsKey],
|
|
5952
|
+
merges: rawMerges ? migrateMerges(rawMerges) : []
|
|
5953
|
+
};
|
|
5954
|
+
}
|
|
5955
|
+
if (tableData && hiddenInput._applyExternalUpdate) {
|
|
5956
|
+
hiddenInput._applyExternalUpdate(tableData);
|
|
5708
5957
|
} else {
|
|
5709
5958
|
hiddenInput.value = JSON.stringify(value);
|
|
5710
5959
|
}
|
|
5711
5960
|
}
|
|
5712
5961
|
|
|
5962
|
+
// src/components/richinput.ts
|
|
5963
|
+
function applyAutoExpand2(textarea, backdrop) {
|
|
5964
|
+
textarea.style.overflow = "hidden";
|
|
5965
|
+
textarea.style.resize = "none";
|
|
5966
|
+
const lineCount = (textarea.value.match(/\n/g) || []).length + 1;
|
|
5967
|
+
textarea.rows = Math.max(3, lineCount);
|
|
5968
|
+
const resize = () => {
|
|
5969
|
+
if (!textarea.isConnected) return;
|
|
5970
|
+
textarea.style.height = "0";
|
|
5971
|
+
textarea.style.height = `${textarea.scrollHeight}px`;
|
|
5972
|
+
if (backdrop) {
|
|
5973
|
+
backdrop.style.height = `${textarea.scrollHeight}px`;
|
|
5974
|
+
}
|
|
5975
|
+
};
|
|
5976
|
+
textarea.addEventListener("input", resize);
|
|
5977
|
+
setTimeout(() => {
|
|
5978
|
+
if (textarea.isConnected) resize();
|
|
5979
|
+
}, 0);
|
|
5980
|
+
}
|
|
5981
|
+
function buildFileLabels(files, state) {
|
|
5982
|
+
const labels = /* @__PURE__ */ new Map();
|
|
5983
|
+
const nameCount = /* @__PURE__ */ new Map();
|
|
5984
|
+
for (const rid of files) {
|
|
5985
|
+
const meta = state.resourceIndex.get(rid);
|
|
5986
|
+
const name = meta?.name ?? rid;
|
|
5987
|
+
nameCount.set(name, (nameCount.get(name) ?? 0) + 1);
|
|
5988
|
+
}
|
|
5989
|
+
for (const rid of files) {
|
|
5990
|
+
const meta = state.resourceIndex.get(rid);
|
|
5991
|
+
const name = meta?.name ?? rid;
|
|
5992
|
+
if ((nameCount.get(name) ?? 1) > 1 && meta) {
|
|
5993
|
+
labels.set(rid, `${name} (${formatFileSize(meta.size)})`);
|
|
5994
|
+
} else {
|
|
5995
|
+
labels.set(rid, name);
|
|
5996
|
+
}
|
|
5997
|
+
}
|
|
5998
|
+
return labels;
|
|
5999
|
+
}
|
|
6000
|
+
function isImageMeta(meta) {
|
|
6001
|
+
if (!meta) return false;
|
|
6002
|
+
return meta.type.startsWith("image/");
|
|
6003
|
+
}
|
|
6004
|
+
function buildNameToRid(files, state) {
|
|
6005
|
+
const labels = buildFileLabels(files, state);
|
|
6006
|
+
const map = /* @__PURE__ */ new Map();
|
|
6007
|
+
for (const rid of files) {
|
|
6008
|
+
const label = labels.get(rid);
|
|
6009
|
+
if (label) map.set(label, rid);
|
|
6010
|
+
}
|
|
6011
|
+
return map;
|
|
6012
|
+
}
|
|
6013
|
+
function formatMention(name) {
|
|
6014
|
+
if (/\s/.test(name) || name.includes('"')) {
|
|
6015
|
+
return `@"${name.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
6016
|
+
}
|
|
6017
|
+
return `@${name}`;
|
|
6018
|
+
}
|
|
6019
|
+
function findAtTokens(text) {
|
|
6020
|
+
const tokens = [];
|
|
6021
|
+
const len = text.length;
|
|
6022
|
+
let i = 0;
|
|
6023
|
+
while (i < len) {
|
|
6024
|
+
if (text[i] === "@") {
|
|
6025
|
+
if (i > 0 && !/\s/.test(text[i - 1])) {
|
|
6026
|
+
i++;
|
|
6027
|
+
continue;
|
|
6028
|
+
}
|
|
6029
|
+
const start = i;
|
|
6030
|
+
i++;
|
|
6031
|
+
if (i < len && text[i] === '"') {
|
|
6032
|
+
i++;
|
|
6033
|
+
let name = "";
|
|
6034
|
+
while (i < len && text[i] !== '"') {
|
|
6035
|
+
if (text[i] === "\\" && i + 1 < len) {
|
|
6036
|
+
name += text[i + 1];
|
|
6037
|
+
i += 2;
|
|
6038
|
+
} else {
|
|
6039
|
+
name += text[i];
|
|
6040
|
+
i++;
|
|
6041
|
+
}
|
|
6042
|
+
}
|
|
6043
|
+
if (i < len && text[i] === '"') {
|
|
6044
|
+
i++;
|
|
6045
|
+
if (i >= len || /\s/.test(text[i])) {
|
|
6046
|
+
tokens.push({ start, end: i, raw: text.slice(start, i), name });
|
|
6047
|
+
}
|
|
6048
|
+
}
|
|
6049
|
+
} else if (i < len && !/\s/.test(text[i])) {
|
|
6050
|
+
const wordStart = i;
|
|
6051
|
+
while (i < len && !/\s/.test(text[i])) {
|
|
6052
|
+
i++;
|
|
6053
|
+
}
|
|
6054
|
+
const name = text.slice(wordStart, i);
|
|
6055
|
+
tokens.push({ start, end: i, raw: text.slice(start, i), name });
|
|
6056
|
+
}
|
|
6057
|
+
} else {
|
|
6058
|
+
i++;
|
|
6059
|
+
}
|
|
6060
|
+
}
|
|
6061
|
+
return tokens;
|
|
6062
|
+
}
|
|
6063
|
+
function findMentions(text, nameToRid) {
|
|
6064
|
+
if (nameToRid.size === 0) return [];
|
|
6065
|
+
const tokens = findAtTokens(text);
|
|
6066
|
+
const results = [];
|
|
6067
|
+
for (const token of tokens) {
|
|
6068
|
+
const rid = nameToRid.get(token.name);
|
|
6069
|
+
if (rid) {
|
|
6070
|
+
results.push({
|
|
6071
|
+
start: token.start,
|
|
6072
|
+
end: token.end,
|
|
6073
|
+
name: token.name,
|
|
6074
|
+
rid
|
|
6075
|
+
});
|
|
6076
|
+
}
|
|
6077
|
+
}
|
|
6078
|
+
return results;
|
|
6079
|
+
}
|
|
6080
|
+
function replaceFilenamesWithRids(text, nameToRid) {
|
|
6081
|
+
const mentions = findMentions(text, nameToRid);
|
|
6082
|
+
if (mentions.length === 0) return text;
|
|
6083
|
+
let result = "";
|
|
6084
|
+
let lastIdx = 0;
|
|
6085
|
+
for (const m of mentions) {
|
|
6086
|
+
result += text.slice(lastIdx, m.start);
|
|
6087
|
+
result += `@${m.rid}`;
|
|
6088
|
+
lastIdx = m.end;
|
|
6089
|
+
}
|
|
6090
|
+
result += text.slice(lastIdx);
|
|
6091
|
+
return result;
|
|
6092
|
+
}
|
|
6093
|
+
function replaceRidsWithFilenames(text, files, state) {
|
|
6094
|
+
const labels = buildFileLabels(files, state);
|
|
6095
|
+
const ridToLabel = /* @__PURE__ */ new Map();
|
|
6096
|
+
for (const rid of files) {
|
|
6097
|
+
const label = labels.get(rid);
|
|
6098
|
+
if (label) ridToLabel.set(rid, label);
|
|
6099
|
+
}
|
|
6100
|
+
const tokens = findAtTokens(text);
|
|
6101
|
+
if (tokens.length === 0) return text;
|
|
6102
|
+
let result = "";
|
|
6103
|
+
let lastIdx = 0;
|
|
6104
|
+
for (const token of tokens) {
|
|
6105
|
+
result += text.slice(lastIdx, token.start);
|
|
6106
|
+
const label = ridToLabel.get(token.name);
|
|
6107
|
+
if (label) {
|
|
6108
|
+
result += formatMention(label);
|
|
6109
|
+
} else {
|
|
6110
|
+
result += token.raw;
|
|
6111
|
+
}
|
|
6112
|
+
lastIdx = token.end;
|
|
6113
|
+
}
|
|
6114
|
+
result += text.slice(lastIdx);
|
|
6115
|
+
return result;
|
|
6116
|
+
}
|
|
6117
|
+
function renderThumbContent(thumb, rid, meta, state) {
|
|
6118
|
+
clear(thumb);
|
|
6119
|
+
if (meta?.file && isImageMeta(meta)) {
|
|
6120
|
+
const img = document.createElement("img");
|
|
6121
|
+
img.alt = meta.name;
|
|
6122
|
+
img.style.cssText = "width: 100%; height: 100%; object-fit: cover; display: block;";
|
|
6123
|
+
const reader = new FileReader();
|
|
6124
|
+
reader.onload = (e) => {
|
|
6125
|
+
img.src = e.target?.result || "";
|
|
6126
|
+
};
|
|
6127
|
+
reader.readAsDataURL(meta.file);
|
|
6128
|
+
thumb.appendChild(img);
|
|
6129
|
+
} else if (state.config.getThumbnail) {
|
|
6130
|
+
state.config.getThumbnail(rid).then((url) => {
|
|
6131
|
+
if (!url || !thumb.isConnected) return;
|
|
6132
|
+
const img = document.createElement("img");
|
|
6133
|
+
img.alt = meta?.name ?? rid;
|
|
6134
|
+
img.src = url;
|
|
6135
|
+
img.style.cssText = "width: 100%; height: 100%; object-fit: cover; display: block;";
|
|
6136
|
+
clear(thumb);
|
|
6137
|
+
thumb.appendChild(img);
|
|
6138
|
+
}).catch((err) => {
|
|
6139
|
+
state.config.onThumbnailError?.(err, rid);
|
|
6140
|
+
});
|
|
6141
|
+
const placeholder = document.createElement("div");
|
|
6142
|
+
placeholder.style.cssText = "width: 100%; height: 100%; background: var(--fb-background-hover-color, #f3f4f6); display: flex; align-items: center; justify-content: center;";
|
|
6143
|
+
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>';
|
|
6144
|
+
thumb.appendChild(placeholder);
|
|
6145
|
+
} else {
|
|
6146
|
+
const icon = document.createElement("div");
|
|
6147
|
+
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;";
|
|
6148
|
+
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>';
|
|
6149
|
+
if (meta?.name) {
|
|
6150
|
+
const nameEl = document.createElement("span");
|
|
6151
|
+
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);";
|
|
6152
|
+
nameEl.textContent = meta.name;
|
|
6153
|
+
icon.appendChild(nameEl);
|
|
6154
|
+
}
|
|
6155
|
+
thumb.appendChild(icon);
|
|
6156
|
+
}
|
|
6157
|
+
}
|
|
6158
|
+
function renderImagePreview(hoverEl, rid, meta, state) {
|
|
6159
|
+
clear(hoverEl);
|
|
6160
|
+
if (meta?.file && isImageMeta(meta)) {
|
|
6161
|
+
const img = document.createElement("img");
|
|
6162
|
+
img.alt = meta.name;
|
|
6163
|
+
img.style.cssText = "max-width: 120px; max-height: 120px; object-fit: contain; display: block;";
|
|
6164
|
+
const reader = new FileReader();
|
|
6165
|
+
reader.onload = (e) => {
|
|
6166
|
+
img.src = e.target?.result || "";
|
|
6167
|
+
};
|
|
6168
|
+
reader.readAsDataURL(meta.file);
|
|
6169
|
+
hoverEl.appendChild(img);
|
|
6170
|
+
} else if (state.config.getThumbnail) {
|
|
6171
|
+
state.config.getThumbnail(rid).then((url) => {
|
|
6172
|
+
if (!url || !hoverEl.isConnected) return;
|
|
6173
|
+
const img = document.createElement("img");
|
|
6174
|
+
img.alt = meta?.name ?? rid;
|
|
6175
|
+
img.src = url;
|
|
6176
|
+
img.style.cssText = "max-width: 120px; max-height: 120px; object-fit: contain; display: block;";
|
|
6177
|
+
clear(hoverEl);
|
|
6178
|
+
hoverEl.appendChild(img);
|
|
6179
|
+
}).catch((err) => {
|
|
6180
|
+
state.config.onThumbnailError?.(err, rid);
|
|
6181
|
+
});
|
|
6182
|
+
}
|
|
6183
|
+
}
|
|
6184
|
+
function positionPortalTooltip(tooltip, anchor) {
|
|
6185
|
+
const rect = anchor.getBoundingClientRect();
|
|
6186
|
+
const ttRect = tooltip.getBoundingClientRect();
|
|
6187
|
+
const left = Math.max(
|
|
6188
|
+
4,
|
|
6189
|
+
Math.min(
|
|
6190
|
+
rect.left + rect.width / 2 - ttRect.width / 2,
|
|
6191
|
+
window.innerWidth - ttRect.width - 4
|
|
6192
|
+
)
|
|
6193
|
+
);
|
|
6194
|
+
const topAbove = rect.top - ttRect.height - 8;
|
|
6195
|
+
const topBelow = rect.bottom + 8;
|
|
6196
|
+
const top = topAbove >= 4 ? topAbove : topBelow;
|
|
6197
|
+
tooltip.style.left = `${left}px`;
|
|
6198
|
+
tooltip.style.top = `${Math.max(4, top)}px`;
|
|
6199
|
+
}
|
|
6200
|
+
function showMentionTooltip(anchor, rid, state) {
|
|
6201
|
+
const meta = state.resourceIndex.get(rid);
|
|
6202
|
+
const tooltip = document.createElement("div");
|
|
6203
|
+
tooltip.className = "fb-richinput-portal-tooltip fb-richinput-mention-tooltip";
|
|
6204
|
+
tooltip.style.cssText = `
|
|
6205
|
+
position: fixed;
|
|
6206
|
+
z-index: 99999;
|
|
6207
|
+
background: #fff;
|
|
6208
|
+
border: 1px solid var(--fb-border-color, #d1d5db);
|
|
6209
|
+
border-radius: 8px;
|
|
6210
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
6211
|
+
padding: 4px;
|
|
6212
|
+
pointer-events: none;
|
|
6213
|
+
min-width: 60px;
|
|
6214
|
+
max-width: 140px;
|
|
6215
|
+
`;
|
|
6216
|
+
const preview = document.createElement("div");
|
|
6217
|
+
preview.style.cssText = "min-height: 60px; max-height: 120px; display: flex; align-items: center; justify-content: center;";
|
|
6218
|
+
renderImagePreview(preview, rid, meta, state);
|
|
6219
|
+
tooltip.appendChild(preview);
|
|
6220
|
+
document.body.appendChild(tooltip);
|
|
6221
|
+
positionPortalTooltip(tooltip, anchor);
|
|
6222
|
+
return tooltip;
|
|
6223
|
+
}
|
|
6224
|
+
function showFileTooltip(anchor, opts) {
|
|
6225
|
+
const { rid, state, isReadonly, onMention, onRemove } = opts;
|
|
6226
|
+
const meta = state.resourceIndex.get(rid);
|
|
6227
|
+
const filename = meta?.name ?? rid;
|
|
6228
|
+
const tooltip = document.createElement("div");
|
|
6229
|
+
tooltip.className = "fb-richinput-portal-tooltip fb-richinput-file-tooltip";
|
|
6230
|
+
tooltip.style.cssText = `
|
|
6231
|
+
position: fixed;
|
|
6232
|
+
z-index: 99999;
|
|
6233
|
+
background: #fff;
|
|
6234
|
+
border: 1px solid var(--fb-border-color, #d1d5db);
|
|
6235
|
+
border-radius: 8px;
|
|
6236
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
6237
|
+
padding: 4px;
|
|
6238
|
+
pointer-events: auto;
|
|
6239
|
+
min-width: 80px;
|
|
6240
|
+
max-width: 260px;
|
|
6241
|
+
`;
|
|
6242
|
+
const preview = document.createElement("div");
|
|
6243
|
+
preview.style.cssText = "min-height: 60px; max-height: 120px; display: flex; align-items: center; justify-content: center;";
|
|
6244
|
+
renderImagePreview(preview, rid, meta, state);
|
|
6245
|
+
tooltip.appendChild(preview);
|
|
6246
|
+
const nameEl = document.createElement("div");
|
|
6247
|
+
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);";
|
|
6248
|
+
nameEl.textContent = filename;
|
|
6249
|
+
tooltip.appendChild(nameEl);
|
|
6250
|
+
const actionsRow = document.createElement("div");
|
|
6251
|
+
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;";
|
|
6252
|
+
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;";
|
|
6253
|
+
const btnHoverIn = (btn) => {
|
|
6254
|
+
btn.style.background = "var(--fb-background-hover-color, #f3f4f6)";
|
|
6255
|
+
btn.style.color = "var(--fb-text-color, #111827)";
|
|
6256
|
+
};
|
|
6257
|
+
const btnHoverOut = (btn) => {
|
|
6258
|
+
btn.style.background = "none";
|
|
6259
|
+
btn.style.color = "var(--fb-text-muted-color, #6b7280)";
|
|
6260
|
+
};
|
|
6261
|
+
if (!isReadonly && onMention) {
|
|
6262
|
+
const mentionBtn = document.createElement("button");
|
|
6263
|
+
mentionBtn.type = "button";
|
|
6264
|
+
mentionBtn.title = "Mention";
|
|
6265
|
+
mentionBtn.style.cssText = btnStyle;
|
|
6266
|
+
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>';
|
|
6267
|
+
mentionBtn.addEventListener("mouseenter", () => btnHoverIn(mentionBtn));
|
|
6268
|
+
mentionBtn.addEventListener("mouseleave", () => btnHoverOut(mentionBtn));
|
|
6269
|
+
mentionBtn.addEventListener("click", (e) => {
|
|
6270
|
+
e.stopPropagation();
|
|
6271
|
+
onMention();
|
|
6272
|
+
tooltip.remove();
|
|
6273
|
+
});
|
|
6274
|
+
actionsRow.appendChild(mentionBtn);
|
|
6275
|
+
}
|
|
6276
|
+
if (state.config.downloadFile) {
|
|
6277
|
+
const dlBtn = document.createElement("button");
|
|
6278
|
+
dlBtn.type = "button";
|
|
6279
|
+
dlBtn.title = "Download";
|
|
6280
|
+
dlBtn.style.cssText = btnStyle;
|
|
6281
|
+
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>';
|
|
6282
|
+
dlBtn.addEventListener("mouseenter", () => btnHoverIn(dlBtn));
|
|
6283
|
+
dlBtn.addEventListener("mouseleave", () => btnHoverOut(dlBtn));
|
|
6284
|
+
dlBtn.addEventListener("click", (e) => {
|
|
6285
|
+
e.stopPropagation();
|
|
6286
|
+
state.config.downloadFile?.(rid, filename);
|
|
6287
|
+
});
|
|
6288
|
+
actionsRow.appendChild(dlBtn);
|
|
6289
|
+
}
|
|
6290
|
+
const hasOpenUrl = !!(state.config.getDownloadUrl ?? state.config.getThumbnail);
|
|
6291
|
+
if (hasOpenUrl) {
|
|
6292
|
+
const openBtn = document.createElement("button");
|
|
6293
|
+
openBtn.type = "button";
|
|
6294
|
+
openBtn.title = "Open in new window";
|
|
6295
|
+
openBtn.style.cssText = btnStyle;
|
|
6296
|
+
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>';
|
|
6297
|
+
openBtn.addEventListener("mouseenter", () => btnHoverIn(openBtn));
|
|
6298
|
+
openBtn.addEventListener("mouseleave", () => btnHoverOut(openBtn));
|
|
6299
|
+
openBtn.addEventListener("click", (e) => {
|
|
6300
|
+
e.stopPropagation();
|
|
6301
|
+
if (state.config.getDownloadUrl) {
|
|
6302
|
+
const url = state.config.getDownloadUrl(rid);
|
|
6303
|
+
if (url) {
|
|
6304
|
+
window.open(url, "_blank");
|
|
6305
|
+
} else {
|
|
6306
|
+
state.config.getThumbnail?.(rid).then((thumbUrl) => {
|
|
6307
|
+
if (thumbUrl) window.open(thumbUrl, "_blank");
|
|
6308
|
+
}).catch(() => {
|
|
6309
|
+
});
|
|
6310
|
+
}
|
|
6311
|
+
} else {
|
|
6312
|
+
state.config.getThumbnail?.(rid).then((url) => {
|
|
6313
|
+
if (url) window.open(url, "_blank");
|
|
6314
|
+
}).catch(() => {
|
|
6315
|
+
});
|
|
6316
|
+
}
|
|
6317
|
+
});
|
|
6318
|
+
actionsRow.appendChild(openBtn);
|
|
6319
|
+
}
|
|
6320
|
+
if (!isReadonly && onRemove) {
|
|
6321
|
+
const removeBtn = document.createElement("button");
|
|
6322
|
+
removeBtn.type = "button";
|
|
6323
|
+
removeBtn.title = "Remove";
|
|
6324
|
+
removeBtn.style.cssText = btnStyle;
|
|
6325
|
+
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>';
|
|
6326
|
+
removeBtn.addEventListener("mouseenter", () => {
|
|
6327
|
+
removeBtn.style.background = "var(--fb-background-hover-color, #f3f4f6)";
|
|
6328
|
+
removeBtn.style.color = "var(--fb-error-color, #ef4444)";
|
|
6329
|
+
});
|
|
6330
|
+
removeBtn.addEventListener("mouseleave", () => btnHoverOut(removeBtn));
|
|
6331
|
+
removeBtn.addEventListener("click", (e) => {
|
|
6332
|
+
e.stopPropagation();
|
|
6333
|
+
tooltip.remove();
|
|
6334
|
+
onRemove();
|
|
6335
|
+
});
|
|
6336
|
+
actionsRow.appendChild(removeBtn);
|
|
6337
|
+
}
|
|
6338
|
+
const hasActions = actionsRow.children.length > 0;
|
|
6339
|
+
if (hasActions) {
|
|
6340
|
+
tooltip.appendChild(actionsRow);
|
|
6341
|
+
}
|
|
6342
|
+
document.body.appendChild(tooltip);
|
|
6343
|
+
positionPortalTooltip(tooltip, anchor);
|
|
6344
|
+
return tooltip;
|
|
6345
|
+
}
|
|
6346
|
+
function createTooltipHandle() {
|
|
6347
|
+
return { element: null, hideTimer: null };
|
|
6348
|
+
}
|
|
6349
|
+
function scheduleHideTooltip(handle, delayMs = 150) {
|
|
6350
|
+
if (handle.hideTimer !== null) return;
|
|
6351
|
+
handle.hideTimer = setTimeout(() => {
|
|
6352
|
+
handle.hideTimer = null;
|
|
6353
|
+
if (handle.element) {
|
|
6354
|
+
handle.element.remove();
|
|
6355
|
+
handle.element = null;
|
|
6356
|
+
}
|
|
6357
|
+
}, delayMs);
|
|
6358
|
+
}
|
|
6359
|
+
function cancelHideTooltip(handle) {
|
|
6360
|
+
if (handle.hideTimer !== null) {
|
|
6361
|
+
clearTimeout(handle.hideTimer);
|
|
6362
|
+
handle.hideTimer = null;
|
|
6363
|
+
}
|
|
6364
|
+
}
|
|
6365
|
+
function removePortalTooltip(tooltip) {
|
|
6366
|
+
if (tooltip) tooltip.remove();
|
|
6367
|
+
return null;
|
|
6368
|
+
}
|
|
6369
|
+
function getAtTrigger(textarea) {
|
|
6370
|
+
const cursorPos = textarea.selectionStart ?? 0;
|
|
6371
|
+
const textBefore = textarea.value.slice(0, cursorPos);
|
|
6372
|
+
for (let i = textBefore.length - 1; i >= 0; i--) {
|
|
6373
|
+
if (textBefore[i] === "@") {
|
|
6374
|
+
if (i === 0 || /\s/.test(textBefore[i - 1])) {
|
|
6375
|
+
let query = textBefore.slice(i + 1);
|
|
6376
|
+
if (query.startsWith('"')) {
|
|
6377
|
+
query = query.slice(1);
|
|
6378
|
+
}
|
|
6379
|
+
return { query, pos: i };
|
|
6380
|
+
}
|
|
6381
|
+
return null;
|
|
6382
|
+
}
|
|
6383
|
+
}
|
|
6384
|
+
return null;
|
|
6385
|
+
}
|
|
6386
|
+
function filterFilesForDropdown(query, files, labels) {
|
|
6387
|
+
const lq = query.toLowerCase();
|
|
6388
|
+
return files.filter((rid) => {
|
|
6389
|
+
const label = labels.get(rid) ?? rid;
|
|
6390
|
+
return label.toLowerCase().includes(lq);
|
|
6391
|
+
});
|
|
6392
|
+
}
|
|
6393
|
+
var TEXTAREA_FONT = "font-size: var(--fb-font-size, 14px); font-family: var(--fb-font-family, inherit); line-height: 1.6;";
|
|
6394
|
+
var TEXTAREA_PADDING = "padding: 12px 52px 12px 14px;";
|
|
6395
|
+
function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
|
|
6396
|
+
const state = ctx.state;
|
|
6397
|
+
const files = [...initialValue.files];
|
|
6398
|
+
const dropdownState = {
|
|
6399
|
+
open: false,
|
|
6400
|
+
query: "",
|
|
6401
|
+
triggerPos: -1,
|
|
6402
|
+
selectedIndex: 0
|
|
6403
|
+
};
|
|
6404
|
+
const docListenerCtrl = new AbortController();
|
|
6405
|
+
const hiddenInput = document.createElement("input");
|
|
6406
|
+
hiddenInput.type = "hidden";
|
|
6407
|
+
hiddenInput.name = pathKey;
|
|
6408
|
+
function getCurrentValue() {
|
|
6409
|
+
const rawText = textarea.value;
|
|
6410
|
+
const nameToRid = buildNameToRid(files, state);
|
|
6411
|
+
const submissionText = rawText ? replaceFilenamesWithRids(rawText, nameToRid) : null;
|
|
6412
|
+
const textKey = element.textKey ?? "text";
|
|
6413
|
+
const filesKey = element.filesKey ?? "files";
|
|
6414
|
+
return {
|
|
6415
|
+
[textKey]: rawText === "" ? null : submissionText,
|
|
6416
|
+
[filesKey]: [...files]
|
|
6417
|
+
};
|
|
6418
|
+
}
|
|
6419
|
+
function writeHidden() {
|
|
6420
|
+
hiddenInput.value = JSON.stringify(getCurrentValue());
|
|
6421
|
+
}
|
|
6422
|
+
const outerDiv = document.createElement("div");
|
|
6423
|
+
outerDiv.className = "fb-richinput-wrapper";
|
|
6424
|
+
outerDiv.style.cssText = `
|
|
6425
|
+
position: relative;
|
|
6426
|
+
border: 1px solid var(--fb-border-color, #d1d5db);
|
|
6427
|
+
border-radius: 16px;
|
|
6428
|
+
background: var(--fb-background-color, #f9fafb);
|
|
6429
|
+
transition: box-shadow 0.15s, border-color 0.15s;
|
|
6430
|
+
`;
|
|
6431
|
+
outerDiv.addEventListener("focusin", () => {
|
|
6432
|
+
outerDiv.style.borderColor = "var(--fb-primary-color, #0066cc)";
|
|
6433
|
+
outerDiv.style.boxShadow = "0 0 0 2px color-mix(in srgb, var(--fb-primary-color, #0066cc) 25%, transparent)";
|
|
6434
|
+
});
|
|
6435
|
+
outerDiv.addEventListener("focusout", () => {
|
|
6436
|
+
outerDiv.style.borderColor = "var(--fb-border-color, #d1d5db)";
|
|
6437
|
+
outerDiv.style.boxShadow = "none";
|
|
6438
|
+
});
|
|
6439
|
+
let dragCounter = 0;
|
|
6440
|
+
outerDiv.addEventListener("dragenter", (e) => {
|
|
6441
|
+
e.preventDefault();
|
|
6442
|
+
dragCounter++;
|
|
6443
|
+
outerDiv.style.borderColor = "var(--fb-primary-color, #0066cc)";
|
|
6444
|
+
outerDiv.style.boxShadow = "0 0 0 2px color-mix(in srgb, var(--fb-primary-color, #0066cc) 25%, transparent)";
|
|
6445
|
+
});
|
|
6446
|
+
outerDiv.addEventListener("dragover", (e) => {
|
|
6447
|
+
e.preventDefault();
|
|
6448
|
+
});
|
|
6449
|
+
outerDiv.addEventListener("dragleave", (e) => {
|
|
6450
|
+
e.preventDefault();
|
|
6451
|
+
dragCounter--;
|
|
6452
|
+
if (dragCounter <= 0) {
|
|
6453
|
+
dragCounter = 0;
|
|
6454
|
+
outerDiv.style.borderColor = "var(--fb-border-color, #d1d5db)";
|
|
6455
|
+
outerDiv.style.boxShadow = "none";
|
|
6456
|
+
}
|
|
6457
|
+
});
|
|
6458
|
+
outerDiv.addEventListener("drop", (e) => {
|
|
6459
|
+
e.preventDefault();
|
|
6460
|
+
dragCounter = 0;
|
|
6461
|
+
outerDiv.style.borderColor = "var(--fb-border-color, #d1d5db)";
|
|
6462
|
+
outerDiv.style.boxShadow = "none";
|
|
6463
|
+
const droppedFiles = e.dataTransfer?.files;
|
|
6464
|
+
if (!droppedFiles || !state.config.uploadFile) return;
|
|
6465
|
+
const maxFiles = element.maxFiles ?? Infinity;
|
|
6466
|
+
for (let i = 0; i < droppedFiles.length && files.length < maxFiles; i++) {
|
|
6467
|
+
uploadFile(droppedFiles[i]);
|
|
6468
|
+
}
|
|
6469
|
+
});
|
|
6470
|
+
const filesRow = document.createElement("div");
|
|
6471
|
+
filesRow.className = "fb-richinput-files";
|
|
6472
|
+
filesRow.style.cssText = "display: none; flex-wrap: wrap; gap: 6px; padding: 10px 14px 0; align-items: center;";
|
|
6473
|
+
const fileInput = document.createElement("input");
|
|
6474
|
+
fileInput.type = "file";
|
|
6475
|
+
fileInput.multiple = true;
|
|
6476
|
+
fileInput.style.display = "none";
|
|
6477
|
+
if (element.accept) {
|
|
6478
|
+
if (typeof element.accept === "string") {
|
|
6479
|
+
fileInput.accept = element.accept;
|
|
6480
|
+
} else {
|
|
6481
|
+
fileInput.accept = element.accept.extensions.map((ext) => ext.startsWith(".") ? ext : `.${ext}`).join(",");
|
|
6482
|
+
}
|
|
6483
|
+
}
|
|
6484
|
+
const textareaArea = document.createElement("div");
|
|
6485
|
+
textareaArea.style.cssText = "position: relative;";
|
|
6486
|
+
const backdrop = document.createElement("div");
|
|
6487
|
+
backdrop.className = "fb-richinput-backdrop";
|
|
6488
|
+
backdrop.style.cssText = `
|
|
6489
|
+
position: absolute;
|
|
6490
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
6491
|
+
${TEXTAREA_PADDING}
|
|
6492
|
+
${TEXTAREA_FONT}
|
|
6493
|
+
white-space: pre-wrap;
|
|
6494
|
+
word-break: break-word;
|
|
6495
|
+
color: transparent;
|
|
6496
|
+
pointer-events: none;
|
|
6497
|
+
overflow: hidden;
|
|
6498
|
+
border-radius: inherit;
|
|
6499
|
+
box-sizing: border-box;
|
|
6500
|
+
z-index: 2;
|
|
6501
|
+
`;
|
|
6502
|
+
const textarea = document.createElement("textarea");
|
|
6503
|
+
textarea.name = `${pathKey}__text`;
|
|
6504
|
+
textarea.placeholder = element.placeholder || t("richinputPlaceholder", state);
|
|
6505
|
+
const rawInitialText = initialValue.text ?? "";
|
|
6506
|
+
textarea.value = rawInitialText ? replaceRidsWithFilenames(rawInitialText, files, state) : "";
|
|
6507
|
+
textarea.style.cssText = `
|
|
6508
|
+
width: 100%;
|
|
6509
|
+
${TEXTAREA_PADDING}
|
|
6510
|
+
${TEXTAREA_FONT}
|
|
6511
|
+
background: transparent;
|
|
6512
|
+
border: none;
|
|
6513
|
+
outline: none;
|
|
6514
|
+
resize: none;
|
|
6515
|
+
color: var(--fb-text-color, #111827);
|
|
6516
|
+
box-sizing: border-box;
|
|
6517
|
+
position: relative;
|
|
6518
|
+
z-index: 1;
|
|
6519
|
+
caret-color: var(--fb-text-color, #111827);
|
|
6520
|
+
`;
|
|
6521
|
+
applyAutoExpand2(textarea, backdrop);
|
|
6522
|
+
textarea.addEventListener("scroll", () => {
|
|
6523
|
+
backdrop.scrollTop = textarea.scrollTop;
|
|
6524
|
+
});
|
|
6525
|
+
let mentionTooltip = null;
|
|
6526
|
+
backdrop.addEventListener("mouseover", (e) => {
|
|
6527
|
+
const mark = e.target.closest?.("mark");
|
|
6528
|
+
if (!mark?.dataset.rid) return;
|
|
6529
|
+
mentionTooltip = removePortalTooltip(mentionTooltip);
|
|
6530
|
+
mentionTooltip = showMentionTooltip(mark, mark.dataset.rid, state);
|
|
6531
|
+
});
|
|
6532
|
+
backdrop.addEventListener("mouseout", (e) => {
|
|
6533
|
+
const mark = e.target.closest?.("mark");
|
|
6534
|
+
if (!mark) return;
|
|
6535
|
+
const related = e.relatedTarget;
|
|
6536
|
+
if (related?.closest?.("mark")) return;
|
|
6537
|
+
mentionTooltip = removePortalTooltip(mentionTooltip);
|
|
6538
|
+
});
|
|
6539
|
+
backdrop.addEventListener("mousedown", (e) => {
|
|
6540
|
+
const mark = e.target.closest?.("mark");
|
|
6541
|
+
if (!mark) return;
|
|
6542
|
+
mentionTooltip = removePortalTooltip(mentionTooltip);
|
|
6543
|
+
const marks = backdrop.querySelectorAll("mark");
|
|
6544
|
+
marks.forEach((m) => m.style.pointerEvents = "none");
|
|
6545
|
+
const under = document.elementFromPoint(e.clientX, e.clientY);
|
|
6546
|
+
if (under) {
|
|
6547
|
+
under.dispatchEvent(
|
|
6548
|
+
new MouseEvent("mousedown", {
|
|
6549
|
+
bubbles: true,
|
|
6550
|
+
cancelable: true,
|
|
6551
|
+
view: window,
|
|
6552
|
+
clientX: e.clientX,
|
|
6553
|
+
clientY: e.clientY,
|
|
6554
|
+
button: e.button,
|
|
6555
|
+
buttons: e.buttons,
|
|
6556
|
+
detail: e.detail
|
|
6557
|
+
})
|
|
6558
|
+
);
|
|
6559
|
+
}
|
|
6560
|
+
document.addEventListener(
|
|
6561
|
+
"mouseup",
|
|
6562
|
+
() => {
|
|
6563
|
+
marks.forEach((m) => m.style.pointerEvents = "auto");
|
|
6564
|
+
},
|
|
6565
|
+
{ once: true }
|
|
6566
|
+
);
|
|
6567
|
+
});
|
|
6568
|
+
function updateBackdrop() {
|
|
6569
|
+
const text = textarea.value;
|
|
6570
|
+
const nameToRid = buildNameToRid(files, state);
|
|
6571
|
+
const tokens = findAtTokens(text);
|
|
6572
|
+
if (tokens.length === 0) {
|
|
6573
|
+
backdrop.innerHTML = escapeHtml(text) + "\n";
|
|
6574
|
+
return;
|
|
6575
|
+
}
|
|
6576
|
+
let html = "";
|
|
6577
|
+
let lastIdx = 0;
|
|
6578
|
+
for (const token of tokens) {
|
|
6579
|
+
html += escapeHtml(text.slice(lastIdx, token.start));
|
|
6580
|
+
const rid = nameToRid.get(token.name);
|
|
6581
|
+
if (rid) {
|
|
6582
|
+
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>`;
|
|
6583
|
+
} else {
|
|
6584
|
+
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>`;
|
|
6585
|
+
}
|
|
6586
|
+
lastIdx = token.end;
|
|
6587
|
+
}
|
|
6588
|
+
html += escapeHtml(text.slice(lastIdx));
|
|
6589
|
+
backdrop.innerHTML = html + "\n";
|
|
6590
|
+
}
|
|
6591
|
+
const paperclipBtn = document.createElement("button");
|
|
6592
|
+
paperclipBtn.type = "button";
|
|
6593
|
+
paperclipBtn.title = t("richinputAttachFile", state);
|
|
6594
|
+
paperclipBtn.style.cssText = `
|
|
6595
|
+
position: absolute;
|
|
6596
|
+
right: 10px;
|
|
6597
|
+
bottom: 10px;
|
|
6598
|
+
z-index: 2;
|
|
6599
|
+
width: 32px;
|
|
6600
|
+
height: 32px;
|
|
6601
|
+
border: none;
|
|
6602
|
+
border-radius: 8px;
|
|
6603
|
+
background: transparent;
|
|
6604
|
+
cursor: pointer;
|
|
6605
|
+
display: flex;
|
|
6606
|
+
align-items: center;
|
|
6607
|
+
justify-content: center;
|
|
6608
|
+
color: var(--fb-text-muted-color, #9ca3af);
|
|
6609
|
+
transition: color 0.15s, background 0.15s;
|
|
6610
|
+
`;
|
|
6611
|
+
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>';
|
|
6612
|
+
paperclipBtn.addEventListener("mouseenter", () => {
|
|
6613
|
+
paperclipBtn.style.color = "var(--fb-primary-color, #0066cc)";
|
|
6614
|
+
paperclipBtn.style.background = "var(--fb-background-hover-color, #f3f4f6)";
|
|
6615
|
+
});
|
|
6616
|
+
paperclipBtn.addEventListener("mouseleave", () => {
|
|
6617
|
+
paperclipBtn.style.color = "var(--fb-text-muted-color, #9ca3af)";
|
|
6618
|
+
paperclipBtn.style.background = "transparent";
|
|
6619
|
+
});
|
|
6620
|
+
paperclipBtn.addEventListener("click", () => {
|
|
6621
|
+
const maxFiles = element.maxFiles ?? Infinity;
|
|
6622
|
+
if (files.length < maxFiles) {
|
|
6623
|
+
fileInput.click();
|
|
6624
|
+
}
|
|
6625
|
+
});
|
|
6626
|
+
const dropdown = document.createElement("div");
|
|
6627
|
+
dropdown.className = "fb-richinput-dropdown";
|
|
6628
|
+
dropdown.style.cssText = `
|
|
6629
|
+
display: none;
|
|
6630
|
+
position: absolute;
|
|
6631
|
+
bottom: 100%;
|
|
6632
|
+
left: 0;
|
|
6633
|
+
z-index: 1000;
|
|
6634
|
+
background: #fff;
|
|
6635
|
+
border: 1px solid var(--fb-border-color, #d1d5db);
|
|
6636
|
+
border-radius: var(--fb-border-radius, 6px);
|
|
6637
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.12);
|
|
6638
|
+
min-width: 180px;
|
|
6639
|
+
max-width: 320px;
|
|
6640
|
+
max-height: 200px;
|
|
6641
|
+
overflow-y: auto;
|
|
6642
|
+
margin-bottom: 4px;
|
|
6643
|
+
${TEXTAREA_FONT}
|
|
6644
|
+
`;
|
|
6645
|
+
function buildFileLabelsFromClosure() {
|
|
6646
|
+
return buildFileLabels(files, state);
|
|
6647
|
+
}
|
|
6648
|
+
function renderDropdownItems(filtered) {
|
|
6649
|
+
clear(dropdown);
|
|
6650
|
+
const labels = buildFileLabelsFromClosure();
|
|
6651
|
+
if (filtered.length === 0) {
|
|
6652
|
+
dropdown.style.display = "none";
|
|
6653
|
+
dropdownState.open = false;
|
|
6654
|
+
return;
|
|
6655
|
+
}
|
|
6656
|
+
filtered.forEach((rid, idx) => {
|
|
6657
|
+
const meta = state.resourceIndex.get(rid);
|
|
6658
|
+
const item = document.createElement("div");
|
|
6659
|
+
item.className = "fb-richinput-dropdown-item";
|
|
6660
|
+
item.dataset.rid = rid;
|
|
6661
|
+
item.style.cssText = `
|
|
6662
|
+
padding: 5px 10px;
|
|
6663
|
+
cursor: pointer;
|
|
6664
|
+
color: var(--fb-text-color, #111827);
|
|
6665
|
+
background: ${idx === dropdownState.selectedIndex ? "var(--fb-background-hover-color, #f3f4f6)" : "transparent"};
|
|
6666
|
+
display: flex;
|
|
6667
|
+
align-items: center;
|
|
6668
|
+
gap: 8px;
|
|
6669
|
+
`;
|
|
6670
|
+
const thumb = document.createElement("div");
|
|
6671
|
+
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;";
|
|
6672
|
+
if (meta?.file && isImageMeta(meta)) {
|
|
6673
|
+
const img = document.createElement("img");
|
|
6674
|
+
img.style.cssText = "width: 100%; height: 100%; object-fit: cover; display: block;";
|
|
6675
|
+
const reader = new FileReader();
|
|
6676
|
+
reader.onload = (ev) => {
|
|
6677
|
+
img.src = ev.target?.result || "";
|
|
6678
|
+
};
|
|
6679
|
+
reader.readAsDataURL(meta.file);
|
|
6680
|
+
thumb.appendChild(img);
|
|
6681
|
+
} else if (state.config.getThumbnail) {
|
|
6682
|
+
state.config.getThumbnail(rid).then((url) => {
|
|
6683
|
+
if (!url || !thumb.isConnected) return;
|
|
6684
|
+
const img = document.createElement("img");
|
|
6685
|
+
img.style.cssText = "width: 100%; height: 100%; object-fit: cover; display: block;";
|
|
6686
|
+
img.src = url;
|
|
6687
|
+
clear(thumb);
|
|
6688
|
+
thumb.appendChild(img);
|
|
6689
|
+
}).catch(() => {
|
|
6690
|
+
});
|
|
6691
|
+
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>';
|
|
6692
|
+
} else {
|
|
6693
|
+
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>';
|
|
6694
|
+
}
|
|
6695
|
+
item.appendChild(thumb);
|
|
6696
|
+
const nameSpan = document.createElement("span");
|
|
6697
|
+
nameSpan.style.cssText = "overflow: hidden; text-overflow: ellipsis; white-space: nowrap;";
|
|
6698
|
+
nameSpan.textContent = labels.get(rid) ?? rid;
|
|
6699
|
+
item.appendChild(nameSpan);
|
|
6700
|
+
dropdown.appendChild(item);
|
|
6701
|
+
});
|
|
6702
|
+
dropdown.onmousemove = (e) => {
|
|
6703
|
+
const target = e.target.closest?.(
|
|
6704
|
+
".fb-richinput-dropdown-item"
|
|
6705
|
+
);
|
|
6706
|
+
if (!target) return;
|
|
6707
|
+
const newIdx = filtered.indexOf(target.dataset.rid ?? "");
|
|
6708
|
+
if (newIdx === -1 || newIdx === dropdownState.selectedIndex) return;
|
|
6709
|
+
const items = dropdown.querySelectorAll(
|
|
6710
|
+
".fb-richinput-dropdown-item"
|
|
6711
|
+
);
|
|
6712
|
+
items.forEach((el, i) => {
|
|
6713
|
+
el.style.background = i === newIdx ? "var(--fb-background-hover-color, #f3f4f6)" : "transparent";
|
|
6714
|
+
});
|
|
6715
|
+
dropdownState.selectedIndex = newIdx;
|
|
6716
|
+
};
|
|
6717
|
+
dropdown.onmousedown = (e) => {
|
|
6718
|
+
e.preventDefault();
|
|
6719
|
+
e.stopPropagation();
|
|
6720
|
+
const target = e.target.closest?.(
|
|
6721
|
+
".fb-richinput-dropdown-item"
|
|
6722
|
+
);
|
|
6723
|
+
if (!target?.dataset.rid) return;
|
|
6724
|
+
insertMention(target.dataset.rid);
|
|
6725
|
+
};
|
|
6726
|
+
dropdown.style.display = "block";
|
|
6727
|
+
dropdownState.open = true;
|
|
6728
|
+
}
|
|
6729
|
+
function openDropdown() {
|
|
6730
|
+
const trigger = getAtTrigger(textarea);
|
|
6731
|
+
if (!trigger) {
|
|
6732
|
+
closeDropdown();
|
|
6733
|
+
return;
|
|
6734
|
+
}
|
|
6735
|
+
dropdownState.query = trigger.query;
|
|
6736
|
+
dropdownState.triggerPos = trigger.pos;
|
|
6737
|
+
dropdownState.selectedIndex = 0;
|
|
6738
|
+
const labels = buildFileLabelsFromClosure();
|
|
6739
|
+
const filtered = filterFilesForDropdown(trigger.query, files, labels);
|
|
6740
|
+
renderDropdownItems(filtered);
|
|
6741
|
+
}
|
|
6742
|
+
function closeDropdown() {
|
|
6743
|
+
dropdown.style.display = "none";
|
|
6744
|
+
dropdownState.open = false;
|
|
6745
|
+
}
|
|
6746
|
+
function insertMention(rid) {
|
|
6747
|
+
const labels = buildFileLabelsFromClosure();
|
|
6748
|
+
const label = labels.get(rid) ?? state.resourceIndex.get(rid)?.name ?? rid;
|
|
6749
|
+
const cursorPos = textarea.selectionStart ?? 0;
|
|
6750
|
+
const before = textarea.value.slice(0, dropdownState.triggerPos);
|
|
6751
|
+
const after = textarea.value.slice(cursorPos);
|
|
6752
|
+
const mention = `${formatMention(label)} `;
|
|
6753
|
+
textarea.value = `${before}${mention}${after}`;
|
|
6754
|
+
const newPos = before.length + mention.length;
|
|
6755
|
+
textarea.setSelectionRange(newPos, newPos);
|
|
6756
|
+
textarea.dispatchEvent(new Event("input"));
|
|
6757
|
+
closeDropdown();
|
|
6758
|
+
}
|
|
6759
|
+
textarea.addEventListener("input", () => {
|
|
6760
|
+
updateBackdrop();
|
|
6761
|
+
writeHidden();
|
|
6762
|
+
ctx.instance?.triggerOnChange(pathKey, getCurrentValue());
|
|
6763
|
+
if (files.length > 0) {
|
|
6764
|
+
openDropdown();
|
|
6765
|
+
} else {
|
|
6766
|
+
closeDropdown();
|
|
6767
|
+
}
|
|
6768
|
+
});
|
|
6769
|
+
function updateDropdownHighlight() {
|
|
6770
|
+
const items = dropdown.querySelectorAll(
|
|
6771
|
+
".fb-richinput-dropdown-item"
|
|
6772
|
+
);
|
|
6773
|
+
items.forEach((el, i) => {
|
|
6774
|
+
el.style.background = i === dropdownState.selectedIndex ? "var(--fb-background-hover-color, #f3f4f6)" : "transparent";
|
|
6775
|
+
});
|
|
6776
|
+
}
|
|
6777
|
+
textarea.addEventListener("keydown", (e) => {
|
|
6778
|
+
if (!dropdownState.open) return;
|
|
6779
|
+
const labels = buildFileLabelsFromClosure();
|
|
6780
|
+
const filtered = filterFilesForDropdown(
|
|
6781
|
+
dropdownState.query,
|
|
6782
|
+
files,
|
|
6783
|
+
labels
|
|
6784
|
+
);
|
|
6785
|
+
if (e.key === "ArrowDown") {
|
|
6786
|
+
e.preventDefault();
|
|
6787
|
+
dropdownState.selectedIndex = Math.min(
|
|
6788
|
+
dropdownState.selectedIndex + 1,
|
|
6789
|
+
filtered.length - 1
|
|
6790
|
+
);
|
|
6791
|
+
updateDropdownHighlight();
|
|
6792
|
+
} else if (e.key === "ArrowUp") {
|
|
6793
|
+
e.preventDefault();
|
|
6794
|
+
dropdownState.selectedIndex = Math.max(
|
|
6795
|
+
dropdownState.selectedIndex - 1,
|
|
6796
|
+
0
|
|
6797
|
+
);
|
|
6798
|
+
updateDropdownHighlight();
|
|
6799
|
+
} else if (e.key === "Enter" && filtered.length > 0) {
|
|
6800
|
+
e.preventDefault();
|
|
6801
|
+
insertMention(filtered[dropdownState.selectedIndex]);
|
|
6802
|
+
} else if (e.key === "Escape") {
|
|
6803
|
+
closeDropdown();
|
|
6804
|
+
}
|
|
6805
|
+
});
|
|
6806
|
+
document.addEventListener(
|
|
6807
|
+
"click",
|
|
6808
|
+
(e) => {
|
|
6809
|
+
if (!outerDiv.contains(e.target) && !dropdown.contains(e.target)) {
|
|
6810
|
+
closeDropdown();
|
|
6811
|
+
}
|
|
6812
|
+
},
|
|
6813
|
+
{ signal: docListenerCtrl.signal }
|
|
6814
|
+
);
|
|
6815
|
+
function renderFilesRow() {
|
|
6816
|
+
clear(filesRow);
|
|
6817
|
+
if (files.length === 0) {
|
|
6818
|
+
filesRow.style.display = "none";
|
|
6819
|
+
return;
|
|
6820
|
+
}
|
|
6821
|
+
filesRow.style.display = "flex";
|
|
6822
|
+
files.forEach((rid) => {
|
|
6823
|
+
const meta = state.resourceIndex.get(rid);
|
|
6824
|
+
const thumbWrapper = document.createElement("div");
|
|
6825
|
+
thumbWrapper.className = "fb-richinput-file-thumb";
|
|
6826
|
+
thumbWrapper.style.cssText = `
|
|
6827
|
+
position: relative;
|
|
6828
|
+
width: 48px;
|
|
6829
|
+
height: 48px;
|
|
6830
|
+
border: 1px solid var(--fb-border-color, #d1d5db);
|
|
6831
|
+
border-radius: 8px;
|
|
6832
|
+
overflow: hidden;
|
|
6833
|
+
flex-shrink: 0;
|
|
6834
|
+
cursor: pointer;
|
|
6835
|
+
background: #fff;
|
|
6836
|
+
`;
|
|
6837
|
+
const thumbInner = document.createElement("div");
|
|
6838
|
+
thumbInner.style.cssText = "width: 48px; height: 48px; border-radius: inherit; overflow: hidden;";
|
|
6839
|
+
renderThumbContent(thumbInner, rid, meta, state);
|
|
6840
|
+
thumbWrapper.appendChild(thumbInner);
|
|
6841
|
+
const tooltipHandle = createTooltipHandle();
|
|
6842
|
+
const doMention = () => {
|
|
6843
|
+
const cursorPos = textarea.selectionStart ?? textarea.value.length;
|
|
6844
|
+
const labels = buildFileLabelsFromClosure();
|
|
6845
|
+
const label = labels.get(rid) ?? meta?.name ?? rid;
|
|
6846
|
+
const before = textarea.value.slice(0, cursorPos);
|
|
6847
|
+
const after = textarea.value.slice(cursorPos);
|
|
6848
|
+
const prefix = before.length > 0 && !/[\s\n]$/.test(before) ? "\n" : "";
|
|
6849
|
+
const mention = `${prefix}${formatMention(label)} `;
|
|
6850
|
+
textarea.value = `${before}${mention}${after}`;
|
|
6851
|
+
const newPos = cursorPos + mention.length;
|
|
6852
|
+
textarea.setSelectionRange(newPos, newPos);
|
|
6853
|
+
textarea.focus();
|
|
6854
|
+
textarea.dispatchEvent(new Event("input"));
|
|
6855
|
+
};
|
|
6856
|
+
const doRemove = () => {
|
|
6857
|
+
const idx = files.indexOf(rid);
|
|
6858
|
+
if (idx !== -1) files.splice(idx, 1);
|
|
6859
|
+
renderFilesRow();
|
|
6860
|
+
updateBackdrop();
|
|
6861
|
+
writeHidden();
|
|
6862
|
+
ctx.instance?.triggerOnChange(pathKey, getCurrentValue());
|
|
6863
|
+
};
|
|
6864
|
+
thumbWrapper.addEventListener("mouseenter", () => {
|
|
6865
|
+
cancelHideTooltip(tooltipHandle);
|
|
6866
|
+
if (!tooltipHandle.element) {
|
|
6867
|
+
tooltipHandle.element = showFileTooltip(thumbWrapper, {
|
|
6868
|
+
rid,
|
|
6869
|
+
state,
|
|
6870
|
+
isReadonly: false,
|
|
6871
|
+
onMention: doMention,
|
|
6872
|
+
onRemove: doRemove
|
|
6873
|
+
});
|
|
6874
|
+
tooltipHandle.element.addEventListener("mouseenter", () => {
|
|
6875
|
+
cancelHideTooltip(tooltipHandle);
|
|
6876
|
+
});
|
|
6877
|
+
tooltipHandle.element.addEventListener("mouseleave", () => {
|
|
6878
|
+
scheduleHideTooltip(tooltipHandle);
|
|
6879
|
+
});
|
|
6880
|
+
}
|
|
6881
|
+
});
|
|
6882
|
+
thumbWrapper.addEventListener("mouseleave", () => {
|
|
6883
|
+
scheduleHideTooltip(tooltipHandle);
|
|
6884
|
+
});
|
|
6885
|
+
filesRow.appendChild(thumbWrapper);
|
|
6886
|
+
});
|
|
6887
|
+
}
|
|
6888
|
+
function uploadFile(file) {
|
|
6889
|
+
if (!state.config.uploadFile) return;
|
|
6890
|
+
const tempId = `temp-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
6891
|
+
state.resourceIndex.set(tempId, {
|
|
6892
|
+
name: file.name,
|
|
6893
|
+
type: file.type,
|
|
6894
|
+
size: file.size,
|
|
6895
|
+
uploadedAt: /* @__PURE__ */ new Date(),
|
|
6896
|
+
file
|
|
6897
|
+
});
|
|
6898
|
+
files.push(tempId);
|
|
6899
|
+
renderFilesRow();
|
|
6900
|
+
const thumbs = filesRow.querySelectorAll(
|
|
6901
|
+
".fb-richinput-file-thumb"
|
|
6902
|
+
);
|
|
6903
|
+
const loadingThumb = thumbs[thumbs.length - 1];
|
|
6904
|
+
if (loadingThumb) loadingThumb.style.opacity = "0.5";
|
|
6905
|
+
state.config.uploadFile(file).then((resourceId) => {
|
|
6906
|
+
const idx = files.indexOf(tempId);
|
|
6907
|
+
if (idx !== -1) files[idx] = resourceId;
|
|
6908
|
+
state.resourceIndex.delete(tempId);
|
|
6909
|
+
state.resourceIndex.set(resourceId, {
|
|
6910
|
+
name: file.name,
|
|
6911
|
+
type: file.type,
|
|
6912
|
+
size: file.size,
|
|
6913
|
+
uploadedAt: /* @__PURE__ */ new Date(),
|
|
6914
|
+
file
|
|
6915
|
+
});
|
|
6916
|
+
renderFilesRow();
|
|
6917
|
+
updateBackdrop();
|
|
6918
|
+
writeHidden();
|
|
6919
|
+
ctx.instance?.triggerOnChange(pathKey, getCurrentValue());
|
|
6920
|
+
}).catch((err) => {
|
|
6921
|
+
const idx = files.indexOf(tempId);
|
|
6922
|
+
if (idx !== -1) files.splice(idx, 1);
|
|
6923
|
+
state.resourceIndex.delete(tempId);
|
|
6924
|
+
renderFilesRow();
|
|
6925
|
+
state.config.onUploadError?.(err, file);
|
|
6926
|
+
});
|
|
6927
|
+
}
|
|
6928
|
+
fileInput.addEventListener("change", () => {
|
|
6929
|
+
const selected = fileInput.files;
|
|
6930
|
+
if (!selected || selected.length === 0) return;
|
|
6931
|
+
const maxFiles = element.maxFiles ?? Infinity;
|
|
6932
|
+
for (let i = 0; i < selected.length && files.length < maxFiles; i++) {
|
|
6933
|
+
uploadFile(selected[i]);
|
|
6934
|
+
}
|
|
6935
|
+
fileInput.value = "";
|
|
6936
|
+
});
|
|
6937
|
+
textareaArea.appendChild(backdrop);
|
|
6938
|
+
textareaArea.appendChild(textarea);
|
|
6939
|
+
textareaArea.appendChild(paperclipBtn);
|
|
6940
|
+
textareaArea.appendChild(dropdown);
|
|
6941
|
+
outerDiv.appendChild(filesRow);
|
|
6942
|
+
outerDiv.appendChild(textareaArea);
|
|
6943
|
+
if (element.minLength != null || element.maxLength != null) {
|
|
6944
|
+
const counterRow = document.createElement("div");
|
|
6945
|
+
counterRow.style.cssText = "position: relative; padding: 2px 14px 6px; text-align: right;";
|
|
6946
|
+
const counter = createCharCounter(element, textarea, false);
|
|
6947
|
+
counter.style.cssText = `
|
|
6948
|
+
position: static;
|
|
6949
|
+
display: inline-block;
|
|
6950
|
+
font-size: var(--fb-font-size-small);
|
|
6951
|
+
color: var(--fb-text-secondary-color);
|
|
6952
|
+
pointer-events: none;
|
|
6953
|
+
`;
|
|
6954
|
+
counterRow.appendChild(counter);
|
|
6955
|
+
outerDiv.appendChild(counterRow);
|
|
6956
|
+
}
|
|
6957
|
+
outerDiv.appendChild(hiddenInput);
|
|
6958
|
+
outerDiv.appendChild(fileInput);
|
|
6959
|
+
writeHidden();
|
|
6960
|
+
updateBackdrop();
|
|
6961
|
+
hiddenInput._applyExternalUpdate = (value) => {
|
|
6962
|
+
const rawText = value.text ?? "";
|
|
6963
|
+
textarea.value = rawText ? replaceRidsWithFilenames(rawText, files, state) : "";
|
|
6964
|
+
textarea.dispatchEvent(new Event("input"));
|
|
6965
|
+
files.length = 0;
|
|
6966
|
+
for (const rid of value.files) files.push(rid);
|
|
6967
|
+
renderFilesRow();
|
|
6968
|
+
updateBackdrop();
|
|
6969
|
+
writeHidden();
|
|
6970
|
+
};
|
|
6971
|
+
wrapper.appendChild(outerDiv);
|
|
6972
|
+
renderFilesRow();
|
|
6973
|
+
const observer = new MutationObserver(() => {
|
|
6974
|
+
if (!outerDiv.isConnected) {
|
|
6975
|
+
docListenerCtrl.abort();
|
|
6976
|
+
mentionTooltip = removePortalTooltip(mentionTooltip);
|
|
6977
|
+
observer.disconnect();
|
|
6978
|
+
}
|
|
6979
|
+
});
|
|
6980
|
+
if (outerDiv.parentElement) {
|
|
6981
|
+
observer.observe(outerDiv.parentElement, { childList: true });
|
|
6982
|
+
}
|
|
6983
|
+
}
|
|
6984
|
+
function renderReadonlyMode(_element, ctx, wrapper, _pathKey, value) {
|
|
6985
|
+
const state = ctx.state;
|
|
6986
|
+
const { text, files } = value;
|
|
6987
|
+
const ridToName = /* @__PURE__ */ new Map();
|
|
6988
|
+
for (const rid of files) {
|
|
6989
|
+
const meta = state.resourceIndex.get(rid);
|
|
6990
|
+
if (meta?.name) ridToName.set(rid, meta.name);
|
|
6991
|
+
}
|
|
6992
|
+
if (files.length > 0) {
|
|
6993
|
+
const filesRow = document.createElement("div");
|
|
6994
|
+
filesRow.style.cssText = "display: flex; flex-wrap: wrap; gap: 6px; padding-bottom: 8px;";
|
|
6995
|
+
files.forEach((rid) => {
|
|
6996
|
+
const meta = state.resourceIndex.get(rid);
|
|
6997
|
+
const thumbWrapper = document.createElement("div");
|
|
6998
|
+
thumbWrapper.style.cssText = `
|
|
6999
|
+
position: relative;
|
|
7000
|
+
width: 48px; height: 48px;
|
|
7001
|
+
border: 1px solid var(--fb-border-color, #d1d5db);
|
|
7002
|
+
border-radius: 8px;
|
|
7003
|
+
overflow: hidden;
|
|
7004
|
+
flex-shrink: 0;
|
|
7005
|
+
background: #fff;
|
|
7006
|
+
cursor: default;
|
|
7007
|
+
`;
|
|
7008
|
+
const thumbInner = document.createElement("div");
|
|
7009
|
+
thumbInner.style.cssText = "width: 48px; height: 48px; border-radius: inherit; overflow: hidden;";
|
|
7010
|
+
renderThumbContent(thumbInner, rid, meta, state);
|
|
7011
|
+
thumbWrapper.appendChild(thumbInner);
|
|
7012
|
+
const tooltipHandle = createTooltipHandle();
|
|
7013
|
+
thumbWrapper.addEventListener("mouseenter", () => {
|
|
7014
|
+
cancelHideTooltip(tooltipHandle);
|
|
7015
|
+
if (!tooltipHandle.element) {
|
|
7016
|
+
tooltipHandle.element = showFileTooltip(thumbWrapper, {
|
|
7017
|
+
rid,
|
|
7018
|
+
state,
|
|
7019
|
+
isReadonly: true
|
|
7020
|
+
});
|
|
7021
|
+
tooltipHandle.element.addEventListener("mouseenter", () => {
|
|
7022
|
+
cancelHideTooltip(tooltipHandle);
|
|
7023
|
+
});
|
|
7024
|
+
tooltipHandle.element.addEventListener("mouseleave", () => {
|
|
7025
|
+
scheduleHideTooltip(tooltipHandle);
|
|
7026
|
+
});
|
|
7027
|
+
}
|
|
7028
|
+
});
|
|
7029
|
+
thumbWrapper.addEventListener("mouseleave", () => {
|
|
7030
|
+
scheduleHideTooltip(tooltipHandle);
|
|
7031
|
+
});
|
|
7032
|
+
filesRow.appendChild(thumbWrapper);
|
|
7033
|
+
});
|
|
7034
|
+
wrapper.appendChild(filesRow);
|
|
7035
|
+
}
|
|
7036
|
+
if (text) {
|
|
7037
|
+
const textDiv = document.createElement("div");
|
|
7038
|
+
textDiv.style.cssText = `
|
|
7039
|
+
${TEXTAREA_FONT}
|
|
7040
|
+
color: var(--fb-text-color, #111827);
|
|
7041
|
+
white-space: pre-wrap;
|
|
7042
|
+
word-break: break-word;
|
|
7043
|
+
`;
|
|
7044
|
+
const tokens = findAtTokens(text);
|
|
7045
|
+
const resolvedTokens = tokens.filter(
|
|
7046
|
+
(tok) => ridToName.has(tok.name) || [...ridToName.values()].includes(tok.name)
|
|
7047
|
+
);
|
|
7048
|
+
if (resolvedTokens.length === 0) {
|
|
7049
|
+
textDiv.textContent = text;
|
|
7050
|
+
} else {
|
|
7051
|
+
let lastIndex = 0;
|
|
7052
|
+
for (const token of resolvedTokens) {
|
|
7053
|
+
if (token.start > lastIndex) {
|
|
7054
|
+
textDiv.appendChild(
|
|
7055
|
+
document.createTextNode(text.slice(lastIndex, token.start))
|
|
7056
|
+
);
|
|
7057
|
+
}
|
|
7058
|
+
const span = document.createElement("span");
|
|
7059
|
+
span.style.cssText = `
|
|
7060
|
+
display: inline;
|
|
7061
|
+
background: color-mix(in srgb, var(--fb-primary-color, #0066cc) 15%, transparent);
|
|
7062
|
+
color: var(--fb-primary-color, #0066cc);
|
|
7063
|
+
border-radius: 8px;
|
|
7064
|
+
padding: 1px 6px;
|
|
7065
|
+
font-weight: 500;
|
|
7066
|
+
cursor: default;
|
|
7067
|
+
`;
|
|
7068
|
+
const rid = ridToName.has(token.name) ? token.name : [...ridToName.entries()].find(([, n]) => n === token.name)?.[0];
|
|
7069
|
+
const displayName = ridToName.get(token.name) ?? token.name;
|
|
7070
|
+
span.textContent = `@${displayName}`;
|
|
7071
|
+
if (rid) {
|
|
7072
|
+
let spanTooltip = null;
|
|
7073
|
+
const mentionRid = rid;
|
|
7074
|
+
span.addEventListener("mouseenter", () => {
|
|
7075
|
+
spanTooltip = removePortalTooltip(spanTooltip);
|
|
7076
|
+
spanTooltip = showMentionTooltip(span, mentionRid, state);
|
|
7077
|
+
});
|
|
7078
|
+
span.addEventListener("mouseleave", () => {
|
|
7079
|
+
spanTooltip = removePortalTooltip(spanTooltip);
|
|
7080
|
+
});
|
|
7081
|
+
}
|
|
7082
|
+
textDiv.appendChild(span);
|
|
7083
|
+
lastIndex = token.end;
|
|
7084
|
+
}
|
|
7085
|
+
if (lastIndex < text.length) {
|
|
7086
|
+
textDiv.appendChild(document.createTextNode(text.slice(lastIndex)));
|
|
7087
|
+
}
|
|
7088
|
+
}
|
|
7089
|
+
wrapper.appendChild(textDiv);
|
|
7090
|
+
}
|
|
7091
|
+
if (!text && files.length === 0) {
|
|
7092
|
+
const empty = document.createElement("div");
|
|
7093
|
+
empty.style.cssText = "color: var(--fb-text-muted-color, #6b7280); font-size: var(--fb-font-size, 14px);";
|
|
7094
|
+
empty.textContent = "\u2014";
|
|
7095
|
+
wrapper.appendChild(empty);
|
|
7096
|
+
}
|
|
7097
|
+
}
|
|
7098
|
+
function renderRichInputElement(element, ctx, wrapper, pathKey) {
|
|
7099
|
+
const state = ctx.state;
|
|
7100
|
+
const textKey = element.textKey ?? "text";
|
|
7101
|
+
const filesKey = element.filesKey ?? "files";
|
|
7102
|
+
const rawPrefill = ctx.prefill[element.key];
|
|
7103
|
+
let initialValue;
|
|
7104
|
+
if (rawPrefill && typeof rawPrefill === "object" && !Array.isArray(rawPrefill)) {
|
|
7105
|
+
const obj = rawPrefill;
|
|
7106
|
+
const textVal = obj[textKey] ?? obj["text"];
|
|
7107
|
+
const filesVal = obj[filesKey] ?? obj["files"];
|
|
7108
|
+
initialValue = {
|
|
7109
|
+
text: typeof textVal === "string" ? textVal : null,
|
|
7110
|
+
files: Array.isArray(filesVal) ? filesVal : []
|
|
7111
|
+
};
|
|
7112
|
+
} else if (typeof rawPrefill === "string") {
|
|
7113
|
+
initialValue = { text: rawPrefill || null, files: [] };
|
|
7114
|
+
} else {
|
|
7115
|
+
initialValue = { text: null, files: [] };
|
|
7116
|
+
}
|
|
7117
|
+
for (const rid of initialValue.files) {
|
|
7118
|
+
if (!state.resourceIndex.has(rid)) {
|
|
7119
|
+
state.resourceIndex.set(rid, {
|
|
7120
|
+
name: rid,
|
|
7121
|
+
type: "application/octet-stream",
|
|
7122
|
+
size: 0,
|
|
7123
|
+
uploadedAt: /* @__PURE__ */ new Date(),
|
|
7124
|
+
file: void 0
|
|
7125
|
+
});
|
|
7126
|
+
}
|
|
7127
|
+
}
|
|
7128
|
+
if (state.config.readonly) {
|
|
7129
|
+
renderReadonlyMode(element, ctx, wrapper, pathKey, initialValue);
|
|
7130
|
+
} else {
|
|
7131
|
+
if (!state.config.uploadFile) {
|
|
7132
|
+
throw new Error(
|
|
7133
|
+
`RichInput field "${element.key}" requires uploadFile handler in config`
|
|
7134
|
+
);
|
|
7135
|
+
}
|
|
7136
|
+
renderEditMode(element, ctx, wrapper, pathKey, initialValue);
|
|
7137
|
+
}
|
|
7138
|
+
}
|
|
7139
|
+
function validateRichInputElement(element, key, context) {
|
|
7140
|
+
const { scopeRoot, state, skipValidation } = context;
|
|
7141
|
+
const errors = [];
|
|
7142
|
+
const textKey = element.textKey ?? "text";
|
|
7143
|
+
const filesKey = element.filesKey ?? "files";
|
|
7144
|
+
const hiddenInput = scopeRoot.querySelector(
|
|
7145
|
+
`[name="${key}"]`
|
|
7146
|
+
);
|
|
7147
|
+
if (!hiddenInput) {
|
|
7148
|
+
return { value: null, errors };
|
|
7149
|
+
}
|
|
7150
|
+
let rawValue = {};
|
|
7151
|
+
try {
|
|
7152
|
+
const parsed = JSON.parse(hiddenInput.value);
|
|
7153
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
7154
|
+
rawValue = parsed;
|
|
7155
|
+
} else {
|
|
7156
|
+
errors.push(`${key}: invalid richinput data`);
|
|
7157
|
+
return { value: null, errors };
|
|
7158
|
+
}
|
|
7159
|
+
} catch {
|
|
7160
|
+
errors.push(`${key}: invalid richinput data`);
|
|
7161
|
+
return { value: null, errors };
|
|
7162
|
+
}
|
|
7163
|
+
const textVal = rawValue[textKey];
|
|
7164
|
+
const filesVal = rawValue[filesKey];
|
|
7165
|
+
const text = textVal === null || typeof textVal === "string" ? textVal : null;
|
|
7166
|
+
const files = Array.isArray(filesVal) ? filesVal : [];
|
|
7167
|
+
const value = {
|
|
7168
|
+
[textKey]: text ?? null,
|
|
7169
|
+
[filesKey]: files
|
|
7170
|
+
};
|
|
7171
|
+
if (!skipValidation) {
|
|
7172
|
+
const textEmpty = !text || text.trim() === "";
|
|
7173
|
+
const filesEmpty = files.length === 0;
|
|
7174
|
+
if (element.required && textEmpty && filesEmpty) {
|
|
7175
|
+
errors.push(`${key}: ${t("required", state)}`);
|
|
7176
|
+
}
|
|
7177
|
+
if (!textEmpty && text) {
|
|
7178
|
+
if (element.minLength != null && text.length < element.minLength) {
|
|
7179
|
+
errors.push(
|
|
7180
|
+
`${key}: ${t("minLength", state, { min: element.minLength })}`
|
|
7181
|
+
);
|
|
7182
|
+
}
|
|
7183
|
+
if (element.maxLength != null && text.length > element.maxLength) {
|
|
7184
|
+
errors.push(
|
|
7185
|
+
`${key}: ${t("maxLength", state, { max: element.maxLength })}`
|
|
7186
|
+
);
|
|
7187
|
+
}
|
|
7188
|
+
}
|
|
7189
|
+
if (element.maxFiles != null && files.length > element.maxFiles) {
|
|
7190
|
+
errors.push(
|
|
7191
|
+
`${key}: ${t("maxFiles", state, { max: element.maxFiles })}`
|
|
7192
|
+
);
|
|
7193
|
+
}
|
|
7194
|
+
}
|
|
7195
|
+
return { value, errors };
|
|
7196
|
+
}
|
|
7197
|
+
function updateRichInputField(element, fieldPath, value, context) {
|
|
7198
|
+
const { scopeRoot } = context;
|
|
7199
|
+
const hiddenInput = scopeRoot.querySelector(
|
|
7200
|
+
`[name="${fieldPath}"]`
|
|
7201
|
+
);
|
|
7202
|
+
if (!hiddenInput) {
|
|
7203
|
+
console.warn(
|
|
7204
|
+
`updateRichInputField: no hidden input found for "${fieldPath}". Re-render to reflect new data.`
|
|
7205
|
+
);
|
|
7206
|
+
return;
|
|
7207
|
+
}
|
|
7208
|
+
let normalized = null;
|
|
7209
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
7210
|
+
const obj = value;
|
|
7211
|
+
const textKey = element.textKey ?? "text";
|
|
7212
|
+
const filesKey = element.filesKey ?? "files";
|
|
7213
|
+
const textVal = obj[textKey] ?? obj["text"];
|
|
7214
|
+
const filesVal = obj[filesKey] ?? obj["files"];
|
|
7215
|
+
if (textVal !== void 0 || filesVal !== void 0) {
|
|
7216
|
+
normalized = {
|
|
7217
|
+
text: typeof textVal === "string" ? textVal : null,
|
|
7218
|
+
files: Array.isArray(filesVal) ? filesVal : []
|
|
7219
|
+
};
|
|
7220
|
+
}
|
|
7221
|
+
}
|
|
7222
|
+
if (normalized && hiddenInput._applyExternalUpdate) {
|
|
7223
|
+
hiddenInput._applyExternalUpdate(normalized);
|
|
7224
|
+
} else if (normalized) {
|
|
7225
|
+
hiddenInput.value = JSON.stringify(normalized);
|
|
7226
|
+
}
|
|
7227
|
+
}
|
|
7228
|
+
|
|
5713
7229
|
// src/components/index.ts
|
|
5714
7230
|
function showTooltip(tooltipId, button) {
|
|
5715
7231
|
const tooltip = document.getElementById(tooltipId);
|
|
@@ -6059,6 +7575,9 @@ function dispatchToRenderer(element, ctx, wrapper, pathKey) {
|
|
|
6059
7575
|
case "table":
|
|
6060
7576
|
renderTableElement(element, ctx, wrapper, pathKey);
|
|
6061
7577
|
break;
|
|
7578
|
+
case "richinput":
|
|
7579
|
+
renderRichInputElement(element, ctx, wrapper, pathKey);
|
|
7580
|
+
break;
|
|
6062
7581
|
default: {
|
|
6063
7582
|
const unsupported = document.createElement("div");
|
|
6064
7583
|
unsupported.className = "text-red-500 text-sm";
|
|
@@ -6103,6 +7622,7 @@ var defaultConfig = {
|
|
|
6103
7622
|
enableFilePreview: true,
|
|
6104
7623
|
maxPreviewSize: "200px",
|
|
6105
7624
|
readonly: false,
|
|
7625
|
+
parseTableFile: null,
|
|
6106
7626
|
locale: "en",
|
|
6107
7627
|
translations: {
|
|
6108
7628
|
en: {
|
|
@@ -6155,7 +7675,14 @@ var defaultConfig = {
|
|
|
6155
7675
|
tableRemoveRow: "Remove row",
|
|
6156
7676
|
tableRemoveColumn: "Remove column",
|
|
6157
7677
|
tableMergeCells: "Merge cells (Ctrl+M)",
|
|
6158
|
-
tableSplitCell: "Split cell (Ctrl+Shift+M)"
|
|
7678
|
+
tableSplitCell: "Split cell (Ctrl+Shift+M)",
|
|
7679
|
+
tableImportFile: "Import",
|
|
7680
|
+
tableImporting: "Importing...",
|
|
7681
|
+
tableImportError: "Import failed: {error}",
|
|
7682
|
+
richinputPlaceholder: "Type text...",
|
|
7683
|
+
richinputAttachFile: "Attach file",
|
|
7684
|
+
richinputMention: "Mention",
|
|
7685
|
+
richinputRemoveFile: "Remove"
|
|
6159
7686
|
},
|
|
6160
7687
|
ru: {
|
|
6161
7688
|
// UI texts
|
|
@@ -6207,7 +7734,14 @@ var defaultConfig = {
|
|
|
6207
7734
|
tableRemoveRow: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0441\u0442\u0440\u043E\u043A\u0443",
|
|
6208
7735
|
tableRemoveColumn: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0441\u0442\u043E\u043B\u0431\u0435\u0446",
|
|
6209
7736
|
tableMergeCells: "\u041E\u0431\u044A\u0435\u0434\u0438\u043D\u0438\u0442\u044C \u044F\u0447\u0435\u0439\u043A\u0438 (Ctrl+M)",
|
|
6210
|
-
tableSplitCell: "\u0420\u0430\u0437\u0434\u0435\u043B\u0438\u0442\u044C \u044F\u0447\u0435\u0439\u043A\u0443 (Ctrl+Shift+M)"
|
|
7737
|
+
tableSplitCell: "\u0420\u0430\u0437\u0434\u0435\u043B\u0438\u0442\u044C \u044F\u0447\u0435\u0439\u043A\u0443 (Ctrl+Shift+M)",
|
|
7738
|
+
tableImportFile: "\u0418\u043C\u043F\u043E\u0440\u0442",
|
|
7739
|
+
tableImporting: "\u0418\u043C\u043F\u043E\u0440\u0442...",
|
|
7740
|
+
tableImportError: "\u041E\u0448\u0438\u0431\u043A\u0430 \u0438\u043C\u043F\u043E\u0440\u0442\u0430: {error}",
|
|
7741
|
+
richinputPlaceholder: "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442...",
|
|
7742
|
+
richinputAttachFile: "\u041F\u0440\u0438\u043A\u0440\u0435\u043F\u0438\u0442\u044C \u0444\u0430\u0439\u043B",
|
|
7743
|
+
richinputMention: "\u0423\u043F\u043E\u043C\u044F\u043D\u0443\u0442\u044C",
|
|
7744
|
+
richinputRemoveFile: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C"
|
|
6211
7745
|
}
|
|
6212
7746
|
},
|
|
6213
7747
|
theme: {}
|
|
@@ -6485,6 +8019,10 @@ var componentRegistry = {
|
|
|
6485
8019
|
table: {
|
|
6486
8020
|
validate: validateTableElement,
|
|
6487
8021
|
update: updateTableField
|
|
8022
|
+
},
|
|
8023
|
+
richinput: {
|
|
8024
|
+
validate: validateRichInputElement,
|
|
8025
|
+
update: updateRichInputField
|
|
6488
8026
|
}
|
|
6489
8027
|
};
|
|
6490
8028
|
function getComponentOperations(elementType) {
|