@dmitryvim/form-builder 0.2.19 → 0.2.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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) {
@@ -5710,6 +5715,1273 @@ function updateTableField(_element, fieldPath, value, context) {
5710
5715
  }
5711
5716
  }
5712
5717
 
5718
+ // src/components/richinput.ts
5719
+ function applyAutoExpand2(textarea, backdrop) {
5720
+ textarea.style.overflow = "hidden";
5721
+ textarea.style.resize = "none";
5722
+ const lineCount = (textarea.value.match(/\n/g) || []).length + 1;
5723
+ textarea.rows = Math.max(3, lineCount);
5724
+ const resize = () => {
5725
+ if (!textarea.isConnected) return;
5726
+ textarea.style.height = "0";
5727
+ textarea.style.height = `${textarea.scrollHeight}px`;
5728
+ if (backdrop) {
5729
+ backdrop.style.height = `${textarea.scrollHeight}px`;
5730
+ }
5731
+ };
5732
+ textarea.addEventListener("input", resize);
5733
+ setTimeout(() => {
5734
+ if (textarea.isConnected) resize();
5735
+ }, 0);
5736
+ }
5737
+ function buildFileLabels(files, state) {
5738
+ const labels = /* @__PURE__ */ new Map();
5739
+ const nameCount = /* @__PURE__ */ new Map();
5740
+ for (const rid of files) {
5741
+ const meta = state.resourceIndex.get(rid);
5742
+ const name = meta?.name ?? rid;
5743
+ nameCount.set(name, (nameCount.get(name) ?? 0) + 1);
5744
+ }
5745
+ for (const rid of files) {
5746
+ const meta = state.resourceIndex.get(rid);
5747
+ const name = meta?.name ?? rid;
5748
+ if ((nameCount.get(name) ?? 1) > 1 && meta) {
5749
+ labels.set(rid, `${name} (${formatFileSize(meta.size)})`);
5750
+ } else {
5751
+ labels.set(rid, name);
5752
+ }
5753
+ }
5754
+ return labels;
5755
+ }
5756
+ function isImageMeta(meta) {
5757
+ if (!meta) return false;
5758
+ return meta.type.startsWith("image/");
5759
+ }
5760
+ function buildNameToRid(files, state) {
5761
+ const labels = buildFileLabels(files, state);
5762
+ const map = /* @__PURE__ */ new Map();
5763
+ for (const rid of files) {
5764
+ const label = labels.get(rid);
5765
+ if (label) map.set(label, rid);
5766
+ }
5767
+ return map;
5768
+ }
5769
+ function formatMention(name) {
5770
+ if (/\s/.test(name) || name.includes('"')) {
5771
+ return `@"${name.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
5772
+ }
5773
+ return `@${name}`;
5774
+ }
5775
+ function findAtTokens(text) {
5776
+ const tokens = [];
5777
+ const len = text.length;
5778
+ let i = 0;
5779
+ while (i < len) {
5780
+ if (text[i] === "@") {
5781
+ if (i > 0 && !/\s/.test(text[i - 1])) {
5782
+ i++;
5783
+ continue;
5784
+ }
5785
+ const start = i;
5786
+ i++;
5787
+ if (i < len && text[i] === '"') {
5788
+ i++;
5789
+ let name = "";
5790
+ while (i < len && text[i] !== '"') {
5791
+ if (text[i] === "\\" && i + 1 < len) {
5792
+ name += text[i + 1];
5793
+ i += 2;
5794
+ } else {
5795
+ name += text[i];
5796
+ i++;
5797
+ }
5798
+ }
5799
+ if (i < len && text[i] === '"') {
5800
+ i++;
5801
+ if (i >= len || /\s/.test(text[i])) {
5802
+ tokens.push({ start, end: i, raw: text.slice(start, i), name });
5803
+ }
5804
+ }
5805
+ } else if (i < len && !/\s/.test(text[i])) {
5806
+ const wordStart = i;
5807
+ while (i < len && !/\s/.test(text[i])) {
5808
+ i++;
5809
+ }
5810
+ const name = text.slice(wordStart, i);
5811
+ tokens.push({ start, end: i, raw: text.slice(start, i), name });
5812
+ }
5813
+ } else {
5814
+ i++;
5815
+ }
5816
+ }
5817
+ return tokens;
5818
+ }
5819
+ function findMentions(text, nameToRid) {
5820
+ if (nameToRid.size === 0) return [];
5821
+ const tokens = findAtTokens(text);
5822
+ const results = [];
5823
+ for (const token of tokens) {
5824
+ const rid = nameToRid.get(token.name);
5825
+ if (rid) {
5826
+ results.push({
5827
+ start: token.start,
5828
+ end: token.end,
5829
+ name: token.name,
5830
+ rid
5831
+ });
5832
+ }
5833
+ }
5834
+ return results;
5835
+ }
5836
+ function replaceFilenamesWithRids(text, nameToRid) {
5837
+ const mentions = findMentions(text, nameToRid);
5838
+ if (mentions.length === 0) return text;
5839
+ let result = "";
5840
+ let lastIdx = 0;
5841
+ for (const m of mentions) {
5842
+ result += text.slice(lastIdx, m.start);
5843
+ result += `@${m.rid}`;
5844
+ lastIdx = m.end;
5845
+ }
5846
+ result += text.slice(lastIdx);
5847
+ return result;
5848
+ }
5849
+ function replaceRidsWithFilenames(text, files, state) {
5850
+ const labels = buildFileLabels(files, state);
5851
+ const ridToLabel = /* @__PURE__ */ new Map();
5852
+ for (const rid of files) {
5853
+ const label = labels.get(rid);
5854
+ if (label) ridToLabel.set(rid, label);
5855
+ }
5856
+ const tokens = findAtTokens(text);
5857
+ if (tokens.length === 0) return text;
5858
+ let result = "";
5859
+ let lastIdx = 0;
5860
+ for (const token of tokens) {
5861
+ result += text.slice(lastIdx, token.start);
5862
+ const label = ridToLabel.get(token.name);
5863
+ if (label) {
5864
+ result += formatMention(label);
5865
+ } else {
5866
+ result += token.raw;
5867
+ }
5868
+ lastIdx = token.end;
5869
+ }
5870
+ result += text.slice(lastIdx);
5871
+ return result;
5872
+ }
5873
+ function renderThumbContent(thumb, rid, meta, state) {
5874
+ clear(thumb);
5875
+ if (meta?.file && isImageMeta(meta)) {
5876
+ const img = document.createElement("img");
5877
+ img.alt = meta.name;
5878
+ img.style.cssText = "width: 100%; height: 100%; object-fit: cover; display: block;";
5879
+ const reader = new FileReader();
5880
+ reader.onload = (e) => {
5881
+ img.src = e.target?.result || "";
5882
+ };
5883
+ reader.readAsDataURL(meta.file);
5884
+ thumb.appendChild(img);
5885
+ } else if (state.config.getThumbnail) {
5886
+ state.config.getThumbnail(rid).then((url) => {
5887
+ if (!url || !thumb.isConnected) return;
5888
+ const img = document.createElement("img");
5889
+ img.alt = meta?.name ?? rid;
5890
+ img.src = url;
5891
+ img.style.cssText = "width: 100%; height: 100%; object-fit: cover; display: block;";
5892
+ clear(thumb);
5893
+ thumb.appendChild(img);
5894
+ }).catch((err) => {
5895
+ state.config.onThumbnailError?.(err, rid);
5896
+ });
5897
+ const placeholder = document.createElement("div");
5898
+ placeholder.style.cssText = "width: 100%; height: 100%; background: var(--fb-background-hover-color, #f3f4f6); display: flex; align-items: center; justify-content: center;";
5899
+ placeholder.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>';
5900
+ thumb.appendChild(placeholder);
5901
+ } else {
5902
+ const icon = document.createElement("div");
5903
+ icon.style.cssText = "width: 100%; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; padding: 2px; box-sizing: border-box;";
5904
+ icon.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>';
5905
+ if (meta?.name) {
5906
+ const nameEl = document.createElement("span");
5907
+ nameEl.style.cssText = "font-size: 9px; text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 44px; color: var(--fb-text-color, #111827);";
5908
+ nameEl.textContent = meta.name;
5909
+ icon.appendChild(nameEl);
5910
+ }
5911
+ thumb.appendChild(icon);
5912
+ }
5913
+ }
5914
+ function renderImagePreview(hoverEl, rid, meta, state) {
5915
+ clear(hoverEl);
5916
+ if (meta?.file && isImageMeta(meta)) {
5917
+ const img = document.createElement("img");
5918
+ img.alt = meta.name;
5919
+ img.style.cssText = "max-width: 120px; max-height: 120px; object-fit: contain; display: block;";
5920
+ const reader = new FileReader();
5921
+ reader.onload = (e) => {
5922
+ img.src = e.target?.result || "";
5923
+ };
5924
+ reader.readAsDataURL(meta.file);
5925
+ hoverEl.appendChild(img);
5926
+ } else if (state.config.getThumbnail) {
5927
+ state.config.getThumbnail(rid).then((url) => {
5928
+ if (!url || !hoverEl.isConnected) return;
5929
+ const img = document.createElement("img");
5930
+ img.alt = meta?.name ?? rid;
5931
+ img.src = url;
5932
+ img.style.cssText = "max-width: 120px; max-height: 120px; object-fit: contain; display: block;";
5933
+ clear(hoverEl);
5934
+ hoverEl.appendChild(img);
5935
+ }).catch((err) => {
5936
+ state.config.onThumbnailError?.(err, rid);
5937
+ });
5938
+ }
5939
+ }
5940
+ function positionPortalTooltip(tooltip, anchor) {
5941
+ const rect = anchor.getBoundingClientRect();
5942
+ const ttRect = tooltip.getBoundingClientRect();
5943
+ const left = Math.max(
5944
+ 4,
5945
+ Math.min(
5946
+ rect.left + rect.width / 2 - ttRect.width / 2,
5947
+ window.innerWidth - ttRect.width - 4
5948
+ )
5949
+ );
5950
+ const topAbove = rect.top - ttRect.height - 8;
5951
+ const topBelow = rect.bottom + 8;
5952
+ const top = topAbove >= 4 ? topAbove : topBelow;
5953
+ tooltip.style.left = `${left}px`;
5954
+ tooltip.style.top = `${Math.max(4, top)}px`;
5955
+ }
5956
+ function showMentionTooltip(anchor, rid, state) {
5957
+ const meta = state.resourceIndex.get(rid);
5958
+ const tooltip = document.createElement("div");
5959
+ tooltip.className = "fb-richinput-portal-tooltip fb-richinput-mention-tooltip";
5960
+ tooltip.style.cssText = `
5961
+ position: fixed;
5962
+ z-index: 99999;
5963
+ background: #fff;
5964
+ border: 1px solid var(--fb-border-color, #d1d5db);
5965
+ border-radius: 8px;
5966
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
5967
+ padding: 4px;
5968
+ pointer-events: none;
5969
+ min-width: 60px;
5970
+ max-width: 140px;
5971
+ `;
5972
+ const preview = document.createElement("div");
5973
+ preview.style.cssText = "min-height: 60px; max-height: 120px; display: flex; align-items: center; justify-content: center;";
5974
+ renderImagePreview(preview, rid, meta, state);
5975
+ tooltip.appendChild(preview);
5976
+ document.body.appendChild(tooltip);
5977
+ positionPortalTooltip(tooltip, anchor);
5978
+ return tooltip;
5979
+ }
5980
+ function showFileTooltip(anchor, opts) {
5981
+ const { rid, state, isReadonly, onMention, onRemove } = opts;
5982
+ const meta = state.resourceIndex.get(rid);
5983
+ const filename = meta?.name ?? rid;
5984
+ const tooltip = document.createElement("div");
5985
+ tooltip.className = "fb-richinput-portal-tooltip fb-richinput-file-tooltip";
5986
+ tooltip.style.cssText = `
5987
+ position: fixed;
5988
+ z-index: 99999;
5989
+ background: #fff;
5990
+ border: 1px solid var(--fb-border-color, #d1d5db);
5991
+ border-radius: 8px;
5992
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
5993
+ padding: 4px;
5994
+ pointer-events: auto;
5995
+ min-width: 80px;
5996
+ max-width: 260px;
5997
+ `;
5998
+ const preview = document.createElement("div");
5999
+ preview.style.cssText = "min-height: 60px; max-height: 120px; display: flex; align-items: center; justify-content: center;";
6000
+ renderImagePreview(preview, rid, meta, state);
6001
+ tooltip.appendChild(preview);
6002
+ const nameEl = document.createElement("div");
6003
+ nameEl.style.cssText = "padding: 4px 6px 2px; font-size: 12px; color: var(--fb-text-color, #111827); word-break: break-word; border-top: 1px solid var(--fb-border-color, #d1d5db);";
6004
+ nameEl.textContent = filename;
6005
+ tooltip.appendChild(nameEl);
6006
+ const actionsRow = document.createElement("div");
6007
+ actionsRow.style.cssText = "display: flex; align-items: center; gap: 2px; padding: 3px 4px 2px; border-top: 1px solid var(--fb-border-color, #d1d5db); justify-content: center;";
6008
+ const btnStyle = "background: none; border: none; cursor: pointer; padding: 3px 5px; border-radius: 4px; color: var(--fb-text-muted-color, #6b7280); display: flex; align-items: center; transition: background 0.1s, color 0.1s;";
6009
+ const btnHoverIn = (btn) => {
6010
+ btn.style.background = "var(--fb-background-hover-color, #f3f4f6)";
6011
+ btn.style.color = "var(--fb-text-color, #111827)";
6012
+ };
6013
+ const btnHoverOut = (btn) => {
6014
+ btn.style.background = "none";
6015
+ btn.style.color = "var(--fb-text-muted-color, #6b7280)";
6016
+ };
6017
+ if (!isReadonly && onMention) {
6018
+ const mentionBtn = document.createElement("button");
6019
+ mentionBtn.type = "button";
6020
+ mentionBtn.title = "Mention";
6021
+ mentionBtn.style.cssText = btnStyle;
6022
+ mentionBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="12" cy="12" r="4"/><path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-3.92 7.94"/></svg>';
6023
+ mentionBtn.addEventListener("mouseenter", () => btnHoverIn(mentionBtn));
6024
+ mentionBtn.addEventListener("mouseleave", () => btnHoverOut(mentionBtn));
6025
+ mentionBtn.addEventListener("click", (e) => {
6026
+ e.stopPropagation();
6027
+ onMention();
6028
+ tooltip.remove();
6029
+ });
6030
+ actionsRow.appendChild(mentionBtn);
6031
+ }
6032
+ if (state.config.downloadFile) {
6033
+ const dlBtn = document.createElement("button");
6034
+ dlBtn.type = "button";
6035
+ dlBtn.title = "Download";
6036
+ dlBtn.style.cssText = btnStyle;
6037
+ dlBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>';
6038
+ dlBtn.addEventListener("mouseenter", () => btnHoverIn(dlBtn));
6039
+ dlBtn.addEventListener("mouseleave", () => btnHoverOut(dlBtn));
6040
+ dlBtn.addEventListener("click", (e) => {
6041
+ e.stopPropagation();
6042
+ state.config.downloadFile?.(rid, filename);
6043
+ });
6044
+ actionsRow.appendChild(dlBtn);
6045
+ }
6046
+ const hasOpenUrl = !!(state.config.getDownloadUrl ?? state.config.getThumbnail);
6047
+ if (hasOpenUrl) {
6048
+ const openBtn = document.createElement("button");
6049
+ openBtn.type = "button";
6050
+ openBtn.title = "Open in new window";
6051
+ openBtn.style.cssText = btnStyle;
6052
+ openBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>';
6053
+ openBtn.addEventListener("mouseenter", () => btnHoverIn(openBtn));
6054
+ openBtn.addEventListener("mouseleave", () => btnHoverOut(openBtn));
6055
+ openBtn.addEventListener("click", (e) => {
6056
+ e.stopPropagation();
6057
+ if (state.config.getDownloadUrl) {
6058
+ const url = state.config.getDownloadUrl(rid);
6059
+ if (url) {
6060
+ window.open(url, "_blank");
6061
+ } else {
6062
+ state.config.getThumbnail?.(rid).then((thumbUrl) => {
6063
+ if (thumbUrl) window.open(thumbUrl, "_blank");
6064
+ }).catch(() => {
6065
+ });
6066
+ }
6067
+ } else {
6068
+ state.config.getThumbnail?.(rid).then((url) => {
6069
+ if (url) window.open(url, "_blank");
6070
+ }).catch(() => {
6071
+ });
6072
+ }
6073
+ });
6074
+ actionsRow.appendChild(openBtn);
6075
+ }
6076
+ if (!isReadonly && onRemove) {
6077
+ const removeBtn = document.createElement("button");
6078
+ removeBtn.type = "button";
6079
+ removeBtn.title = "Remove";
6080
+ removeBtn.style.cssText = btnStyle;
6081
+ removeBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>';
6082
+ removeBtn.addEventListener("mouseenter", () => {
6083
+ removeBtn.style.background = "var(--fb-background-hover-color, #f3f4f6)";
6084
+ removeBtn.style.color = "var(--fb-error-color, #ef4444)";
6085
+ });
6086
+ removeBtn.addEventListener("mouseleave", () => btnHoverOut(removeBtn));
6087
+ removeBtn.addEventListener("click", (e) => {
6088
+ e.stopPropagation();
6089
+ tooltip.remove();
6090
+ onRemove();
6091
+ });
6092
+ actionsRow.appendChild(removeBtn);
6093
+ }
6094
+ const hasActions = actionsRow.children.length > 0;
6095
+ if (hasActions) {
6096
+ tooltip.appendChild(actionsRow);
6097
+ }
6098
+ document.body.appendChild(tooltip);
6099
+ positionPortalTooltip(tooltip, anchor);
6100
+ return tooltip;
6101
+ }
6102
+ function createTooltipHandle() {
6103
+ return { element: null, hideTimer: null };
6104
+ }
6105
+ function scheduleHideTooltip(handle, delayMs = 150) {
6106
+ if (handle.hideTimer !== null) return;
6107
+ handle.hideTimer = setTimeout(() => {
6108
+ handle.hideTimer = null;
6109
+ if (handle.element) {
6110
+ handle.element.remove();
6111
+ handle.element = null;
6112
+ }
6113
+ }, delayMs);
6114
+ }
6115
+ function cancelHideTooltip(handle) {
6116
+ if (handle.hideTimer !== null) {
6117
+ clearTimeout(handle.hideTimer);
6118
+ handle.hideTimer = null;
6119
+ }
6120
+ }
6121
+ function removePortalTooltip(tooltip) {
6122
+ if (tooltip) tooltip.remove();
6123
+ return null;
6124
+ }
6125
+ function getAtTrigger(textarea) {
6126
+ const cursorPos = textarea.selectionStart ?? 0;
6127
+ const textBefore = textarea.value.slice(0, cursorPos);
6128
+ for (let i = textBefore.length - 1; i >= 0; i--) {
6129
+ if (textBefore[i] === "@") {
6130
+ if (i === 0 || /\s/.test(textBefore[i - 1])) {
6131
+ let query = textBefore.slice(i + 1);
6132
+ if (query.startsWith('"')) {
6133
+ query = query.slice(1);
6134
+ }
6135
+ return { query, pos: i };
6136
+ }
6137
+ return null;
6138
+ }
6139
+ }
6140
+ return null;
6141
+ }
6142
+ function filterFilesForDropdown(query, files, labels) {
6143
+ const lq = query.toLowerCase();
6144
+ return files.filter((rid) => {
6145
+ const label = labels.get(rid) ?? rid;
6146
+ return label.toLowerCase().includes(lq);
6147
+ });
6148
+ }
6149
+ var TEXTAREA_FONT = "font-size: var(--fb-font-size, 14px); font-family: var(--fb-font-family, inherit); line-height: 1.6;";
6150
+ var TEXTAREA_PADDING = "padding: 12px 52px 12px 14px;";
6151
+ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
6152
+ const state = ctx.state;
6153
+ const files = [...initialValue.files];
6154
+ const dropdownState = {
6155
+ open: false,
6156
+ query: "",
6157
+ triggerPos: -1,
6158
+ selectedIndex: 0
6159
+ };
6160
+ const docListenerCtrl = new AbortController();
6161
+ const hiddenInput = document.createElement("input");
6162
+ hiddenInput.type = "hidden";
6163
+ hiddenInput.name = pathKey;
6164
+ function getCurrentValue() {
6165
+ const rawText = textarea.value;
6166
+ const nameToRid = buildNameToRid(files, state);
6167
+ const submissionText = rawText ? replaceFilenamesWithRids(rawText, nameToRid) : null;
6168
+ const textKey = element.textKey ?? "text";
6169
+ const filesKey = element.filesKey ?? "files";
6170
+ return {
6171
+ [textKey]: rawText === "" ? null : submissionText,
6172
+ [filesKey]: [...files]
6173
+ };
6174
+ }
6175
+ function writeHidden() {
6176
+ hiddenInput.value = JSON.stringify(getCurrentValue());
6177
+ }
6178
+ const outerDiv = document.createElement("div");
6179
+ outerDiv.className = "fb-richinput-wrapper";
6180
+ outerDiv.style.cssText = `
6181
+ position: relative;
6182
+ border: 1px solid var(--fb-border-color, #d1d5db);
6183
+ border-radius: 16px;
6184
+ background: var(--fb-background-color, #f9fafb);
6185
+ transition: box-shadow 0.15s, border-color 0.15s;
6186
+ `;
6187
+ outerDiv.addEventListener("focusin", () => {
6188
+ outerDiv.style.borderColor = "var(--fb-primary-color, #0066cc)";
6189
+ outerDiv.style.boxShadow = "0 0 0 2px color-mix(in srgb, var(--fb-primary-color, #0066cc) 25%, transparent)";
6190
+ });
6191
+ outerDiv.addEventListener("focusout", () => {
6192
+ outerDiv.style.borderColor = "var(--fb-border-color, #d1d5db)";
6193
+ outerDiv.style.boxShadow = "none";
6194
+ });
6195
+ let dragCounter = 0;
6196
+ outerDiv.addEventListener("dragenter", (e) => {
6197
+ e.preventDefault();
6198
+ dragCounter++;
6199
+ outerDiv.style.borderColor = "var(--fb-primary-color, #0066cc)";
6200
+ outerDiv.style.boxShadow = "0 0 0 2px color-mix(in srgb, var(--fb-primary-color, #0066cc) 25%, transparent)";
6201
+ });
6202
+ outerDiv.addEventListener("dragover", (e) => {
6203
+ e.preventDefault();
6204
+ });
6205
+ outerDiv.addEventListener("dragleave", (e) => {
6206
+ e.preventDefault();
6207
+ dragCounter--;
6208
+ if (dragCounter <= 0) {
6209
+ dragCounter = 0;
6210
+ outerDiv.style.borderColor = "var(--fb-border-color, #d1d5db)";
6211
+ outerDiv.style.boxShadow = "none";
6212
+ }
6213
+ });
6214
+ outerDiv.addEventListener("drop", (e) => {
6215
+ e.preventDefault();
6216
+ dragCounter = 0;
6217
+ outerDiv.style.borderColor = "var(--fb-border-color, #d1d5db)";
6218
+ outerDiv.style.boxShadow = "none";
6219
+ const droppedFiles = e.dataTransfer?.files;
6220
+ if (!droppedFiles || !state.config.uploadFile) return;
6221
+ const maxFiles = element.maxFiles ?? Infinity;
6222
+ for (let i = 0; i < droppedFiles.length && files.length < maxFiles; i++) {
6223
+ uploadFile(droppedFiles[i]);
6224
+ }
6225
+ });
6226
+ const filesRow = document.createElement("div");
6227
+ filesRow.className = "fb-richinput-files";
6228
+ filesRow.style.cssText = "display: none; flex-wrap: wrap; gap: 6px; padding: 10px 14px 0; align-items: center;";
6229
+ const fileInput = document.createElement("input");
6230
+ fileInput.type = "file";
6231
+ fileInput.multiple = true;
6232
+ fileInput.style.display = "none";
6233
+ if (element.accept) {
6234
+ if (typeof element.accept === "string") {
6235
+ fileInput.accept = element.accept;
6236
+ } else {
6237
+ fileInput.accept = element.accept.extensions.map((ext) => ext.startsWith(".") ? ext : `.${ext}`).join(",");
6238
+ }
6239
+ }
6240
+ const textareaArea = document.createElement("div");
6241
+ textareaArea.style.cssText = "position: relative;";
6242
+ const backdrop = document.createElement("div");
6243
+ backdrop.className = "fb-richinput-backdrop";
6244
+ backdrop.style.cssText = `
6245
+ position: absolute;
6246
+ top: 0; left: 0; right: 0; bottom: 0;
6247
+ ${TEXTAREA_PADDING}
6248
+ ${TEXTAREA_FONT}
6249
+ white-space: pre-wrap;
6250
+ word-break: break-word;
6251
+ color: transparent;
6252
+ pointer-events: none;
6253
+ overflow: hidden;
6254
+ border-radius: inherit;
6255
+ box-sizing: border-box;
6256
+ z-index: 2;
6257
+ `;
6258
+ const textarea = document.createElement("textarea");
6259
+ textarea.name = `${pathKey}__text`;
6260
+ textarea.placeholder = element.placeholder || t("richinputPlaceholder", state);
6261
+ const rawInitialText = initialValue.text ?? "";
6262
+ textarea.value = rawInitialText ? replaceRidsWithFilenames(rawInitialText, files, state) : "";
6263
+ textarea.style.cssText = `
6264
+ width: 100%;
6265
+ ${TEXTAREA_PADDING}
6266
+ ${TEXTAREA_FONT}
6267
+ background: transparent;
6268
+ border: none;
6269
+ outline: none;
6270
+ resize: none;
6271
+ color: var(--fb-text-color, #111827);
6272
+ box-sizing: border-box;
6273
+ position: relative;
6274
+ z-index: 1;
6275
+ caret-color: var(--fb-text-color, #111827);
6276
+ `;
6277
+ applyAutoExpand2(textarea, backdrop);
6278
+ textarea.addEventListener("scroll", () => {
6279
+ backdrop.scrollTop = textarea.scrollTop;
6280
+ });
6281
+ let mentionTooltip = null;
6282
+ backdrop.addEventListener("mouseover", (e) => {
6283
+ const mark = e.target.closest?.("mark");
6284
+ if (!mark?.dataset.rid) return;
6285
+ mentionTooltip = removePortalTooltip(mentionTooltip);
6286
+ mentionTooltip = showMentionTooltip(mark, mark.dataset.rid, state);
6287
+ });
6288
+ backdrop.addEventListener("mouseout", (e) => {
6289
+ const mark = e.target.closest?.("mark");
6290
+ if (!mark) return;
6291
+ const related = e.relatedTarget;
6292
+ if (related?.closest?.("mark")) return;
6293
+ mentionTooltip = removePortalTooltip(mentionTooltip);
6294
+ });
6295
+ backdrop.addEventListener("mousedown", (e) => {
6296
+ const mark = e.target.closest?.("mark");
6297
+ if (!mark) return;
6298
+ mentionTooltip = removePortalTooltip(mentionTooltip);
6299
+ const marks = backdrop.querySelectorAll("mark");
6300
+ marks.forEach((m) => m.style.pointerEvents = "none");
6301
+ const under = document.elementFromPoint(e.clientX, e.clientY);
6302
+ if (under) {
6303
+ under.dispatchEvent(
6304
+ new MouseEvent("mousedown", {
6305
+ bubbles: true,
6306
+ cancelable: true,
6307
+ view: window,
6308
+ clientX: e.clientX,
6309
+ clientY: e.clientY,
6310
+ button: e.button,
6311
+ buttons: e.buttons,
6312
+ detail: e.detail
6313
+ })
6314
+ );
6315
+ }
6316
+ document.addEventListener(
6317
+ "mouseup",
6318
+ () => {
6319
+ marks.forEach((m) => m.style.pointerEvents = "auto");
6320
+ },
6321
+ { once: true }
6322
+ );
6323
+ });
6324
+ function updateBackdrop() {
6325
+ const text = textarea.value;
6326
+ const nameToRid = buildNameToRid(files, state);
6327
+ const tokens = findAtTokens(text);
6328
+ if (tokens.length === 0) {
6329
+ backdrop.innerHTML = escapeHtml(text) + "\n";
6330
+ return;
6331
+ }
6332
+ let html = "";
6333
+ let lastIdx = 0;
6334
+ for (const token of tokens) {
6335
+ html += escapeHtml(text.slice(lastIdx, token.start));
6336
+ const rid = nameToRid.get(token.name);
6337
+ if (rid) {
6338
+ html += `<mark data-rid="${escapeHtml(rid)}" style="background: color-mix(in srgb, var(--fb-primary-color, #0066cc) 15%, transparent); color: transparent; border-radius: 8px; padding: 0; border: none; box-shadow: 0 0 0 2px color-mix(in srgb, var(--fb-primary-color, #0066cc) 15%, transparent), 0 0 0 3px color-mix(in srgb, var(--fb-primary-color, #0066cc) 30%, transparent); box-decoration-break: clone; -webkit-box-decoration-break: clone; pointer-events: auto; cursor: text;">${escapeHtml(text.slice(token.start, token.end))}</mark>`;
6339
+ } else {
6340
+ html += `<mark style="color: transparent; background: none; padding: 0; border: none; text-decoration-line: underline; text-decoration-style: wavy; text-decoration-color: rgba(239, 68, 68, 0.45); text-underline-offset: 2px;">${escapeHtml(text.slice(token.start, token.end))}</mark>`;
6341
+ }
6342
+ lastIdx = token.end;
6343
+ }
6344
+ html += escapeHtml(text.slice(lastIdx));
6345
+ backdrop.innerHTML = html + "\n";
6346
+ }
6347
+ const paperclipBtn = document.createElement("button");
6348
+ paperclipBtn.type = "button";
6349
+ paperclipBtn.title = t("richinputAttachFile", state);
6350
+ paperclipBtn.style.cssText = `
6351
+ position: absolute;
6352
+ right: 10px;
6353
+ bottom: 10px;
6354
+ z-index: 2;
6355
+ width: 32px;
6356
+ height: 32px;
6357
+ border: none;
6358
+ border-radius: 8px;
6359
+ background: transparent;
6360
+ cursor: pointer;
6361
+ display: flex;
6362
+ align-items: center;
6363
+ justify-content: center;
6364
+ color: var(--fb-text-muted-color, #9ca3af);
6365
+ transition: color 0.15s, background 0.15s;
6366
+ `;
6367
+ paperclipBtn.innerHTML = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>';
6368
+ paperclipBtn.addEventListener("mouseenter", () => {
6369
+ paperclipBtn.style.color = "var(--fb-primary-color, #0066cc)";
6370
+ paperclipBtn.style.background = "var(--fb-background-hover-color, #f3f4f6)";
6371
+ });
6372
+ paperclipBtn.addEventListener("mouseleave", () => {
6373
+ paperclipBtn.style.color = "var(--fb-text-muted-color, #9ca3af)";
6374
+ paperclipBtn.style.background = "transparent";
6375
+ });
6376
+ paperclipBtn.addEventListener("click", () => {
6377
+ const maxFiles = element.maxFiles ?? Infinity;
6378
+ if (files.length < maxFiles) {
6379
+ fileInput.click();
6380
+ }
6381
+ });
6382
+ const dropdown = document.createElement("div");
6383
+ dropdown.className = "fb-richinput-dropdown";
6384
+ dropdown.style.cssText = `
6385
+ display: none;
6386
+ position: absolute;
6387
+ bottom: 100%;
6388
+ left: 0;
6389
+ z-index: 1000;
6390
+ background: #fff;
6391
+ border: 1px solid var(--fb-border-color, #d1d5db);
6392
+ border-radius: var(--fb-border-radius, 6px);
6393
+ box-shadow: 0 4px 12px rgba(0,0,0,0.12);
6394
+ min-width: 180px;
6395
+ max-width: 320px;
6396
+ max-height: 200px;
6397
+ overflow-y: auto;
6398
+ margin-bottom: 4px;
6399
+ ${TEXTAREA_FONT}
6400
+ `;
6401
+ function buildFileLabelsFromClosure() {
6402
+ return buildFileLabels(files, state);
6403
+ }
6404
+ function renderDropdownItems(filtered) {
6405
+ clear(dropdown);
6406
+ const labels = buildFileLabelsFromClosure();
6407
+ if (filtered.length === 0) {
6408
+ dropdown.style.display = "none";
6409
+ dropdownState.open = false;
6410
+ return;
6411
+ }
6412
+ filtered.forEach((rid, idx) => {
6413
+ const meta = state.resourceIndex.get(rid);
6414
+ const item = document.createElement("div");
6415
+ item.className = "fb-richinput-dropdown-item";
6416
+ item.dataset.rid = rid;
6417
+ item.style.cssText = `
6418
+ padding: 5px 10px;
6419
+ cursor: pointer;
6420
+ color: var(--fb-text-color, #111827);
6421
+ background: ${idx === dropdownState.selectedIndex ? "var(--fb-background-hover-color, #f3f4f6)" : "transparent"};
6422
+ display: flex;
6423
+ align-items: center;
6424
+ gap: 8px;
6425
+ `;
6426
+ const thumb = document.createElement("div");
6427
+ thumb.style.cssText = "width: 24px; height: 24px; border-radius: 4px; overflow: hidden; flex-shrink: 0; background: var(--fb-background-hover-color, #f3f4f6); display: flex; align-items: center; justify-content: center;";
6428
+ if (meta?.file && isImageMeta(meta)) {
6429
+ const img = document.createElement("img");
6430
+ img.style.cssText = "width: 100%; height: 100%; object-fit: cover; display: block;";
6431
+ const reader = new FileReader();
6432
+ reader.onload = (ev) => {
6433
+ img.src = ev.target?.result || "";
6434
+ };
6435
+ reader.readAsDataURL(meta.file);
6436
+ thumb.appendChild(img);
6437
+ } else if (state.config.getThumbnail) {
6438
+ state.config.getThumbnail(rid).then((url) => {
6439
+ if (!url || !thumb.isConnected) return;
6440
+ const img = document.createElement("img");
6441
+ img.style.cssText = "width: 100%; height: 100%; object-fit: cover; display: block;";
6442
+ img.src = url;
6443
+ clear(thumb);
6444
+ thumb.appendChild(img);
6445
+ }).catch(() => {
6446
+ });
6447
+ thumb.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>';
6448
+ } else {
6449
+ thumb.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>';
6450
+ }
6451
+ item.appendChild(thumb);
6452
+ const nameSpan = document.createElement("span");
6453
+ nameSpan.style.cssText = "overflow: hidden; text-overflow: ellipsis; white-space: nowrap;";
6454
+ nameSpan.textContent = labels.get(rid) ?? rid;
6455
+ item.appendChild(nameSpan);
6456
+ dropdown.appendChild(item);
6457
+ });
6458
+ dropdown.onmousemove = (e) => {
6459
+ const target = e.target.closest?.(
6460
+ ".fb-richinput-dropdown-item"
6461
+ );
6462
+ if (!target) return;
6463
+ const newIdx = filtered.indexOf(target.dataset.rid ?? "");
6464
+ if (newIdx === -1 || newIdx === dropdownState.selectedIndex) return;
6465
+ const items = dropdown.querySelectorAll(
6466
+ ".fb-richinput-dropdown-item"
6467
+ );
6468
+ items.forEach((el, i) => {
6469
+ el.style.background = i === newIdx ? "var(--fb-background-hover-color, #f3f4f6)" : "transparent";
6470
+ });
6471
+ dropdownState.selectedIndex = newIdx;
6472
+ };
6473
+ dropdown.onmousedown = (e) => {
6474
+ e.preventDefault();
6475
+ e.stopPropagation();
6476
+ const target = e.target.closest?.(
6477
+ ".fb-richinput-dropdown-item"
6478
+ );
6479
+ if (!target?.dataset.rid) return;
6480
+ insertMention(target.dataset.rid);
6481
+ };
6482
+ dropdown.style.display = "block";
6483
+ dropdownState.open = true;
6484
+ }
6485
+ function openDropdown() {
6486
+ const trigger = getAtTrigger(textarea);
6487
+ if (!trigger) {
6488
+ closeDropdown();
6489
+ return;
6490
+ }
6491
+ dropdownState.query = trigger.query;
6492
+ dropdownState.triggerPos = trigger.pos;
6493
+ dropdownState.selectedIndex = 0;
6494
+ const labels = buildFileLabelsFromClosure();
6495
+ const filtered = filterFilesForDropdown(trigger.query, files, labels);
6496
+ renderDropdownItems(filtered);
6497
+ }
6498
+ function closeDropdown() {
6499
+ dropdown.style.display = "none";
6500
+ dropdownState.open = false;
6501
+ }
6502
+ function insertMention(rid) {
6503
+ const labels = buildFileLabelsFromClosure();
6504
+ const label = labels.get(rid) ?? state.resourceIndex.get(rid)?.name ?? rid;
6505
+ const cursorPos = textarea.selectionStart ?? 0;
6506
+ const before = textarea.value.slice(0, dropdownState.triggerPos);
6507
+ const after = textarea.value.slice(cursorPos);
6508
+ const mention = `${formatMention(label)} `;
6509
+ textarea.value = `${before}${mention}${after}`;
6510
+ const newPos = before.length + mention.length;
6511
+ textarea.setSelectionRange(newPos, newPos);
6512
+ textarea.dispatchEvent(new Event("input"));
6513
+ closeDropdown();
6514
+ }
6515
+ textarea.addEventListener("input", () => {
6516
+ updateBackdrop();
6517
+ writeHidden();
6518
+ ctx.instance?.triggerOnChange(pathKey, getCurrentValue());
6519
+ if (files.length > 0) {
6520
+ openDropdown();
6521
+ } else {
6522
+ closeDropdown();
6523
+ }
6524
+ });
6525
+ function updateDropdownHighlight() {
6526
+ const items = dropdown.querySelectorAll(
6527
+ ".fb-richinput-dropdown-item"
6528
+ );
6529
+ items.forEach((el, i) => {
6530
+ el.style.background = i === dropdownState.selectedIndex ? "var(--fb-background-hover-color, #f3f4f6)" : "transparent";
6531
+ });
6532
+ }
6533
+ textarea.addEventListener("keydown", (e) => {
6534
+ if (!dropdownState.open) return;
6535
+ const labels = buildFileLabelsFromClosure();
6536
+ const filtered = filterFilesForDropdown(
6537
+ dropdownState.query,
6538
+ files,
6539
+ labels
6540
+ );
6541
+ if (e.key === "ArrowDown") {
6542
+ e.preventDefault();
6543
+ dropdownState.selectedIndex = Math.min(
6544
+ dropdownState.selectedIndex + 1,
6545
+ filtered.length - 1
6546
+ );
6547
+ updateDropdownHighlight();
6548
+ } else if (e.key === "ArrowUp") {
6549
+ e.preventDefault();
6550
+ dropdownState.selectedIndex = Math.max(
6551
+ dropdownState.selectedIndex - 1,
6552
+ 0
6553
+ );
6554
+ updateDropdownHighlight();
6555
+ } else if (e.key === "Enter" && filtered.length > 0) {
6556
+ e.preventDefault();
6557
+ insertMention(filtered[dropdownState.selectedIndex]);
6558
+ } else if (e.key === "Escape") {
6559
+ closeDropdown();
6560
+ }
6561
+ });
6562
+ document.addEventListener(
6563
+ "click",
6564
+ (e) => {
6565
+ if (!outerDiv.contains(e.target) && !dropdown.contains(e.target)) {
6566
+ closeDropdown();
6567
+ }
6568
+ },
6569
+ { signal: docListenerCtrl.signal }
6570
+ );
6571
+ function renderFilesRow() {
6572
+ clear(filesRow);
6573
+ if (files.length === 0) {
6574
+ filesRow.style.display = "none";
6575
+ return;
6576
+ }
6577
+ filesRow.style.display = "flex";
6578
+ files.forEach((rid) => {
6579
+ const meta = state.resourceIndex.get(rid);
6580
+ const thumbWrapper = document.createElement("div");
6581
+ thumbWrapper.className = "fb-richinput-file-thumb";
6582
+ thumbWrapper.style.cssText = `
6583
+ position: relative;
6584
+ width: 48px;
6585
+ height: 48px;
6586
+ border: 1px solid var(--fb-border-color, #d1d5db);
6587
+ border-radius: 8px;
6588
+ overflow: hidden;
6589
+ flex-shrink: 0;
6590
+ cursor: pointer;
6591
+ background: #fff;
6592
+ `;
6593
+ const thumbInner = document.createElement("div");
6594
+ thumbInner.style.cssText = "width: 48px; height: 48px; border-radius: inherit; overflow: hidden;";
6595
+ renderThumbContent(thumbInner, rid, meta, state);
6596
+ thumbWrapper.appendChild(thumbInner);
6597
+ const tooltipHandle = createTooltipHandle();
6598
+ const doMention = () => {
6599
+ const cursorPos = textarea.selectionStart ?? textarea.value.length;
6600
+ const labels = buildFileLabelsFromClosure();
6601
+ const label = labels.get(rid) ?? meta?.name ?? rid;
6602
+ const before = textarea.value.slice(0, cursorPos);
6603
+ const after = textarea.value.slice(cursorPos);
6604
+ const prefix = before.length > 0 && !/[\s\n]$/.test(before) ? "\n" : "";
6605
+ const mention = `${prefix}${formatMention(label)} `;
6606
+ textarea.value = `${before}${mention}${after}`;
6607
+ const newPos = cursorPos + mention.length;
6608
+ textarea.setSelectionRange(newPos, newPos);
6609
+ textarea.focus();
6610
+ textarea.dispatchEvent(new Event("input"));
6611
+ };
6612
+ const doRemove = () => {
6613
+ const idx = files.indexOf(rid);
6614
+ if (idx !== -1) files.splice(idx, 1);
6615
+ renderFilesRow();
6616
+ updateBackdrop();
6617
+ writeHidden();
6618
+ ctx.instance?.triggerOnChange(pathKey, getCurrentValue());
6619
+ };
6620
+ thumbWrapper.addEventListener("mouseenter", () => {
6621
+ cancelHideTooltip(tooltipHandle);
6622
+ if (!tooltipHandle.element) {
6623
+ tooltipHandle.element = showFileTooltip(thumbWrapper, {
6624
+ rid,
6625
+ state,
6626
+ isReadonly: false,
6627
+ onMention: doMention,
6628
+ onRemove: doRemove
6629
+ });
6630
+ tooltipHandle.element.addEventListener("mouseenter", () => {
6631
+ cancelHideTooltip(tooltipHandle);
6632
+ });
6633
+ tooltipHandle.element.addEventListener("mouseleave", () => {
6634
+ scheduleHideTooltip(tooltipHandle);
6635
+ });
6636
+ }
6637
+ });
6638
+ thumbWrapper.addEventListener("mouseleave", () => {
6639
+ scheduleHideTooltip(tooltipHandle);
6640
+ });
6641
+ filesRow.appendChild(thumbWrapper);
6642
+ });
6643
+ }
6644
+ function uploadFile(file) {
6645
+ if (!state.config.uploadFile) return;
6646
+ const tempId = `temp-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
6647
+ state.resourceIndex.set(tempId, {
6648
+ name: file.name,
6649
+ type: file.type,
6650
+ size: file.size,
6651
+ uploadedAt: /* @__PURE__ */ new Date(),
6652
+ file
6653
+ });
6654
+ files.push(tempId);
6655
+ renderFilesRow();
6656
+ const thumbs = filesRow.querySelectorAll(
6657
+ ".fb-richinput-file-thumb"
6658
+ );
6659
+ const loadingThumb = thumbs[thumbs.length - 1];
6660
+ if (loadingThumb) loadingThumb.style.opacity = "0.5";
6661
+ state.config.uploadFile(file).then((resourceId) => {
6662
+ const idx = files.indexOf(tempId);
6663
+ if (idx !== -1) files[idx] = resourceId;
6664
+ state.resourceIndex.delete(tempId);
6665
+ state.resourceIndex.set(resourceId, {
6666
+ name: file.name,
6667
+ type: file.type,
6668
+ size: file.size,
6669
+ uploadedAt: /* @__PURE__ */ new Date(),
6670
+ file
6671
+ });
6672
+ renderFilesRow();
6673
+ updateBackdrop();
6674
+ writeHidden();
6675
+ ctx.instance?.triggerOnChange(pathKey, getCurrentValue());
6676
+ }).catch((err) => {
6677
+ const idx = files.indexOf(tempId);
6678
+ if (idx !== -1) files.splice(idx, 1);
6679
+ state.resourceIndex.delete(tempId);
6680
+ renderFilesRow();
6681
+ state.config.onUploadError?.(err, file);
6682
+ });
6683
+ }
6684
+ fileInput.addEventListener("change", () => {
6685
+ const selected = fileInput.files;
6686
+ if (!selected || selected.length === 0) return;
6687
+ const maxFiles = element.maxFiles ?? Infinity;
6688
+ for (let i = 0; i < selected.length && files.length < maxFiles; i++) {
6689
+ uploadFile(selected[i]);
6690
+ }
6691
+ fileInput.value = "";
6692
+ });
6693
+ textareaArea.appendChild(backdrop);
6694
+ textareaArea.appendChild(textarea);
6695
+ textareaArea.appendChild(paperclipBtn);
6696
+ textareaArea.appendChild(dropdown);
6697
+ outerDiv.appendChild(filesRow);
6698
+ outerDiv.appendChild(textareaArea);
6699
+ if (element.minLength != null || element.maxLength != null) {
6700
+ const counterRow = document.createElement("div");
6701
+ counterRow.style.cssText = "position: relative; padding: 2px 14px 6px; text-align: right;";
6702
+ const counter = createCharCounter(element, textarea, false);
6703
+ counter.style.cssText = `
6704
+ position: static;
6705
+ display: inline-block;
6706
+ font-size: var(--fb-font-size-small);
6707
+ color: var(--fb-text-secondary-color);
6708
+ pointer-events: none;
6709
+ `;
6710
+ counterRow.appendChild(counter);
6711
+ outerDiv.appendChild(counterRow);
6712
+ }
6713
+ outerDiv.appendChild(hiddenInput);
6714
+ outerDiv.appendChild(fileInput);
6715
+ writeHidden();
6716
+ updateBackdrop();
6717
+ hiddenInput._applyExternalUpdate = (value) => {
6718
+ const rawText = value.text ?? "";
6719
+ textarea.value = rawText ? replaceRidsWithFilenames(rawText, files, state) : "";
6720
+ textarea.dispatchEvent(new Event("input"));
6721
+ files.length = 0;
6722
+ for (const rid of value.files) files.push(rid);
6723
+ renderFilesRow();
6724
+ updateBackdrop();
6725
+ writeHidden();
6726
+ };
6727
+ wrapper.appendChild(outerDiv);
6728
+ renderFilesRow();
6729
+ const observer = new MutationObserver(() => {
6730
+ if (!outerDiv.isConnected) {
6731
+ docListenerCtrl.abort();
6732
+ mentionTooltip = removePortalTooltip(mentionTooltip);
6733
+ observer.disconnect();
6734
+ }
6735
+ });
6736
+ if (outerDiv.parentElement) {
6737
+ observer.observe(outerDiv.parentElement, { childList: true });
6738
+ }
6739
+ }
6740
+ function renderReadonlyMode(_element, ctx, wrapper, _pathKey, value) {
6741
+ const state = ctx.state;
6742
+ const { text, files } = value;
6743
+ const ridToName = /* @__PURE__ */ new Map();
6744
+ for (const rid of files) {
6745
+ const meta = state.resourceIndex.get(rid);
6746
+ if (meta?.name) ridToName.set(rid, meta.name);
6747
+ }
6748
+ if (files.length > 0) {
6749
+ const filesRow = document.createElement("div");
6750
+ filesRow.style.cssText = "display: flex; flex-wrap: wrap; gap: 6px; padding-bottom: 8px;";
6751
+ files.forEach((rid) => {
6752
+ const meta = state.resourceIndex.get(rid);
6753
+ const thumbWrapper = document.createElement("div");
6754
+ thumbWrapper.style.cssText = `
6755
+ position: relative;
6756
+ width: 48px; height: 48px;
6757
+ border: 1px solid var(--fb-border-color, #d1d5db);
6758
+ border-radius: 8px;
6759
+ overflow: hidden;
6760
+ flex-shrink: 0;
6761
+ background: #fff;
6762
+ cursor: default;
6763
+ `;
6764
+ const thumbInner = document.createElement("div");
6765
+ thumbInner.style.cssText = "width: 48px; height: 48px; border-radius: inherit; overflow: hidden;";
6766
+ renderThumbContent(thumbInner, rid, meta, state);
6767
+ thumbWrapper.appendChild(thumbInner);
6768
+ const tooltipHandle = createTooltipHandle();
6769
+ thumbWrapper.addEventListener("mouseenter", () => {
6770
+ cancelHideTooltip(tooltipHandle);
6771
+ if (!tooltipHandle.element) {
6772
+ tooltipHandle.element = showFileTooltip(thumbWrapper, {
6773
+ rid,
6774
+ state,
6775
+ isReadonly: true
6776
+ });
6777
+ tooltipHandle.element.addEventListener("mouseenter", () => {
6778
+ cancelHideTooltip(tooltipHandle);
6779
+ });
6780
+ tooltipHandle.element.addEventListener("mouseleave", () => {
6781
+ scheduleHideTooltip(tooltipHandle);
6782
+ });
6783
+ }
6784
+ });
6785
+ thumbWrapper.addEventListener("mouseleave", () => {
6786
+ scheduleHideTooltip(tooltipHandle);
6787
+ });
6788
+ filesRow.appendChild(thumbWrapper);
6789
+ });
6790
+ wrapper.appendChild(filesRow);
6791
+ }
6792
+ if (text) {
6793
+ const textDiv = document.createElement("div");
6794
+ textDiv.style.cssText = `
6795
+ ${TEXTAREA_FONT}
6796
+ color: var(--fb-text-color, #111827);
6797
+ white-space: pre-wrap;
6798
+ word-break: break-word;
6799
+ `;
6800
+ const tokens = findAtTokens(text);
6801
+ const resolvedTokens = tokens.filter(
6802
+ (tok) => ridToName.has(tok.name) || [...ridToName.values()].includes(tok.name)
6803
+ );
6804
+ if (resolvedTokens.length === 0) {
6805
+ textDiv.textContent = text;
6806
+ } else {
6807
+ let lastIndex = 0;
6808
+ for (const token of resolvedTokens) {
6809
+ if (token.start > lastIndex) {
6810
+ textDiv.appendChild(
6811
+ document.createTextNode(text.slice(lastIndex, token.start))
6812
+ );
6813
+ }
6814
+ const span = document.createElement("span");
6815
+ span.style.cssText = `
6816
+ display: inline;
6817
+ background: color-mix(in srgb, var(--fb-primary-color, #0066cc) 15%, transparent);
6818
+ color: var(--fb-primary-color, #0066cc);
6819
+ border-radius: 8px;
6820
+ padding: 1px 6px;
6821
+ font-weight: 500;
6822
+ cursor: default;
6823
+ `;
6824
+ const rid = ridToName.has(token.name) ? token.name : [...ridToName.entries()].find(([, n]) => n === token.name)?.[0];
6825
+ const displayName = ridToName.get(token.name) ?? token.name;
6826
+ span.textContent = `@${displayName}`;
6827
+ if (rid) {
6828
+ let spanTooltip = null;
6829
+ const mentionRid = rid;
6830
+ span.addEventListener("mouseenter", () => {
6831
+ spanTooltip = removePortalTooltip(spanTooltip);
6832
+ spanTooltip = showMentionTooltip(span, mentionRid, state);
6833
+ });
6834
+ span.addEventListener("mouseleave", () => {
6835
+ spanTooltip = removePortalTooltip(spanTooltip);
6836
+ });
6837
+ }
6838
+ textDiv.appendChild(span);
6839
+ lastIndex = token.end;
6840
+ }
6841
+ if (lastIndex < text.length) {
6842
+ textDiv.appendChild(document.createTextNode(text.slice(lastIndex)));
6843
+ }
6844
+ }
6845
+ wrapper.appendChild(textDiv);
6846
+ }
6847
+ if (!text && files.length === 0) {
6848
+ const empty = document.createElement("div");
6849
+ empty.style.cssText = "color: var(--fb-text-muted-color, #6b7280); font-size: var(--fb-font-size, 14px);";
6850
+ empty.textContent = "\u2014";
6851
+ wrapper.appendChild(empty);
6852
+ }
6853
+ }
6854
+ function renderRichInputElement(element, ctx, wrapper, pathKey) {
6855
+ const state = ctx.state;
6856
+ const textKey = element.textKey ?? "text";
6857
+ const filesKey = element.filesKey ?? "files";
6858
+ const rawPrefill = ctx.prefill[element.key];
6859
+ let initialValue;
6860
+ if (rawPrefill && typeof rawPrefill === "object" && !Array.isArray(rawPrefill)) {
6861
+ const obj = rawPrefill;
6862
+ const textVal = obj[textKey] ?? obj["text"];
6863
+ const filesVal = obj[filesKey] ?? obj["files"];
6864
+ initialValue = {
6865
+ text: typeof textVal === "string" ? textVal : null,
6866
+ files: Array.isArray(filesVal) ? filesVal : []
6867
+ };
6868
+ } else if (typeof rawPrefill === "string") {
6869
+ initialValue = { text: rawPrefill || null, files: [] };
6870
+ } else {
6871
+ initialValue = { text: null, files: [] };
6872
+ }
6873
+ for (const rid of initialValue.files) {
6874
+ if (!state.resourceIndex.has(rid)) {
6875
+ state.resourceIndex.set(rid, {
6876
+ name: rid,
6877
+ type: "application/octet-stream",
6878
+ size: 0,
6879
+ uploadedAt: /* @__PURE__ */ new Date(),
6880
+ file: void 0
6881
+ });
6882
+ }
6883
+ }
6884
+ if (state.config.readonly) {
6885
+ renderReadonlyMode(element, ctx, wrapper, pathKey, initialValue);
6886
+ } else {
6887
+ if (!state.config.uploadFile) {
6888
+ throw new Error(
6889
+ `RichInput field "${element.key}" requires uploadFile handler in config`
6890
+ );
6891
+ }
6892
+ renderEditMode(element, ctx, wrapper, pathKey, initialValue);
6893
+ }
6894
+ }
6895
+ function validateRichInputElement(element, key, context) {
6896
+ const { scopeRoot, state, skipValidation } = context;
6897
+ const errors = [];
6898
+ const textKey = element.textKey ?? "text";
6899
+ const filesKey = element.filesKey ?? "files";
6900
+ const hiddenInput = scopeRoot.querySelector(
6901
+ `[name="${key}"]`
6902
+ );
6903
+ if (!hiddenInput) {
6904
+ return { value: null, errors };
6905
+ }
6906
+ let rawValue = {};
6907
+ try {
6908
+ const parsed = JSON.parse(hiddenInput.value);
6909
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
6910
+ rawValue = parsed;
6911
+ } else {
6912
+ errors.push(`${key}: invalid richinput data`);
6913
+ return { value: null, errors };
6914
+ }
6915
+ } catch {
6916
+ errors.push(`${key}: invalid richinput data`);
6917
+ return { value: null, errors };
6918
+ }
6919
+ const textVal = rawValue[textKey];
6920
+ const filesVal = rawValue[filesKey];
6921
+ const text = textVal === null || typeof textVal === "string" ? textVal : null;
6922
+ const files = Array.isArray(filesVal) ? filesVal : [];
6923
+ const value = {
6924
+ [textKey]: text ?? null,
6925
+ [filesKey]: files
6926
+ };
6927
+ if (!skipValidation) {
6928
+ const textEmpty = !text || text.trim() === "";
6929
+ const filesEmpty = files.length === 0;
6930
+ if (element.required && textEmpty && filesEmpty) {
6931
+ errors.push(`${key}: ${t("required", state)}`);
6932
+ }
6933
+ if (!textEmpty && text) {
6934
+ if (element.minLength != null && text.length < element.minLength) {
6935
+ errors.push(
6936
+ `${key}: ${t("minLength", state, { min: element.minLength })}`
6937
+ );
6938
+ }
6939
+ if (element.maxLength != null && text.length > element.maxLength) {
6940
+ errors.push(
6941
+ `${key}: ${t("maxLength", state, { max: element.maxLength })}`
6942
+ );
6943
+ }
6944
+ }
6945
+ if (element.maxFiles != null && files.length > element.maxFiles) {
6946
+ errors.push(
6947
+ `${key}: ${t("maxFiles", state, { max: element.maxFiles })}`
6948
+ );
6949
+ }
6950
+ }
6951
+ return { value, errors };
6952
+ }
6953
+ function updateRichInputField(element, fieldPath, value, context) {
6954
+ const { scopeRoot } = context;
6955
+ const hiddenInput = scopeRoot.querySelector(
6956
+ `[name="${fieldPath}"]`
6957
+ );
6958
+ if (!hiddenInput) {
6959
+ console.warn(
6960
+ `updateRichInputField: no hidden input found for "${fieldPath}". Re-render to reflect new data.`
6961
+ );
6962
+ return;
6963
+ }
6964
+ let normalized = null;
6965
+ if (value && typeof value === "object" && !Array.isArray(value)) {
6966
+ const obj = value;
6967
+ const textKey = element.textKey ?? "text";
6968
+ const filesKey = element.filesKey ?? "files";
6969
+ const textVal = obj[textKey] ?? obj["text"];
6970
+ const filesVal = obj[filesKey] ?? obj["files"];
6971
+ if (textVal !== void 0 || filesVal !== void 0) {
6972
+ normalized = {
6973
+ text: typeof textVal === "string" ? textVal : null,
6974
+ files: Array.isArray(filesVal) ? filesVal : []
6975
+ };
6976
+ }
6977
+ }
6978
+ if (normalized && hiddenInput._applyExternalUpdate) {
6979
+ hiddenInput._applyExternalUpdate(normalized);
6980
+ } else if (normalized) {
6981
+ hiddenInput.value = JSON.stringify(normalized);
6982
+ }
6983
+ }
6984
+
5713
6985
  // src/components/index.ts
5714
6986
  function showTooltip(tooltipId, button) {
5715
6987
  const tooltip = document.getElementById(tooltipId);
@@ -6059,6 +7331,9 @@ function dispatchToRenderer(element, ctx, wrapper, pathKey) {
6059
7331
  case "table":
6060
7332
  renderTableElement(element, ctx, wrapper, pathKey);
6061
7333
  break;
7334
+ case "richinput":
7335
+ renderRichInputElement(element, ctx, wrapper, pathKey);
7336
+ break;
6062
7337
  default: {
6063
7338
  const unsupported = document.createElement("div");
6064
7339
  unsupported.className = "text-red-500 text-sm";
@@ -6155,7 +7430,11 @@ var defaultConfig = {
6155
7430
  tableRemoveRow: "Remove row",
6156
7431
  tableRemoveColumn: "Remove column",
6157
7432
  tableMergeCells: "Merge cells (Ctrl+M)",
6158
- tableSplitCell: "Split cell (Ctrl+Shift+M)"
7433
+ tableSplitCell: "Split cell (Ctrl+Shift+M)",
7434
+ richinputPlaceholder: "Type text...",
7435
+ richinputAttachFile: "Attach file",
7436
+ richinputMention: "Mention",
7437
+ richinputRemoveFile: "Remove"
6159
7438
  },
6160
7439
  ru: {
6161
7440
  // UI texts
@@ -6207,7 +7486,11 @@ var defaultConfig = {
6207
7486
  tableRemoveRow: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0441\u0442\u0440\u043E\u043A\u0443",
6208
7487
  tableRemoveColumn: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0441\u0442\u043E\u043B\u0431\u0435\u0446",
6209
7488
  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)"
7489
+ tableSplitCell: "\u0420\u0430\u0437\u0434\u0435\u043B\u0438\u0442\u044C \u044F\u0447\u0435\u0439\u043A\u0443 (Ctrl+Shift+M)",
7490
+ richinputPlaceholder: "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442...",
7491
+ richinputAttachFile: "\u041F\u0440\u0438\u043A\u0440\u0435\u043F\u0438\u0442\u044C \u0444\u0430\u0439\u043B",
7492
+ richinputMention: "\u0423\u043F\u043E\u043C\u044F\u043D\u0443\u0442\u044C",
7493
+ richinputRemoveFile: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C"
6211
7494
  }
6212
7495
  },
6213
7496
  theme: {}
@@ -6485,6 +7768,10 @@ var componentRegistry = {
6485
7768
  table: {
6486
7769
  validate: validateTableElement,
6487
7770
  update: updateTableField
7771
+ },
7772
+ richinput: {
7773
+ validate: validateRichInputElement,
7774
+ update: updateRichInputField
6488
7775
  }
6489
7776
  };
6490
7777
  function getComponentOperations(elementType) {