@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/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.row === row && m.col === col) {
4760
+ if (m.top === row && m.left === col) {
4720
4761
  return null;
4721
4762
  }
4722
- if (row >= m.row && row < m.row + m.rowspan && col >= m.col && col < m.col + m.colspan) {
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.row === row && m.col === col) ?? null;
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
- if (merge.rowspan > 1) td.rowSpan = merge.rowspan;
4804
- if (merge.colspan > 1) td.colSpan = merge.colspan;
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.row >= insertAt) {
5055
- return { ...m, row: m.row + 1 };
5239
+ if (m.top >= insertAt) {
5240
+ return { ...m, top: m.top + 1, bottom: m.bottom + 1 };
5056
5241
  }
5057
- if (m.row < insertAt && m.row + m.rowspan > insertAt) {
5058
- return { ...m, rowspan: m.rowspan + 1 };
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
- const mEndRow = m.row + m.rowspan - 1;
5070
- if (m.row === rowToRemove && m.rowspan === 1) return null;
5071
- if (m.row === rowToRemove) {
5072
- return { ...m, row: m.row + 1, rowspan: m.rowspan - 1 };
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 (mEndRow === rowToRemove) {
5075
- return { ...m, rowspan: m.rowspan - 1 };
5258
+ if (m.bottom === rowToRemove) {
5259
+ return { ...m, bottom: m.bottom - 1 };
5076
5260
  }
5077
- if (m.row < rowToRemove && mEndRow > rowToRemove) {
5078
- return { ...m, rowspan: m.rowspan - 1 };
5261
+ if (m.top < rowToRemove && m.bottom > rowToRemove) {
5262
+ return { ...m, bottom: m.bottom - 1 };
5079
5263
  }
5080
- if (m.row > rowToRemove) {
5081
- return { ...m, row: m.row - 1 };
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.col >= insertAt) {
5097
- return { ...m, col: m.col + 1 };
5280
+ if (m.left >= insertAt) {
5281
+ return { ...m, left: m.left + 1, right: m.right + 1 };
5098
5282
  }
5099
- if (m.col < insertAt && m.col + m.colspan > insertAt) {
5100
- return { ...m, colspan: m.colspan + 1 };
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
- const mEndCol = m.col + m.colspan - 1;
5112
- if (m.col === colToRemove && m.colspan === 1) return null;
5113
- if (m.col === colToRemove) {
5114
- return { ...m, col: m.col + 1, colspan: m.colspan - 1 };
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 (mEndCol === colToRemove) {
5117
- return { ...m, colspan: m.colspan - 1 };
5299
+ if (m.right === colToRemove) {
5300
+ return { ...m, right: m.right - 1 };
5118
5301
  }
5119
- if (m.col < colToRemove && mEndCol > colToRemove) {
5120
- return { ...m, colspan: m.colspan - 1 };
5302
+ if (m.left < colToRemove && m.right > colToRemove) {
5303
+ return { ...m, right: m.right - 1 };
5121
5304
  }
5122
- if (m.col > colToRemove) {
5123
- return { ...m, col: m.col - 1 };
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 mEndRow = m.row + m.rowspan - 1;
5141
- const mEndCol = m.col + m.colspan - 1;
5142
- const overlaps = m.row <= r2 && mEndRow >= r1 && m.col <= c2 && mEndCol >= c1;
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
- merges.push({ row: r1, col: c1, rowspan: r2 - r1 + 1, colspan: c2 - c1 + 1 });
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.row === row && m.col === col);
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
- if (merge.rowspan > 1) td.rowSpan = merge.rowspan;
5188
- if (merge.colspan > 1) td.colSpan = merge.colspan;
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 initialData = isTableData(rawPrefill) ? rawPrefill : isTableData(element.default) ? element.default : defaultTableData(element);
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 hasContent = value.cells.some(
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(_element, fieldPath, value, context) {
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
- if (isTableData(value) && hiddenInput._applyExternalUpdate) {
5707
- hiddenInput._applyExternalUpdate(value);
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) {