@underverse-ui/underverse 1.0.184 → 1.0.185

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/index.cjs CHANGED
@@ -1086,6 +1086,11 @@ var en_default = {
1086
1086
  borderColor: "Border Color",
1087
1087
  clearBorder: "Clear Border",
1088
1088
  formula: "Formula",
1089
+ formulaBar: "Formula bar",
1090
+ formulaError: "Formula error",
1091
+ editFormula: "Edit formula",
1092
+ clearCell: "Clear cell",
1093
+ convertToValue: "Convert to value",
1089
1094
  numberFormat: "Number Format",
1090
1095
  formatText: "Text",
1091
1096
  formatDate: "Date",
@@ -1508,6 +1513,11 @@ var vi_default = {
1508
1513
  borderColor: "M\xE0u vi\u1EC1n",
1509
1514
  clearBorder: "X\xF3a vi\u1EC1n",
1510
1515
  formula: "C\xF4ng th\u1EE9c",
1516
+ formulaBar: "Thanh c\xF4ng th\u1EE9c",
1517
+ formulaError: "L\u1ED7i c\xF4ng th\u1EE9c",
1518
+ editFormula: "S\u1EEDa c\xF4ng th\u1EE9c",
1519
+ clearCell: "X\xF3a n\u1ED9i dung \xF4",
1520
+ convertToValue: "Chuy\u1EC3n th\xE0nh gi\xE1 tr\u1ECB",
1511
1521
  numberFormat: "\u0110\u1ECBnh d\u1EA1ng s\u1ED1",
1512
1522
  formatText: "Ch\u1EEF",
1513
1523
  formatDate: "Ng\xE0y",
@@ -1930,6 +1940,11 @@ var ko_default = {
1930
1940
  borderColor: "\uD14C\uB450\uB9AC \uC0C9\uC0C1",
1931
1941
  clearBorder: "\uD14C\uB450\uB9AC \uC9C0\uC6B0\uAE30",
1932
1942
  formula: "\uC218\uC2DD",
1943
+ formulaBar: "\uC218\uC2DD \uC785\uB825\uC904",
1944
+ formulaError: "\uC218\uC2DD \uC624\uB958",
1945
+ editFormula: "\uC218\uC2DD \uD3B8\uC9D1",
1946
+ clearCell: "\uC140 \uB0B4\uC6A9 \uC0AD\uC81C",
1947
+ convertToValue: "\uAC12\uC73C\uB85C \uBCC0\uD658",
1933
1948
  numberFormat: "\uD45C\uC2DC \uD615\uC2DD",
1934
1949
  formatText: "\uD14D\uC2A4\uD2B8",
1935
1950
  formatDate: "\uB0A0\uC9DC",
@@ -2352,6 +2367,11 @@ var ja_default = {
2352
2367
  borderColor: "\u67A0\u7DDA\u306E\u8272",
2353
2368
  clearBorder: "\u67A0\u7DDA\u3092\u30AF\u30EA\u30A2",
2354
2369
  formula: "\u6570\u5F0F",
2370
+ formulaBar: "\u6570\u5F0F\u30D0\u30FC",
2371
+ formulaError: "\u6570\u5F0F\u30A8\u30E9\u30FC",
2372
+ editFormula: "\u6570\u5F0F\u3092\u7DE8\u96C6",
2373
+ clearCell: "\u30BB\u30EB\u3092\u30AF\u30EA\u30A2",
2374
+ convertToValue: "\u5024\u306B\u5909\u63DB",
2355
2375
  numberFormat: "\u8868\u793A\u5F62\u5F0F",
2356
2376
  formatText: "\u30C6\u30AD\u30B9\u30C8",
2357
2377
  formatDate: "\u65E5\u4ED8",
@@ -27548,8 +27568,8 @@ function useLocale2() {
27548
27568
  }
27549
27569
 
27550
27570
  // src/components/UEditor/UEditor.tsx
27551
- var import_react80 = __toESM(require("react"), 1);
27552
- var import_react81 = require("@tiptap/react");
27571
+ var import_react82 = __toESM(require("react"), 1);
27572
+ var import_react83 = require("@tiptap/react");
27553
27573
 
27554
27574
  // src/components/UEditor/extensions.ts
27555
27575
  var import_core16 = require("@tiptap/core");
@@ -27711,6 +27731,54 @@ var import_react49 = require("@tiptap/react");
27711
27731
  var import_react47 = __toESM(require("react"), 1);
27712
27732
  var import_react48 = require("@tiptap/react");
27713
27733
  var import_lucide_react43 = require("lucide-react");
27734
+
27735
+ // src/components/UEditor/url-safety.ts
27736
+ var LINK_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:"]);
27737
+ var IMAGE_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
27738
+ var FILE_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:", "blob:"]);
27739
+ function normalizeUrlInput(raw) {
27740
+ return raw.trim().replace(/[\u0000-\u001F\u007F\s]+/g, "");
27741
+ }
27742
+ function isProtocolRelativeUrl(value) {
27743
+ return value.startsWith("//");
27744
+ }
27745
+ function isRelativeUrl(value) {
27746
+ return value.startsWith("/") || value.startsWith("./") || value.startsWith("../") || value.startsWith("#");
27747
+ }
27748
+ function isDataImageUrl(value) {
27749
+ return /^data:image\/(?:png|jpe?g|gif|webp|svg\+xml|bmp|x-icon|avif);base64,/i.test(value);
27750
+ }
27751
+ function isDataFileUrl(value) {
27752
+ return /^data:[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*(?:;[a-z0-9!#$&^_.+-]+=[^;,]*)*;base64,[a-z0-9+/]*={0,2}$/i.test(value);
27753
+ }
27754
+ function isSafeUEditorUrl(raw, kind) {
27755
+ const value = normalizeUrlInput(raw);
27756
+ if (!value) return false;
27757
+ if (kind === "image" && isDataImageUrl(value)) return true;
27758
+ if (kind === "file" && isDataFileUrl(value)) return true;
27759
+ if (isProtocolRelativeUrl(value)) return false;
27760
+ if (isRelativeUrl(value)) return true;
27761
+ try {
27762
+ const parsed = new URL(value);
27763
+ if (kind === "image") return IMAGE_PROTOCOLS.has(parsed.protocol);
27764
+ if (kind === "file") return FILE_PROTOCOLS.has(parsed.protocol);
27765
+ return LINK_PROTOCOLS.has(parsed.protocol);
27766
+ } catch {
27767
+ return false;
27768
+ }
27769
+ }
27770
+ function sanitizeUEditorUrl(raw, kind) {
27771
+ const value = raw.trim();
27772
+ if (!value) return "";
27773
+ if (isSafeUEditorUrl(value, kind)) return normalizeUrlInput(value);
27774
+ if (kind === "link" && !isProtocolRelativeUrl(value) && !/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(value)) {
27775
+ const withProtocol = `https://${value}`;
27776
+ return isSafeUEditorUrl(withProtocol, kind) ? withProtocol : "";
27777
+ }
27778
+ return "";
27779
+ }
27780
+
27781
+ // src/components/UEditor/BookmarkView.tsx
27714
27782
  var import_jsx_runtime80 = require("react/jsx-runtime");
27715
27783
  var BookmarkView = ({ node, updateAttributes, selected, editor }) => {
27716
27784
  const t = useSmartTranslations("UEditor");
@@ -27718,17 +27786,25 @@ var BookmarkView = ({ node, updateAttributes, selected, editor }) => {
27718
27786
  const [loading2, setLoading] = (0, import_react47.useState)(false);
27719
27787
  const [error, setError] = (0, import_react47.useState)(false);
27720
27788
  const [retryToken, setRetryToken] = (0, import_react47.useState)(0);
27721
- const fetchedRetryTokenRef = import_react47.default.useRef(-1);
27789
+ const lastFetchKeyRef = import_react47.default.useRef("");
27722
27790
  const fetchRequestIdRef = import_react47.default.useRef(0);
27791
+ const safeUrl = sanitizeUEditorUrl(String(url ?? ""), "link");
27792
+ const safeImage = sanitizeUEditorUrl(String(image ?? ""), "image");
27793
+ const clickToOpenNewTabMessage = t("bookmark.clickToOpenNewTab");
27794
+ const clickToOpenLinkMessage = t("bookmark.clickToOpenLink");
27795
+ const noDescriptionMessage = t("bookmark.noDescription");
27723
27796
  (0, import_react47.useEffect)(() => {
27724
27797
  if (title && retryToken === 0) return;
27725
- if (fetchedRetryTokenRef.current === retryToken) return;
27726
- fetchedRetryTokenRef.current = retryToken;
27798
+ if (!safeUrl) return;
27799
+ const fetchKey = `${safeUrl}\0${retryToken}`;
27800
+ if (lastFetchKeyRef.current === fetchKey) return;
27801
+ lastFetchKeyRef.current = fetchKey;
27727
27802
  const requestId = fetchRequestIdRef.current + 1;
27728
27803
  fetchRequestIdRef.current = requestId;
27804
+ let active = true;
27729
27805
  const safeUpdateAttributes = (attrs) => {
27730
27806
  Promise.resolve().then(() => {
27731
- if (fetchRequestIdRef.current === requestId && editor && !editor.isDestroyed) {
27807
+ if (active && fetchRequestIdRef.current === requestId && editor && !editor.isDestroyed) {
27732
27808
  updateAttributes(attrs);
27733
27809
  }
27734
27810
  });
@@ -27737,43 +27813,75 @@ var BookmarkView = ({ node, updateAttributes, selected, editor }) => {
27737
27813
  const fetchMetadata = extension?.options?.fetchMetadata;
27738
27814
  if (!fetchMetadata) {
27739
27815
  try {
27740
- const hostname = new URL(url).hostname;
27816
+ const hostname = new URL(safeUrl).hostname;
27741
27817
  safeUpdateAttributes({
27742
- title: hostname || url,
27743
- description: t("bookmark.clickToOpenNewTab"),
27818
+ title: hostname || safeUrl,
27819
+ description: clickToOpenNewTabMessage,
27744
27820
  publisher: hostname
27745
27821
  });
27746
27822
  } catch {
27747
27823
  safeUpdateAttributes({
27748
- title: url,
27749
- description: t("bookmark.clickToOpenLink"),
27824
+ title: safeUrl,
27825
+ description: clickToOpenLinkMessage,
27750
27826
  publisher: ""
27751
27827
  });
27752
27828
  }
27753
- return;
27829
+ return () => {
27830
+ active = false;
27831
+ };
27754
27832
  }
27755
- setLoading(true);
27756
- setError(false);
27757
- fetchMetadata(url).then((meta) => {
27758
- if (fetchRequestIdRef.current !== requestId) return;
27833
+ queueMicrotask(() => {
27834
+ if (!active) return;
27835
+ setLoading(true);
27836
+ setError(false);
27837
+ });
27838
+ fetchMetadata(safeUrl).then((meta) => {
27839
+ if (!active || fetchRequestIdRef.current !== requestId) return;
27840
+ const hostname = (() => {
27841
+ try {
27842
+ return new URL(safeUrl).hostname;
27843
+ } catch {
27844
+ return "";
27845
+ }
27846
+ })();
27759
27847
  safeUpdateAttributes({
27760
- title: meta.title || new URL(url).hostname || url,
27761
- description: meta.description || t("bookmark.noDescription"),
27762
- image: meta.image || "",
27763
- publisher: meta.publisher || new URL(url).hostname || ""
27848
+ title: meta.title || hostname || safeUrl,
27849
+ description: meta.description || noDescriptionMessage,
27850
+ image: meta.image ? sanitizeUEditorUrl(meta.image, "image") : "",
27851
+ publisher: meta.publisher || hostname
27764
27852
  });
27765
27853
  setLoading(false);
27766
27854
  }).catch(() => {
27767
- if (fetchRequestIdRef.current !== requestId) return;
27855
+ if (!active || fetchRequestIdRef.current !== requestId) return;
27768
27856
  setError(true);
27769
27857
  setLoading(false);
27770
27858
  });
27771
- return void 0;
27772
- }, [url, title, editor, updateAttributes, t, retryToken]);
27773
- const handleOpenLink = (e) => {
27774
- e.preventDefault();
27775
- e.stopPropagation();
27776
- window.open(url, "_blank", "noopener,noreferrer");
27859
+ return () => {
27860
+ active = false;
27861
+ };
27862
+ }, [
27863
+ safeUrl,
27864
+ title,
27865
+ editor,
27866
+ updateAttributes,
27867
+ retryToken,
27868
+ clickToOpenNewTabMessage,
27869
+ clickToOpenLinkMessage,
27870
+ noDescriptionMessage
27871
+ ]);
27872
+ const openLink = () => {
27873
+ if (safeUrl) window.open(safeUrl, "_blank", "noopener,noreferrer");
27874
+ };
27875
+ const handleOpenLink = (event) => {
27876
+ event.preventDefault();
27877
+ event.stopPropagation();
27878
+ openLink();
27879
+ };
27880
+ const handleKeyDown2 = (event) => {
27881
+ if (event.key !== "Enter") return;
27882
+ event.preventDefault();
27883
+ event.stopPropagation();
27884
+ openLink();
27777
27885
  };
27778
27886
  const handleRetryPreview = (event) => {
27779
27887
  event.preventDefault();
@@ -27782,7 +27890,7 @@ var BookmarkView = ({ node, updateAttributes, selected, editor }) => {
27782
27890
  };
27783
27891
  const displayPublisher = publisher || (() => {
27784
27892
  try {
27785
- return new URL(url).hostname;
27893
+ return safeUrl ? new URL(safeUrl).hostname : "";
27786
27894
  } catch {
27787
27895
  return "";
27788
27896
  }
@@ -27795,6 +27903,10 @@ var BookmarkView = ({ node, updateAttributes, selected, editor }) => {
27795
27903
  selected && "ring-2 ring-primary/45 border-primary/20 shadow-sm"
27796
27904
  ),
27797
27905
  onClick: handleOpenLink,
27906
+ onKeyDown: handleKeyDown2,
27907
+ role: "link",
27908
+ tabIndex: safeUrl ? 0 : -1,
27909
+ "aria-disabled": !safeUrl,
27798
27910
  children: loading2 ? /* @__PURE__ */ (0, import_jsx_runtime80.jsxs)("div", { className: "flex items-center justify-between p-4 gap-4 animate-pulse", children: [
27799
27911
  /* @__PURE__ */ (0, import_jsx_runtime80.jsxs)("div", { className: "flex-1 flex flex-col gap-2", children: [
27800
27912
  /* @__PURE__ */ (0, import_jsx_runtime80.jsx)("div", { className: "h-4 w-1/3 bg-muted-foreground/20 rounded" }),
@@ -27809,11 +27921,11 @@ var BookmarkView = ({ node, updateAttributes, selected, editor }) => {
27809
27921
  /* @__PURE__ */ (0, import_jsx_runtime80.jsx)(import_lucide_react43.Globe, { className: "h-3 w-3 flex-shrink-0" }),
27810
27922
  displayPublisher
27811
27923
  ] }),
27812
- /* @__PURE__ */ (0, import_jsx_runtime80.jsx)("h4", { className: "text-sm font-semibold text-foreground line-clamp-1 break-all", children: title || url }),
27924
+ /* @__PURE__ */ (0, import_jsx_runtime80.jsx)("h4", { className: "text-sm font-semibold text-foreground line-clamp-1 break-all", children: title || safeUrl || String(url ?? "") }),
27813
27925
  /* @__PURE__ */ (0, import_jsx_runtime80.jsx)("p", { className: "text-xs text-muted-foreground line-clamp-2 leading-relaxed", children: description })
27814
27926
  ] }),
27815
27927
  /* @__PURE__ */ (0, import_jsx_runtime80.jsxs)("span", { className: "text-[11px] text-primary/80 font-mono mt-2 truncate flex items-center gap-1", children: [
27816
- url,
27928
+ safeUrl || String(url ?? ""),
27817
27929
  /* @__PURE__ */ (0, import_jsx_runtime80.jsx)(import_lucide_react43.ExternalLink, { className: "h-3 w-3 flex-shrink-0" })
27818
27930
  ] }),
27819
27931
  error && /* @__PURE__ */ (0, import_jsx_runtime80.jsxs)("div", { className: "mt-2 flex flex-wrap items-center gap-2 text-xs text-destructive", children: [
@@ -27835,10 +27947,10 @@ var BookmarkView = ({ node, updateAttributes, selected, editor }) => {
27835
27947
  )
27836
27948
  ] })
27837
27949
  ] }),
27838
- image && /* @__PURE__ */ (0, import_jsx_runtime80.jsx)("div", { className: "relative w-full sm:w-44 h-32 sm:h-auto flex-shrink-0 bg-muted overflow-hidden border-t sm:border-t-0 sm:border-l border-border/50", children: /* @__PURE__ */ (0, import_jsx_runtime80.jsx)(
27950
+ safeImage && /* @__PURE__ */ (0, import_jsx_runtime80.jsx)("div", { className: "relative w-full sm:w-44 h-32 sm:h-auto flex-shrink-0 bg-muted overflow-hidden border-t sm:border-t-0 sm:border-l border-border/50", children: /* @__PURE__ */ (0, import_jsx_runtime80.jsx)(
27839
27951
  "img",
27840
27952
  {
27841
- src: image,
27953
+ src: safeImage,
27842
27954
  alt: title || "Link preview image",
27843
27955
  className: "absolute inset-0 w-full h-full object-cover transition-opacity duration-300",
27844
27956
  onError: (e) => {
@@ -27868,11 +27980,11 @@ var Bookmark = import_core2.Node.create({
27868
27980
  return {
27869
27981
  url: {
27870
27982
  default: "",
27871
- parseHTML: (element) => element.getAttribute("href") || element.getAttribute("data-url") || "",
27872
- renderHTML: (attributes) => ({
27873
- href: attributes.url,
27874
- "data-url": attributes.url
27875
- })
27983
+ parseHTML: (element) => sanitizeUEditorUrl(element.getAttribute("href") || element.getAttribute("data-url") || "", "link"),
27984
+ renderHTML: (attributes) => {
27985
+ const url = sanitizeUEditorUrl(String(attributes.url ?? ""), "link");
27986
+ return url ? { href: url, "data-url": url } : {};
27987
+ }
27876
27988
  },
27877
27989
  title: {
27878
27990
  default: "",
@@ -27890,10 +28002,11 @@ var Bookmark = import_core2.Node.create({
27890
28002
  },
27891
28003
  image: {
27892
28004
  default: "",
27893
- parseHTML: (element) => element.getAttribute("data-image") || "",
27894
- renderHTML: (attributes) => ({
27895
- "data-image": attributes.image
27896
- })
28005
+ parseHTML: (element) => sanitizeUEditorUrl(element.getAttribute("data-image") || "", "image"),
28006
+ renderHTML: (attributes) => {
28007
+ const image = sanitizeUEditorUrl(String(attributes.image ?? ""), "image");
28008
+ return image ? { "data-image": image } : {};
28009
+ }
27897
28010
  },
27898
28011
  publisher: {
27899
28012
  default: "",
@@ -27926,9 +28039,12 @@ var Bookmark = import_core2.Node.create({
27926
28039
  addCommands() {
27927
28040
  return {
27928
28041
  setBookmark: (attributes) => ({ commands }) => {
28042
+ const url = sanitizeUEditorUrl(attributes.url, "link");
28043
+ if (!url) return false;
28044
+ const image = attributes.image ? sanitizeUEditorUrl(attributes.image, "image") : "";
27929
28045
  return commands.insertContent({
27930
28046
  type: this.name,
27931
- attrs: attributes
28047
+ attrs: { ...attributes, url, image }
27932
28048
  });
27933
28049
  }
27934
28050
  };
@@ -27945,7 +28061,6 @@ var Bookmark = import_core2.Node.create({
27945
28061
  if (!text || !URL_REGEX.test(text)) return false;
27946
28062
  const { selection } = view.state;
27947
28063
  const parent = selection.$from.parent;
27948
- console.log("DEBUG handlePaste name:", parent.type.name, "size:", parent.content.size);
27949
28064
  const isEmptyParagraph = parent.type.name === "paragraph" && parent.content.size === 0;
27950
28065
  if (isEmptyParagraph) {
27951
28066
  event.preventDefault();
@@ -27973,15 +28088,15 @@ var import_react51 = require("@tiptap/react");
27973
28088
  var import_lucide_react44 = require("lucide-react");
27974
28089
  var import_jsx_runtime81 = require("react/jsx-runtime");
27975
28090
  function formatBytes(bytes, decimals = 1) {
27976
- if (!bytes || bytes <= 0) return "0 Bytes";
28091
+ if (!Number.isFinite(bytes) || bytes <= 0) return "0 Bytes";
27977
28092
  const k = 1024;
27978
28093
  const dm = decimals < 0 ? 0 : decimals;
27979
- const sizes = ["Bytes", "KB", "MB", "GB"];
27980
- const i = Math.floor(Math.log(bytes) / Math.log(k));
28094
+ const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB"];
28095
+ const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1);
27981
28096
  return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
27982
28097
  }
27983
28098
  function getFileIcon2(type) {
27984
- const mime = (type || "").toLowerCase();
28099
+ const mime = String(type || "").toLowerCase();
27985
28100
  if (mime.includes("pdf")) return import_lucide_react44.FileText;
27986
28101
  if (mime.startsWith("image/")) return import_lucide_react44.Image;
27987
28102
  if (mime.startsWith("audio/")) return import_lucide_react44.Music;
@@ -28007,11 +28122,13 @@ function dataURLtoFile(dataurl, filename, mimeType) {
28007
28122
  }
28008
28123
  }
28009
28124
  return new File([bytes], filename, { type: mime });
28010
- } catch (e) {
28011
- console.error("Error converting data URI to file:", e);
28125
+ } catch {
28012
28126
  return null;
28013
28127
  }
28014
28128
  }
28129
+ function getErrorMessage(error) {
28130
+ return error instanceof Error && error.message ? error.message : "Upload failed";
28131
+ }
28015
28132
  var FileCardView = ({ node, selected, editor, updateAttributes }) => {
28016
28133
  const t = useSmartTranslations("UEditor");
28017
28134
  const { src, fileName, fileSize, fileType } = node.attrs;
@@ -28019,49 +28136,64 @@ var FileCardView = ({ node, selected, editor, updateAttributes }) => {
28019
28136
  const [uploadError, setUploadError] = (0, import_react50.useState)(null);
28020
28137
  const [retryToken, setRetryToken] = (0, import_react50.useState)(0);
28021
28138
  const uploadAttemptedRef = (0, import_react50.useRef)(false);
28139
+ const lastUploadSrcRef = (0, import_react50.useRef)("");
28140
+ const uploadRequestIdRef = (0, import_react50.useRef)(0);
28141
+ const safeSrc = sanitizeUEditorUrl(String(src ?? ""), "file");
28142
+ const extension = editor?.extensionManager?.extensions?.find((item) => item.name === "fileCard");
28143
+ const uploadFn = extension?.options?.upload;
28022
28144
  (0, import_react50.useEffect)(() => {
28023
- if (!src || !src.startsWith("data:")) {
28145
+ if (lastUploadSrcRef.current !== safeSrc) {
28146
+ lastUploadSrcRef.current = safeSrc;
28147
+ uploadAttemptedRef.current = false;
28148
+ }
28149
+ if (!safeSrc || !safeSrc.startsWith("data:") || !uploadFn) {
28024
28150
  return;
28025
28151
  }
28026
- const extension = editor?.extensionManager?.extensions?.find((e) => e.name === "fileCard");
28027
- const uploadFn = extension?.options?.upload;
28028
- if (!uploadFn || uploadAttemptedRef.current || !updateAttributes) {
28152
+ if (uploadAttemptedRef.current || !updateAttributes) {
28029
28153
  return;
28030
28154
  }
28031
28155
  uploadAttemptedRef.current = true;
28032
28156
  setUploadError(null);
28157
+ const requestId = uploadRequestIdRef.current + 1;
28158
+ uploadRequestIdRef.current = requestId;
28159
+ let active = true;
28033
28160
  const runUpload = async () => {
28034
28161
  try {
28035
- const file = dataURLtoFile(src, fileName || "file", fileType);
28162
+ const file = dataURLtoFile(safeSrc, fileName || "file", fileType);
28036
28163
  if (!file) {
28037
28164
  throw new Error("Failed to parse file data");
28038
28165
  }
28039
28166
  const resolvedUrl = await uploadFn(file);
28167
+ const nextSrc = sanitizeUEditorUrl(String(resolvedUrl ?? ""), "file");
28168
+ if (!nextSrc) throw new Error("Upload handler returned an unsafe URL");
28040
28169
  Promise.resolve().then(() => {
28041
- if (editor && !editor.isDestroyed) {
28042
- updateAttributes({ src: resolvedUrl });
28170
+ if (active && uploadRequestIdRef.current === requestId && editor && !editor.isDestroyed) {
28171
+ updateAttributes({ src: nextSrc });
28043
28172
  }
28044
28173
  });
28045
- } catch (err) {
28046
- console.error("File upload failed:", err);
28047
- setUploadError(err?.message || "Upload failed");
28174
+ } catch (error) {
28175
+ if (!active || uploadRequestIdRef.current !== requestId) return;
28176
+ setUploadError(getErrorMessage(error));
28048
28177
  uploadAttemptedRef.current = false;
28049
28178
  }
28050
28179
  };
28051
28180
  void runUpload();
28052
- }, [src, fileName, fileType, editor, updateAttributes, retryToken]);
28181
+ return () => {
28182
+ active = false;
28183
+ };
28184
+ }, [safeSrc, fileName, fileType, editor, updateAttributes, uploadFn, retryToken]);
28053
28185
  const handleDownload = (e) => {
28054
28186
  e.preventDefault();
28055
28187
  e.stopPropagation();
28056
- if (!src) return;
28188
+ if (!safeSrc) return;
28057
28189
  const link = document.createElement("a");
28058
- link.href = src;
28190
+ link.href = safeSrc;
28059
28191
  link.download = fileName || "download";
28060
28192
  document.body.appendChild(link);
28061
28193
  link.click();
28062
28194
  document.body.removeChild(link);
28063
28195
  };
28064
- const isUploading = (src || "").startsWith("data:");
28196
+ const hasInlineUpload = safeSrc.startsWith("data:") && Boolean(uploadFn);
28065
28197
  const handleRetryUpload = (event) => {
28066
28198
  event.preventDefault();
28067
28199
  event.stopPropagation();
@@ -28084,11 +28216,11 @@ var FileCardView = ({ node, selected, editor, updateAttributes }) => {
28084
28216
  /* @__PURE__ */ (0, import_jsx_runtime81.jsx)("div", { className: "text-sm font-semibold text-foreground truncate break-all", children: fileName || t("fileCard.unnamed") }),
28085
28217
  /* @__PURE__ */ (0, import_jsx_runtime81.jsxs)("div", { className: "text-xs text-muted-foreground flex items-center gap-2 mt-0.5", children: [
28086
28218
  fileSize ? /* @__PURE__ */ (0, import_jsx_runtime81.jsx)("span", { children: formatBytes(fileSize) }) : null,
28087
- isUploading && /* @__PURE__ */ (0, import_jsx_runtime81.jsx)("span", { className: cn(
28219
+ hasInlineUpload && /* @__PURE__ */ (0, import_jsx_runtime81.jsx)("span", { className: cn(
28088
28220
  "font-medium truncate max-w-50",
28089
28221
  uploadError ? "text-destructive" : "text-primary animate-pulse"
28090
28222
  ), children: uploadError ? `${t("fileCard.uploadFailed") || "T\u1EA3i l\xEAn th\u1EA5t b\u1EA1i"}: ${uploadError}` : t("fileCard.uploading") }),
28091
- isUploading && uploadError && /* @__PURE__ */ (0, import_jsx_runtime81.jsxs)(
28223
+ hasInlineUpload && uploadError && /* @__PURE__ */ (0, import_jsx_runtime81.jsxs)(
28092
28224
  "button",
28093
28225
  {
28094
28226
  type: "button",
@@ -28103,7 +28235,7 @@ var FileCardView = ({ node, selected, editor, updateAttributes }) => {
28103
28235
  ] })
28104
28236
  ] })
28105
28237
  ] }),
28106
- src && /* @__PURE__ */ (0, import_jsx_runtime81.jsx)(
28238
+ safeSrc && /* @__PURE__ */ (0, import_jsx_runtime81.jsx)(
28107
28239
  "button",
28108
28240
  {
28109
28241
  type: "button",
@@ -28132,11 +28264,11 @@ var FileCard = import_core3.Node.create({
28132
28264
  return {
28133
28265
  src: {
28134
28266
  default: "",
28135
- parseHTML: (element) => element.getAttribute("href") || element.getAttribute("data-src") || "",
28136
- renderHTML: (attributes) => ({
28137
- href: attributes.src,
28138
- "data-src": attributes.src
28139
- })
28267
+ parseHTML: (element) => sanitizeUEditorUrl(element.getAttribute("href") || element.getAttribute("data-src") || "", "file"),
28268
+ renderHTML: (attributes) => {
28269
+ const src = sanitizeUEditorUrl(String(attributes.src ?? ""), "file");
28270
+ return src ? { href: src, "data-src": src } : {};
28271
+ }
28140
28272
  },
28141
28273
  fileName: {
28142
28274
  default: "",
@@ -28148,13 +28280,15 @@ var FileCard = import_core3.Node.create({
28148
28280
  fileSize: {
28149
28281
  default: null,
28150
28282
  parseHTML: (element) => {
28151
- const val = element.getAttribute("data-file-size");
28152
- return val ? Number(val) : null;
28283
+ const rawValue = element.getAttribute("data-file-size");
28284
+ const value = rawValue ? Number(rawValue) : Number.NaN;
28285
+ return Number.isFinite(value) && value >= 0 ? value : null;
28153
28286
  },
28154
28287
  renderHTML: (attributes) => {
28155
- if (attributes.fileSize === null || attributes.fileSize === void 0) return {};
28288
+ const fileSize = Number(attributes.fileSize);
28289
+ if (!Number.isFinite(fileSize) || fileSize < 0) return {};
28156
28290
  return {
28157
- "data-file-size": attributes.fileSize
28291
+ "data-file-size": fileSize
28158
28292
  };
28159
28293
  }
28160
28294
  },
@@ -28189,9 +28323,16 @@ var FileCard = import_core3.Node.create({
28189
28323
  addCommands() {
28190
28324
  return {
28191
28325
  setFileCard: (attributes) => ({ commands }) => {
28326
+ const src = sanitizeUEditorUrl(attributes.src, "file");
28327
+ if (!src) return false;
28328
+ const fileSize = Number(attributes.fileSize);
28192
28329
  return commands.insertContent({
28193
28330
  type: this.name,
28194
- attrs: attributes
28331
+ attrs: {
28332
+ ...attributes,
28333
+ src,
28334
+ fileSize: Number.isFinite(fileSize) && fileSize >= 0 ? fileSize : null
28335
+ }
28195
28336
  });
28196
28337
  }
28197
28338
  };
@@ -28605,6 +28746,10 @@ function destroyTippyInstance(instance) {
28605
28746
  if (!instance || instance.state.isDestroyed) return;
28606
28747
  instance.destroy();
28607
28748
  }
28749
+ function setTippyReferenceClientRect(instance, clientRect) {
28750
+ if (!instance || instance.state.isDestroyed) return;
28751
+ instance.setProps({ getReferenceClientRect: clientRect });
28752
+ }
28608
28753
 
28609
28754
  // src/components/UEditor/slash-command.tsx
28610
28755
  var import_jsx_runtime84 = require("react/jsx-runtime");
@@ -28977,9 +29122,7 @@ var SlashCommand = import_core6.Extension.create({
28977
29122
  if (!props.clientRect) {
28978
29123
  return;
28979
29124
  }
28980
- popup?.[0]?.setProps({
28981
- getReferenceClientRect: props.clientRect
28982
- });
29125
+ setTippyReferenceClientRect(getFirstTippyInstance(popup), props.clientRect);
28983
29126
  },
28984
29127
  onKeyDown(props) {
28985
29128
  if (!props || !props.event) return false;
@@ -29716,45 +29859,6 @@ function getClipboardTsvTableContent(dataTransfer) {
29716
29859
  );
29717
29860
  }
29718
29861
 
29719
- // src/components/UEditor/url-safety.ts
29720
- var LINK_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:"]);
29721
- var IMAGE_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
29722
- function normalizeUrlInput(raw) {
29723
- return raw.trim().replace(/[\u0000-\u001F\u007F\s]+/g, "");
29724
- }
29725
- function isProtocolRelativeUrl(value) {
29726
- return value.startsWith("//");
29727
- }
29728
- function isRelativeUrl(value) {
29729
- return value.startsWith("/") || value.startsWith("./") || value.startsWith("../") || value.startsWith("#");
29730
- }
29731
- function isDataImageUrl(value) {
29732
- return /^data:image\/(?:png|jpe?g|gif|webp|svg\+xml|bmp|x-icon|avif);base64,/i.test(value);
29733
- }
29734
- function isSafeUEditorUrl(raw, kind) {
29735
- const value = normalizeUrlInput(raw);
29736
- if (!value) return false;
29737
- if (kind === "image" && isDataImageUrl(value)) return true;
29738
- if (isRelativeUrl(value)) return true;
29739
- if (isProtocolRelativeUrl(value)) return false;
29740
- try {
29741
- const parsed = new URL(value);
29742
- return kind === "image" ? IMAGE_PROTOCOLS.has(parsed.protocol) : LINK_PROTOCOLS.has(parsed.protocol);
29743
- } catch {
29744
- return false;
29745
- }
29746
- }
29747
- function sanitizeUEditorUrl(raw, kind) {
29748
- const value = raw.trim();
29749
- if (!value) return "";
29750
- if (isSafeUEditorUrl(value, kind)) return normalizeUrlInput(value);
29751
- if (kind === "link" && !isProtocolRelativeUrl(value) && !/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(value)) {
29752
- const withProtocol = `https://${value}`;
29753
- return isSafeUEditorUrl(withProtocol, kind) ? withProtocol : "";
29754
- }
29755
- return "";
29756
- }
29757
-
29758
29862
  // src/components/UEditor/clipboard-images.ts
29759
29863
  var DEFAULT_UEDITOR_IMAGE_MAX_FILE_SIZE = 10 * 1024 * 1024;
29760
29864
  var DEFAULT_UEDITOR_IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif", "image/svg+xml"];
@@ -29996,9 +30100,7 @@ var EmojiSuggestion = import_core8.Extension.create({
29996
30100
  if (!props.clientRect) {
29997
30101
  return;
29998
30102
  }
29999
- popup?.[0]?.setProps({
30000
- getReferenceClientRect: props.clientRect
30001
- });
30103
+ setTippyReferenceClientRect(getFirstTippyInstance(popup), props.clientRect);
30002
30104
  },
30003
30105
  onKeyDown(props) {
30004
30106
  if (!props || !props.event) return false;
@@ -30095,7 +30197,7 @@ var FormulaSuggestionList = (0, import_react62.forwardRef)((props, ref) => {
30095
30197
  setSelectedIndex((prev) => (prev + 1) % props.items.length);
30096
30198
  return true;
30097
30199
  }
30098
- if (event.key === "Enter" || event.key === "Tab") {
30200
+ if (event.key === "Tab") {
30099
30201
  const item = props.items[selectedIndex];
30100
30202
  if (item) {
30101
30203
  props.command(item);
@@ -30185,9 +30287,7 @@ var FormulaSuggestion = import_core9.Extension.create({
30185
30287
  if (!props.clientRect) {
30186
30288
  return;
30187
30289
  }
30188
- popup?.[0]?.setProps({
30189
- getReferenceClientRect: props.clientRect
30190
- });
30290
+ setTippyReferenceClientRect(getFirstTippyInstance(popup), props.clientRect);
30191
30291
  },
30192
30292
  onKeyDown(props) {
30193
30293
  const popupInstance = getFirstTippyInstance(popup);
@@ -31929,7 +32029,6 @@ function buildUEditorExtensions({
31929
32029
  import_extension_gapcursor.default,
31930
32030
  import_extension_link.default.configure({
31931
32031
  openOnClick: false,
31932
- protocols: ["http", "https", "mailto", "tel"],
31933
32032
  isAllowedUri: (url) => isSafeUEditorUrl(url ?? "", "link"),
31934
32033
  HTMLAttributes: {
31935
32034
  class: "text-primary underline underline-offset-2 hover:text-primary/80 cursor-pointer"
@@ -32981,7 +33080,7 @@ var EditorToolbar = ({
32981
33080
  const [showImageInput, setShowImageInput] = (0, import_react70.useState)(false);
32982
33081
  const [showLinkInput, setShowLinkInput] = (0, import_react70.useState)(false);
32983
33082
  const [isTableMenuOpen, setIsTableMenuOpen] = (0, import_react70.useState)(false);
32984
- const tableCommandAnchorPosRef = (0, import_react70.useRef)(null);
33083
+ const [preservedTableAnchorPos, setPreservedTableAnchorPos] = (0, import_react70.useState)(null);
32985
33084
  const fileInputRef = (0, import_react70.useRef)(null);
32986
33085
  const [isUploadingImage, setIsUploadingImage] = (0, import_react70.useState)(false);
32987
33086
  const [imageUploadError, setImageUploadError] = (0, import_react70.useState)(null);
@@ -32996,7 +33095,7 @@ var EditorToolbar = ({
32996
33095
  const tableAlignAttr = tableAttrs.tableAlign ?? tableAttrs.textAlign;
32997
33096
  const currentTableAlign = tableAlignAttr === "center" || tableAlignAttr === "right" ? tableAlignAttr : "left";
32998
33097
  const isTableSelected = tableInfo !== null;
32999
- const hasTableContext = isTableSelected || tableCommandAnchorPosRef.current !== null;
33098
+ const hasTableContext = isTableSelected || preservedTableAnchorPos !== null;
33000
33099
  const canMergeCells = hasTableContext && editor.can().mergeCells();
33001
33100
  const canSplitCell = hasTableContext && editor.can().splitCell();
33002
33101
  const currentCellVerticalAlign = normalizeStyleValue(editor.getAttributes("tableCell").verticalAlign || editor.getAttributes("tableHeader").verticalAlign) || "";
@@ -33021,7 +33120,7 @@ var EditorToolbar = ({
33021
33120
  const displayedFontFamilyValue = currentFontFamily || defaultFontFamilyValue;
33022
33121
  const displayedFontSizeLabel = currentFontSize ? currentFontSizeLabel : "13";
33023
33122
  const activeFontSize = currentFontSize || "13px";
33024
- const tableCommandAnchorPos = tableCommandAnchorPosRef.current ?? tableAnchorPos ?? void 0;
33123
+ const tableCommandAnchorPos = preservedTableAnchorPos ?? tableAnchorPos ?? void 0;
33025
33124
  const isMedium = variant === "medium";
33026
33125
  const isMediumFull = variant === "medium-full";
33027
33126
  const isFull = variant === "default" || variant === "full" || variant === "notion" || !variant;
@@ -33697,7 +33796,7 @@ var EditorToolbar = ({
33697
33796
  isOpen: isTableMenuOpen,
33698
33797
  onOpenChange: (open) => {
33699
33798
  setIsTableMenuOpen(open);
33700
- tableCommandAnchorPosRef.current = open ? getTableAnchorPos(editor) : null;
33799
+ setPreservedTableAnchorPos(open ? getTableAnchorPos(editor) : null);
33701
33800
  },
33702
33801
  trigger: /* @__PURE__ */ (0, import_jsx_runtime91.jsxs)(ToolbarButton, { onClick: () => {
33703
33802
  }, title: t("toolbar.table"), children: [
@@ -33714,6 +33813,7 @@ var EditorToolbar = ({
33714
33813
  onInsert: (rows, cols) => {
33715
33814
  editor.chain().focus().insertTable({ rows, cols, withHeaderRow: true }).run();
33716
33815
  setIsTableMenuOpen(false);
33816
+ setPreservedTableAnchorPos(null);
33717
33817
  }
33718
33818
  }
33719
33819
  ),
@@ -33904,6 +34004,7 @@ var import_react_dom8 = require("react-dom");
33904
34004
  var import_lucide_react54 = require("lucide-react");
33905
34005
 
33906
34006
  // src/components/UEditor/table-formula-commands.ts
34007
+ var import_state10 = require("@tiptap/pm/state");
33907
34008
  var import_tables5 = require("@tiptap/pm/tables");
33908
34009
 
33909
34010
  // src/components/UEditor/table-formula.ts
@@ -34467,6 +34568,75 @@ function getSelectionTableCellNode(editor) {
34467
34568
  }
34468
34569
  return null;
34469
34570
  }
34571
+ function getSelectionTableCellPos(editor) {
34572
+ const cellSelection = editor.state.selection;
34573
+ const anchorCellPos = cellSelection.$anchorCell?.pos;
34574
+ if (typeof anchorCellPos === "number") {
34575
+ const node = editor.state.doc.nodeAt(anchorCellPos);
34576
+ if (node?.type.name === "tableCell" || node?.type.name === "tableHeader") {
34577
+ return anchorCellPos;
34578
+ }
34579
+ }
34580
+ const { $from } = editor.state.selection;
34581
+ for (let depth = $from.depth; depth > 0; depth -= 1) {
34582
+ const node = $from.node(depth);
34583
+ if (node.type.name === "tableCell" || node.type.name === "tableHeader") {
34584
+ return $from.before(depth);
34585
+ }
34586
+ }
34587
+ return null;
34588
+ }
34589
+ function getSelectionTableCellAnchor(editor) {
34590
+ const cellPos = getSelectionTableCellPos(editor);
34591
+ if (cellPos == null) return null;
34592
+ const node = editor.state.doc.nodeAt(cellPos);
34593
+ if (!node || node.type.name !== "tableCell" && node.type.name !== "tableHeader") return null;
34594
+ const $cell = editor.state.doc.resolve(Math.min(cellPos + 1, editor.state.doc.content.size));
34595
+ for (let depth = $cell.depth; depth > 0; depth -= 1) {
34596
+ const tableNode = $cell.node(depth);
34597
+ if (tableNode.type.name !== "table") continue;
34598
+ const tableStart = $cell.start(depth);
34599
+ const rect = safeFindCell2(import_tables5.TableMap.get(tableNode), cellPos - tableStart);
34600
+ if (!rect) return null;
34601
+ return {
34602
+ cellPos,
34603
+ column: rect.left,
34604
+ label: `${indexToColumnName(rect.left)}${rect.top + 1}`,
34605
+ node,
34606
+ row: rect.top,
34607
+ tablePos: tableStart - 1
34608
+ };
34609
+ }
34610
+ return null;
34611
+ }
34612
+ function restoreSelectionAtTableCell(editor, anchor) {
34613
+ const tableNode = editor.state.doc.nodeAt(anchor.tablePos);
34614
+ if (!tableNode || tableNode.type.name !== "table") return false;
34615
+ const map = import_tables5.TableMap.get(tableNode);
34616
+ if (anchor.row >= map.height || anchor.column >= map.width) return false;
34617
+ const relativeCellPos = map.positionAt(anchor.row, anchor.column, tableNode);
34618
+ const cellPos = anchor.tablePos + 1 + relativeCellPos;
34619
+ const cellNode = editor.state.doc.nodeAt(cellPos);
34620
+ if (!cellNode || cellNode.type.name !== "tableCell" && cellNode.type.name !== "tableHeader") return false;
34621
+ const selectionPos = Math.min(cellPos + 2, editor.state.doc.content.size);
34622
+ editor.view.dispatch(
34623
+ editor.state.tr.setSelection(import_state10.TextSelection.near(editor.state.doc.resolve(selectionPos))).setMeta("addToHistory", false)
34624
+ );
34625
+ return true;
34626
+ }
34627
+ function getSelectedTableFormulaCell(editor) {
34628
+ const selectedCell = getSelectionTableCellAnchor(editor);
34629
+ if (!selectedCell) return null;
34630
+ const formula = typeof selectedCell.node.attrs.formula === "string" ? selectedCell.node.attrs.formula.trim() : "";
34631
+ if (!formula) return null;
34632
+ const computedValue = typeof selectedCell.node.attrs.computedValue === "string" ? selectedCell.node.attrs.computedValue : "";
34633
+ return {
34634
+ ...selectedCell,
34635
+ computedValue,
34636
+ formula,
34637
+ formulaState: computedValue.startsWith("#") ? "error" : "computed"
34638
+ };
34639
+ }
34470
34640
  function getSelectionTableInfo(editor) {
34471
34641
  const { $from } = editor.state.selection;
34472
34642
  for (let depth = $from.depth; depth > 0; depth -= 1) {
@@ -34513,6 +34683,28 @@ function createCellDisplayContent(cellNode, displayValue) {
34513
34683
  }
34514
34684
  return [paragraphType.create(null, cellNode.type.schema.text(displayValue))];
34515
34685
  }
34686
+ function removeSelectedTableCellFormula(editor, clearContent) {
34687
+ const selectedCell = getSelectedTableFormulaCell(editor);
34688
+ if (!selectedCell) return false;
34689
+ const nextCell2 = selectedCell.node.type.create(
34690
+ {
34691
+ ...selectedCell.node.attrs,
34692
+ formula: null,
34693
+ computedValue: null
34694
+ },
34695
+ clearContent ? createCellDisplayContent(selectedCell.node, "") : selectedCell.node.content,
34696
+ selectedCell.node.marks
34697
+ );
34698
+ let transaction = editor.state.tr.replaceWith(selectedCell.cellPos, selectedCell.cellPos + selectedCell.node.nodeSize, nextCell2).setMeta(UEDITOR_TABLE_FORMULA_RECALCULATE_META, true);
34699
+ const selectionPos = Math.min(selectedCell.cellPos + 2, transaction.doc.content.size);
34700
+ transaction = transaction.setSelection(import_state10.TextSelection.near(transaction.doc.resolve(selectionPos)));
34701
+ editor.view.dispatch(transaction);
34702
+ recalculateActiveTableFormulas(editor);
34703
+ restoreSelectionAtTableCell(editor, selectedCell);
34704
+ editor.view.focus();
34705
+ dispatchTableLayoutChange(editor);
34706
+ return true;
34707
+ }
34516
34708
  function buildTableValueMap(tableNode) {
34517
34709
  const map = import_tables5.TableMap.get(tableNode);
34518
34710
  const values = /* @__PURE__ */ new Map();
@@ -34535,21 +34727,15 @@ function normalizeFormulaInput(formula) {
34535
34727
  function setSelectedTableCellFormula(editor, formula) {
34536
34728
  const normalized = normalizeFormulaInput(formula);
34537
34729
  const { state, view } = editor;
34730
+ const selectionAnchor = getSelectionTableCellAnchor(editor);
34538
34731
  if (!normalized) {
34539
- const clearedFormula = (0, import_tables5.setCellAttr)("formula", null)(state, view.dispatch.bind(view));
34540
- const clearedValue2 = (0, import_tables5.setCellAttr)("computedValue", null)(editor.state, view.dispatch.bind(view));
34541
- if (clearedFormula || clearedValue2) {
34542
- recalculateActiveTableFormulas(editor);
34543
- view.focus();
34544
- dispatchTableLayoutChange(editor);
34545
- return true;
34546
- }
34547
- return false;
34732
+ return clearSelectedTableCellFormula(editor);
34548
34733
  }
34549
34734
  const appliedFormula = (0, import_tables5.setCellAttr)("formula", normalized)(state, view.dispatch.bind(view));
34550
34735
  const clearedValue = (0, import_tables5.setCellAttr)("computedValue", null)(editor.state, view.dispatch.bind(view));
34551
34736
  if (appliedFormula || clearedValue) {
34552
34737
  recalculateActiveTableFormulas(editor);
34738
+ if (selectionAnchor) restoreSelectionAtTableCell(editor, selectionAnchor);
34553
34739
  view.focus();
34554
34740
  dispatchTableLayoutChange(editor);
34555
34741
  return true;
@@ -34557,7 +34743,10 @@ function setSelectedTableCellFormula(editor, formula) {
34557
34743
  return false;
34558
34744
  }
34559
34745
  function clearSelectedTableCellFormula(editor) {
34560
- return setSelectedTableCellFormula(editor, "");
34746
+ return removeSelectedTableCellFormula(editor, true);
34747
+ }
34748
+ function convertSelectedTableCellFormulaToValue(editor) {
34749
+ return removeSelectedTableCellFormula(editor, false);
34561
34750
  }
34562
34751
  function setSelectedTableCellNumberFormat(editor, numberFormat) {
34563
34752
  const value = numberFormat && numberFormat !== "text" ? numberFormat : null;
@@ -34757,6 +34946,58 @@ function recalculateAllTableFormulas(editor) {
34757
34946
 
34758
34947
  // src/components/UEditor/menus.tsx
34759
34948
  var import_jsx_runtime92 = require("react/jsx-runtime");
34949
+ var FloatingSlashCommandMenu = ({ editor, onClose }) => {
34950
+ const t = useSmartTranslations("UEditor");
34951
+ const messages = (0, import_react72.useMemo)(() => buildSlashCommandMessages(t), [t]);
34952
+ const items = (0, import_react72.useMemo)(() => buildSlashCommandItems({ query: "", messages }), [messages]);
34953
+ const listRef = (0, import_react72.useRef)(null);
34954
+ (0, import_react72.useEffect)(() => {
34955
+ const handleKeyDown2 = (event) => {
34956
+ if (event.key === "Escape") {
34957
+ event.preventDefault();
34958
+ onClose();
34959
+ return;
34960
+ }
34961
+ const handled = listRef.current?.onKeyDown({ event }) ?? false;
34962
+ if (handled) {
34963
+ event.preventDefault();
34964
+ }
34965
+ };
34966
+ document.addEventListener("keydown", handleKeyDown2);
34967
+ return () => document.removeEventListener("keydown", handleKeyDown2);
34968
+ }, [onClose]);
34969
+ return /* @__PURE__ */ (0, import_jsx_runtime92.jsx)(
34970
+ SlashCommandList,
34971
+ {
34972
+ ref: listRef,
34973
+ items,
34974
+ messages,
34975
+ command: (item) => {
34976
+ item.command({ editor });
34977
+ onClose();
34978
+ }
34979
+ }
34980
+ );
34981
+ };
34982
+ var FloatingMenuContent = ({ editor }) => {
34983
+ const t = useSmartTranslations("UEditor");
34984
+ const [showCommands, setShowCommands] = (0, import_react72.useState)(false);
34985
+ if (showCommands) {
34986
+ return /* @__PURE__ */ (0, import_jsx_runtime92.jsx)(FloatingSlashCommandMenu, { editor, onClose: () => setShowCommands(false) });
34987
+ }
34988
+ return /* @__PURE__ */ (0, import_jsx_runtime92.jsxs)(
34989
+ "button",
34990
+ {
34991
+ type: "button",
34992
+ onClick: () => setShowCommands(true),
34993
+ className: "flex items-center gap-1 px-2 py-1.5 rounded-lg hover:bg-accent transition-all group",
34994
+ children: [
34995
+ /* @__PURE__ */ (0, import_jsx_runtime92.jsx)(import_lucide_react54.Plus, { className: "w-4 h-4 text-muted-foreground group-hover:text-foreground" }),
34996
+ /* @__PURE__ */ (0, import_jsx_runtime92.jsx)("span", { className: "text-sm text-muted-foreground group-hover:text-foreground", children: t("floatingMenu.addBlock") })
34997
+ ]
34998
+ }
34999
+ );
35000
+ };
34760
35001
  function applyTableCellBackground(editor, color) {
34761
35002
  const value = color || null;
34762
35003
  const { state, view } = editor;
@@ -35495,7 +35736,8 @@ var LinkPreviewContent = ({ editor, onEdit }) => {
35495
35736
  const url = editor.getAttributes("link").href || "";
35496
35737
  const t = useSmartTranslations("UEditor");
35497
35738
  const handleOpen = () => {
35498
- window.open(url, "_blank", "noopener,noreferrer");
35739
+ const safeUrl = sanitizeUEditorUrl(url, "link");
35740
+ if (safeUrl) window.open(safeUrl, "_blank", "noopener,noreferrer");
35499
35741
  };
35500
35742
  const handleUnlink = () => {
35501
35743
  editor.chain().focus().unsetLink().run();
@@ -35525,16 +35767,19 @@ var CustomBubbleMenu = ({
35525
35767
  });
35526
35768
  const menuRef = (0, import_react72.useRef)(null);
35527
35769
  const keepOpenRef = (0, import_react72.useRef)(false);
35770
+ const [keepOpen, setKeepOpenState] = (0, import_react72.useState)(false);
35528
35771
  const showTimeoutRef = (0, import_react72.useRef)(null);
35529
35772
  const suppressShowUntilRef = (0, import_react72.useRef)(0);
35530
35773
  const setKeepOpen = (0, import_react72.useCallback)((next) => {
35531
35774
  keepOpenRef.current = next;
35775
+ setKeepOpenState(next);
35532
35776
  if (!next) setLinkInputOpen(false);
35533
35777
  if (next) setIsVisible(true);
35534
35778
  }, []);
35535
35779
  const closeBubbleMenu = (0, import_react72.useCallback)(() => {
35536
35780
  suppressShowUntilRef.current = Date.now() + 1e3;
35537
35781
  keepOpenRef.current = false;
35782
+ setKeepOpenState(false);
35538
35783
  setLinkInputOpen(false);
35539
35784
  setIsVisible(false);
35540
35785
  if (showTimeoutRef.current) {
@@ -35640,13 +35885,13 @@ var CustomBubbleMenu = ({
35640
35885
  onMouseDown: (e) => {
35641
35886
  const target = e.target;
35642
35887
  if (target?.closest?.("[data-ueditor-close-on-select]")) {
35643
- keepOpenRef.current = false;
35888
+ setKeepOpen(false);
35644
35889
  } else if (target?.closest?.("[data-ueditor-keep-open]")) {
35645
- keepOpenRef.current = true;
35890
+ setKeepOpen(true);
35646
35891
  }
35647
35892
  e.preventDefault();
35648
35893
  },
35649
- children: editor.isActive("link") && !keepOpenRef.current && !linkInputOpen ? /* @__PURE__ */ (0, import_jsx_runtime92.jsx)(
35894
+ children: editor.isActive("link") && !keepOpen && !linkInputOpen ? /* @__PURE__ */ (0, import_jsx_runtime92.jsx)(
35650
35895
  LinkPreviewContent,
35651
35896
  {
35652
35897
  editor,
@@ -35672,6 +35917,54 @@ var CustomBubbleMenu = ({
35672
35917
  document.body
35673
35918
  );
35674
35919
  };
35920
+ var CustomFloatingMenu = ({ editor }) => {
35921
+ const FLOATING_MENU_OFFSET = 16;
35922
+ const [isVisible, setIsVisible] = (0, import_react72.useState)(false);
35923
+ const [position, setPosition2] = (0, import_react72.useState)({ top: 0, left: 0 });
35924
+ (0, import_react72.useEffect)(() => {
35925
+ const updatePosition = () => {
35926
+ const { state, view } = editor;
35927
+ const { $from, empty } = state.selection;
35928
+ const isEmptyTextBlock = $from.parent.isTextblock && $from.parent.type.name === "paragraph" && $from.parent.textContent === "" && empty;
35929
+ if (!isEmptyTextBlock || !view.hasFocus()) {
35930
+ setIsVisible(false);
35931
+ return;
35932
+ }
35933
+ const coords = view.coordsAtPos($from.pos);
35934
+ setPosition2({ top: coords.top - FLOATING_MENU_OFFSET, left: coords.left });
35935
+ setIsVisible(true);
35936
+ };
35937
+ const handleBlur = () => setIsVisible(false);
35938
+ editor.on("selectionUpdate", updatePosition);
35939
+ editor.on("focus", updatePosition);
35940
+ editor.on("blur", handleBlur);
35941
+ editor.on("update", updatePosition);
35942
+ return () => {
35943
+ editor.off("selectionUpdate", updatePosition);
35944
+ editor.off("focus", updatePosition);
35945
+ editor.off("blur", handleBlur);
35946
+ editor.off("update", updatePosition);
35947
+ };
35948
+ }, [editor]);
35949
+ if (!isVisible) return null;
35950
+ return (0, import_react_dom8.createPortal)(
35951
+ /* @__PURE__ */ (0, import_jsx_runtime92.jsx)(
35952
+ "div",
35953
+ {
35954
+ "data-popover": true,
35955
+ className: "fixed z-99999 rounded-2xl border border-border/50 bg-card text-card-foreground shadow-lg backdrop-blur-sm overflow-hidden animate-in fade-in-0 slide-in-from-bottom-2",
35956
+ style: {
35957
+ top: `${position.top}px`,
35958
+ left: `${position.left}px`,
35959
+ transform: "translate(-50%, -100%)"
35960
+ },
35961
+ onMouseDown: (e) => e.preventDefault(),
35962
+ children: /* @__PURE__ */ (0, import_jsx_runtime92.jsx)(FloatingMenuContent, { editor })
35963
+ }
35964
+ ),
35965
+ document.body
35966
+ );
35967
+ };
35675
35968
 
35676
35969
  // src/components/UEditor/CharacterCount.tsx
35677
35970
  var import_jsx_runtime93 = require("react/jsx-runtime");
@@ -38747,7 +39040,7 @@ var Selection = class {
38747
39040
  found.
38748
39041
  */
38749
39042
  static findFrom($pos, dir, textOnly = false) {
38750
- let inner = $pos.parent.inlineContent ? new TextSelection3($pos) : findSelectionIn($pos.node(0), $pos.parent, $pos.pos, $pos.index(), dir, textOnly);
39043
+ let inner = $pos.parent.inlineContent ? new TextSelection4($pos) : findSelectionIn($pos.node(0), $pos.parent, $pos.pos, $pos.index(), dir, textOnly);
38751
39044
  if (inner)
38752
39045
  return inner;
38753
39046
  for (let depth = $pos.depth - 1; depth >= 0; depth--) {
@@ -38816,7 +39109,7 @@ var Selection = class {
38816
39109
  returns the bookmark for that.
38817
39110
  */
38818
39111
  getBookmark() {
38819
- return TextSelection3.between(this.$anchor, this.$head).getBookmark();
39112
+ return TextSelection4.between(this.$anchor, this.$head).getBookmark();
38820
39113
  }
38821
39114
  };
38822
39115
  Selection.prototype.visible = true;
@@ -38836,7 +39129,7 @@ function checkTextSelection($pos) {
38836
39129
  console["warn"]("TextSelection endpoint not pointing into a node with inline content (" + $pos.parent.type.name + ")");
38837
39130
  }
38838
39131
  }
38839
- var TextSelection3 = class _TextSelection extends Selection {
39132
+ var TextSelection4 = class _TextSelection extends Selection {
38840
39133
  /**
38841
39134
  Construct a text selection between the given points.
38842
39135
  */
@@ -38922,7 +39215,7 @@ var TextSelection3 = class _TextSelection extends Selection {
38922
39215
  return new _TextSelection($anchor, $head);
38923
39216
  }
38924
39217
  };
38925
- Selection.jsonID("text", TextSelection3);
39218
+ Selection.jsonID("text", TextSelection4);
38926
39219
  var TextBookmark = class _TextBookmark {
38927
39220
  constructor(anchor, head) {
38928
39221
  this.anchor = anchor;
@@ -38932,7 +39225,7 @@ var TextBookmark = class _TextBookmark {
38932
39225
  return new _TextBookmark(mapping.map(this.anchor), mapping.map(this.head));
38933
39226
  }
38934
39227
  resolve(doc) {
38935
- return TextSelection3.between(doc.resolve(this.anchor), doc.resolve(this.head));
39228
+ return TextSelection4.between(doc.resolve(this.anchor), doc.resolve(this.head));
38936
39229
  }
38937
39230
  };
38938
39231
  var NodeSelection2 = class _NodeSelection extends Selection {
@@ -39051,7 +39344,7 @@ var AllBookmark = {
39051
39344
  };
39052
39345
  function findSelectionIn(doc, node, pos, index, dir, text = false) {
39053
39346
  if (node.inlineContent)
39054
- return TextSelection3.create(doc, pos);
39347
+ return TextSelection4.create(doc, pos);
39055
39348
  for (let i = index - (dir > 0 ? 0 : 1); dir > 0 ? i < node.childCount : i >= 0; i += dir) {
39056
39349
  let child = node.child(i);
39057
39350
  if (!child.isAtom) {
@@ -39643,7 +39936,7 @@ var CellSelection = class CellSelection2 extends Selection {
39643
39936
  else if (tableChanged && this.isColSelection()) return CellSelection2.colSelection($anchorCell, $headCell);
39644
39937
  else return new CellSelection2($anchorCell, $headCell);
39645
39938
  }
39646
- return TextSelection3.between($anchorCell, $headCell);
39939
+ return TextSelection4.between($anchorCell, $headCell);
39647
39940
  }
39648
39941
  content() {
39649
39942
  const table = this.$anchorCell.node(-1);
@@ -40258,7 +40551,7 @@ function shiftArrow(axis, dir) {
40258
40551
  };
40259
40552
  }
40260
40553
  function atEndOfCell(view, axis, dir) {
40261
- if (!(view.state.selection instanceof TextSelection3)) return null;
40554
+ if (!(view.state.selection instanceof TextSelection4)) return null;
40262
40555
  const { $head } = view.state.selection;
40263
40556
  for (let d = $head.depth - 1; d >= 0; d--) {
40264
40557
  const parent = $head.node(d);
@@ -41378,7 +41671,7 @@ function TableControls({ editor, containerRef }) {
41378
41671
  destructive: true
41379
41672
  }
41380
41673
  ];
41381
- }, [layout, runAtActiveCell, t]);
41674
+ }, [editor, layout, runAtActiveCell, t]);
41382
41675
  const getRowHandleMenuItems = import_react74.default.useCallback((rowHandle) => [
41383
41676
  {
41384
41677
  label: t("tableMenu.addRowBefore"),
@@ -41594,6 +41887,42 @@ var UEDITOR_PROSEMIRROR_CLASS_NAME = cn(
41594
41887
  "[&_th]:px-2",
41595
41888
  "[&_th]:py-0",
41596
41889
  "[&_th_p]:my-0",
41890
+ "[&_td[data-formula]]:pr-7",
41891
+ "[&_th[data-formula]]:pr-7",
41892
+ "[&_td[data-formula]]:before:pointer-events-none",
41893
+ "[&_th[data-formula]]:before:pointer-events-none",
41894
+ "[&_td[data-formula]]:before:absolute",
41895
+ "[&_th[data-formula]]:before:absolute",
41896
+ "[&_td[data-formula]]:before:right-1",
41897
+ "[&_th[data-formula]]:before:right-1",
41898
+ "[&_td[data-formula]]:before:top-1",
41899
+ "[&_th[data-formula]]:before:top-1",
41900
+ "[&_td[data-formula]]:before:z-[1]",
41901
+ "[&_th[data-formula]]:before:z-[1]",
41902
+ "[&_td[data-formula]]:before:rounded-sm",
41903
+ "[&_th[data-formula]]:before:rounded-sm",
41904
+ "[&_td[data-formula]]:before:bg-primary/10",
41905
+ "[&_th[data-formula]]:before:bg-primary/10",
41906
+ "[&_td[data-formula]]:before:px-1",
41907
+ "[&_th[data-formula]]:before:px-1",
41908
+ "[&_td[data-formula]]:before:font-mono",
41909
+ "[&_th[data-formula]]:before:font-mono",
41910
+ "[&_td[data-formula]]:before:text-[9px]",
41911
+ "[&_th[data-formula]]:before:text-[9px]",
41912
+ "[&_td[data-formula]]:before:font-semibold",
41913
+ "[&_th[data-formula]]:before:font-semibold",
41914
+ "[&_td[data-formula]]:before:leading-4",
41915
+ "[&_th[data-formula]]:before:leading-4",
41916
+ "[&_td[data-formula]]:before:text-primary",
41917
+ "[&_th[data-formula]]:before:text-primary",
41918
+ "[&_td[data-formula]]:before:content-['fx']",
41919
+ "[&_th[data-formula]]:before:content-['fx']",
41920
+ "[&_td[data-formula-state='error']]:bg-destructive/5",
41921
+ "[&_th[data-formula-state='error']]:bg-destructive/5",
41922
+ "[&_td[data-formula-state='error']]:before:bg-destructive/10",
41923
+ "[&_th[data-formula-state='error']]:before:bg-destructive/10",
41924
+ "[&_td[data-formula-state='error']]:before:text-destructive",
41925
+ "[&_th[data-formula-state='error']]:before:text-destructive",
41597
41926
  "[&_td[colwidth]]:min-w-0",
41598
41927
  "[&_th[colwidth]]:min-w-0",
41599
41928
  "[&_td[data-colwidth]]:min-w-0",
@@ -42099,7 +42428,7 @@ function useUEditorTableInteractions(editor, editable = true) {
42099
42428
  clearHoveredTableCell();
42100
42429
  clearAllTableResizeHover();
42101
42430
  };
42102
- }, [beginResize, cancelResize, cleanupRowResize, clearActiveTableCell, clearAllTableResizeHover, clearHoveredTableCell, editable, editor, handleRowResizePointerMove, handleRowResizePointerUp, hideColumnGuide, hideRowGuide, isRowResizing, scheduleTableLayoutSync, showColumnGuide, showRowGuide, syncActiveRowResizeGuide, syncActiveTableCellFromSelection, updateActiveCellHighlight]);
42431
+ }, [beginResize, cancelResize, cleanupRowResize, clearActiveTableCell, clearAllTableResizeHover, clearHoveredTableCell, editable, editor, handleRowResizePointerMove, handleRowResizePointerUp, hideColumnGuide, hideRowGuide, isRowResizing, scheduleTableLayoutSync, setActiveTableCell, setHoveredTableCell, showColumnGuide, showRowGuide, syncActiveRowResizeGuide, syncActiveTableCellFromSelection, updateActiveCellHighlight]);
42103
42432
  return {
42104
42433
  editorContentRef,
42105
42434
  tableColumnGuideRef,
@@ -42113,7 +42442,7 @@ var import_react77 = require("react");
42113
42442
  var import_tables8 = require("@tiptap/pm/tables");
42114
42443
 
42115
42444
  // src/components/UEditor/table-formula-range-picker.ts
42116
- var import_state10 = require("@tiptap/pm/state");
42445
+ var import_state11 = require("@tiptap/pm/state");
42117
42446
  var import_tables7 = require("@tiptap/pm/tables");
42118
42447
  function getCellText2(cellNode) {
42119
42448
  return cellNode.textBetween(0, cellNode.content.size, "\n").trim();
@@ -42149,7 +42478,7 @@ function cancelFormulaEditing(view) {
42149
42478
  if (!context) return false;
42150
42479
  let tr = view.state.tr.delete(context.cellContentStart, context.cellContentEnd);
42151
42480
  const selectionPos = Math.min(context.cellContentStart, tr.doc.content.size);
42152
- tr = tr.setSelection(import_state10.TextSelection.near(tr.doc.resolve(selectionPos)));
42481
+ tr = tr.setSelection(import_state11.TextSelection.near(tr.doc.resolve(selectionPos)));
42153
42482
  view.dispatch(tr);
42154
42483
  view.focus();
42155
42484
  return true;
@@ -42216,7 +42545,7 @@ function replacePickedLabel(view, pickState, nextLabel, currentCell) {
42216
42545
  }
42217
42546
  let tr = view.state.tr.insertText(nextLabel, pickState.insertedFrom, pickState.insertedTo);
42218
42547
  const nextTo = pickState.insertedFrom + nextLabel.length;
42219
- tr = tr.setSelection(import_state10.TextSelection.create(tr.doc, nextTo));
42548
+ tr = tr.setSelection(import_state11.TextSelection.create(tr.doc, nextTo));
42220
42549
  view.dispatch(tr);
42221
42550
  return {
42222
42551
  ...pickState,
@@ -42236,7 +42565,7 @@ function beginFormulaRangePick(view, event) {
42236
42565
  const insertedText = `${prefix}${picked.label}`;
42237
42566
  const insertedFrom = from + prefix.length;
42238
42567
  let tr = view.state.tr.insertText(insertedText, from, to);
42239
- tr = tr.setSelection(import_state10.TextSelection.create(tr.doc, from + insertedText.length));
42568
+ tr = tr.setSelection(import_state11.TextSelection.create(tr.doc, from + insertedText.length));
42240
42569
  view.dispatch(tr);
42241
42570
  view.focus();
42242
42571
  event.preventDefault();
@@ -43385,9 +43714,156 @@ var MenuBar = ({
43385
43714
  ] });
43386
43715
  };
43387
43716
 
43388
- // src/components/UEditor/UEditor.tsx
43717
+ // src/components/UEditor/table-formula-bar.tsx
43718
+ var import_react80 = require("react");
43719
+ var import_react81 = require("@tiptap/react");
43720
+ var import_lucide_react59 = require("lucide-react");
43389
43721
  var import_jsx_runtime100 = require("react/jsx-runtime");
43390
- var UEditor = import_react80.default.forwardRef(({
43722
+ function TableFormulaBar({ editor }) {
43723
+ const t = useSmartTranslations("UEditor");
43724
+ const inputRef = (0, import_react80.useRef)(null);
43725
+ const [draftState, setDraftState] = (0, import_react80.useState)(null);
43726
+ (0, import_react81.useEditorState)({
43727
+ editor,
43728
+ selector: ({ transactionNumber }) => transactionNumber
43729
+ });
43730
+ const selectedCell = getSelectedTableFormulaCell(editor);
43731
+ (0, import_react80.useEffect)(() => {
43732
+ const focusFormulaInput = () => {
43733
+ inputRef.current?.focus();
43734
+ };
43735
+ const handleDoubleClick = (event) => {
43736
+ const target = resolveEventElement(event.target);
43737
+ if (!target?.closest?.("td[data-formula],th[data-formula]")) return;
43738
+ event.preventDefault();
43739
+ focusFormulaInput();
43740
+ };
43741
+ editor.view.dom.addEventListener("dblclick", handleDoubleClick);
43742
+ return () => {
43743
+ editor.view.dom.removeEventListener("dblclick", handleDoubleClick);
43744
+ };
43745
+ }, [editor]);
43746
+ if (!selectedCell) return null;
43747
+ const selectedCellKey = `${selectedCell.tablePos}:${selectedCell.row}:${selectedCell.column}`;
43748
+ const draft = draftState?.cellKey === selectedCellKey && draftState.sourceFormula === selectedCell.formula ? draftState.value : selectedCell.formula;
43749
+ const setDraft = (value) => {
43750
+ setDraftState({
43751
+ cellKey: selectedCellKey,
43752
+ sourceFormula: selectedCell.formula,
43753
+ value
43754
+ });
43755
+ };
43756
+ const applyDraft = () => {
43757
+ const normalized = normalizeFormulaInput(draft);
43758
+ if (normalized) {
43759
+ setSelectedTableCellFormula(editor, normalized);
43760
+ } else {
43761
+ clearSelectedTableCellFormula(editor);
43762
+ }
43763
+ setDraftState(null);
43764
+ };
43765
+ const cancelDraft = () => {
43766
+ setDraftState(null);
43767
+ window.setTimeout(() => {
43768
+ if (!editor.isDestroyed) editor.commands.focus();
43769
+ }, 0);
43770
+ };
43771
+ return /* @__PURE__ */ (0, import_jsx_runtime100.jsxs)(
43772
+ "div",
43773
+ {
43774
+ "data-ueditor-formula-bar": "",
43775
+ role: "toolbar",
43776
+ "aria-label": t("tableMenu.formulaBar"),
43777
+ className: "flex min-h-10 items-center gap-2 border-b border-border/60 bg-muted/25 px-2 py-1.5",
43778
+ children: [
43779
+ /* @__PURE__ */ (0, import_jsx_runtime100.jsx)(
43780
+ "span",
43781
+ {
43782
+ "data-ueditor-formula-cell-label": "",
43783
+ className: "flex h-7 min-w-12 items-center justify-center rounded-md border border-border/60 bg-background px-2 font-mono text-xs font-semibold text-foreground",
43784
+ children: selectedCell.label
43785
+ }
43786
+ ),
43787
+ /* @__PURE__ */ (0, import_jsx_runtime100.jsx)(import_lucide_react59.Sigma, { "aria-hidden": "true", className: "h-4 w-4 shrink-0 text-primary" }),
43788
+ /* @__PURE__ */ (0, import_jsx_runtime100.jsx)(
43789
+ "input",
43790
+ {
43791
+ ref: inputRef,
43792
+ "data-ueditor-formula-input": "",
43793
+ value: draft,
43794
+ onChange: (event) => setDraft(event.target.value),
43795
+ onKeyDown: (event) => {
43796
+ event.stopPropagation();
43797
+ if (event.key === "Enter") {
43798
+ event.preventDefault();
43799
+ applyDraft();
43800
+ } else if (event.key === "Escape") {
43801
+ event.preventDefault();
43802
+ cancelDraft();
43803
+ }
43804
+ },
43805
+ "aria-label": t("tableMenu.editFormula"),
43806
+ className: "h-7 min-w-0 flex-1 rounded-md border border-border/60 bg-background px-2 font-mono text-sm text-foreground outline-none focus:border-primary/40 focus:ring-2 focus:ring-primary/15"
43807
+ }
43808
+ ),
43809
+ selectedCell.formulaState === "error" && /* @__PURE__ */ (0, import_jsx_runtime100.jsxs)(
43810
+ "span",
43811
+ {
43812
+ role: "status",
43813
+ className: "hidden shrink-0 items-center gap-1 text-xs font-medium text-destructive sm:inline-flex",
43814
+ children: [
43815
+ /* @__PURE__ */ (0, import_jsx_runtime100.jsx)(import_lucide_react59.AlertCircle, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
43816
+ t("tableMenu.formulaError")
43817
+ ]
43818
+ }
43819
+ ),
43820
+ /* @__PURE__ */ (0, import_jsx_runtime100.jsx)(
43821
+ "button",
43822
+ {
43823
+ type: "button",
43824
+ onClick: applyDraft,
43825
+ "aria-label": t("tableMenu.apply"),
43826
+ title: t("tableMenu.apply"),
43827
+ className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-primary text-primary-foreground transition-colors hover:bg-primary/90",
43828
+ children: /* @__PURE__ */ (0, import_jsx_runtime100.jsx)(import_lucide_react59.Check, { "aria-hidden": "true", className: "h-4 w-4" })
43829
+ }
43830
+ ),
43831
+ /* @__PURE__ */ (0, import_jsx_runtime100.jsx)(
43832
+ "button",
43833
+ {
43834
+ type: "button",
43835
+ onClick: () => {
43836
+ convertSelectedTableCellFormulaToValue(editor);
43837
+ setDraftState(null);
43838
+ },
43839
+ "aria-label": t("tableMenu.convertToValue"),
43840
+ title: t("tableMenu.convertToValue"),
43841
+ className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-md border border-border/60 bg-background text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
43842
+ children: /* @__PURE__ */ (0, import_jsx_runtime100.jsx)(import_lucide_react59.Hash, { "aria-hidden": "true", className: "h-4 w-4" })
43843
+ }
43844
+ ),
43845
+ /* @__PURE__ */ (0, import_jsx_runtime100.jsx)(
43846
+ "button",
43847
+ {
43848
+ type: "button",
43849
+ onClick: () => {
43850
+ clearSelectedTableCellFormula(editor);
43851
+ setDraftState(null);
43852
+ },
43853
+ "aria-label": t("tableMenu.clearCell"),
43854
+ title: t("tableMenu.clearCell"),
43855
+ className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-md border border-destructive/30 bg-background text-destructive transition-colors hover:bg-destructive/10",
43856
+ children: /* @__PURE__ */ (0, import_jsx_runtime100.jsx)(import_lucide_react59.Trash2, { "aria-hidden": "true", className: "h-4 w-4" })
43857
+ }
43858
+ )
43859
+ ]
43860
+ }
43861
+ );
43862
+ }
43863
+
43864
+ // src/components/UEditor/UEditor.tsx
43865
+ var import_jsx_runtime101 = require("react/jsx-runtime");
43866
+ var UEditor = import_react82.default.forwardRef(({
43391
43867
  content = "",
43392
43868
  onChange,
43393
43869
  onHtmlChange,
@@ -43405,6 +43881,7 @@ var UEditor = import_react80.default.forwardRef(({
43405
43881
  autofocus = false,
43406
43882
  showToolbar = true,
43407
43883
  showBubbleMenu = true,
43884
+ showFloatingMenu = false,
43408
43885
  showCharacterCount = true,
43409
43886
  showFooter = true,
43410
43887
  maxCharacters,
@@ -43428,14 +43905,14 @@ var UEditor = import_react80.default.forwardRef(({
43428
43905
  }, ref) => {
43429
43906
  const t = useSmartTranslations("UEditor");
43430
43907
  const effectivePlaceholder = placeholder ?? t("placeholder");
43431
- const inFlightPrepareRef = (0, import_react80.useRef)(null);
43432
- const lastAppliedContentRef = (0, import_react80.useRef)(content ?? "");
43433
- const scheduledFormulaRecalculateRef = (0, import_react80.useRef)(false);
43434
- const editorInstanceRef = (0, import_react80.useRef)(null);
43435
- const formulaRangePickRef = (0, import_react80.useRef)(null);
43436
- const formulaRangeSurfaceRef = (0, import_react80.useRef)(null);
43437
- const [formulaRangeHighlight, setFormulaRangeHighlight] = import_react80.default.useState(null);
43438
- const scheduleFormulaRecalculate = import_react80.default.useCallback((editor2, options) => {
43908
+ const inFlightPrepareRef = (0, import_react82.useRef)(null);
43909
+ const lastAppliedContentRef = (0, import_react82.useRef)(content ?? "");
43910
+ const scheduledFormulaRecalculateRef = (0, import_react82.useRef)(false);
43911
+ const editorInstanceRef = (0, import_react82.useRef)(null);
43912
+ const formulaRangePickRef = (0, import_react82.useRef)(null);
43913
+ const formulaRangeSurfaceRef = (0, import_react82.useRef)(null);
43914
+ const [formulaRangeHighlight, setFormulaRangeHighlight] = import_react82.default.useState(null);
43915
+ const scheduleFormulaRecalculate = import_react82.default.useCallback((editor2, options) => {
43439
43916
  if (editor2.isDestroyed || scheduledFormulaRecalculateRef.current) return;
43440
43917
  if (!options?.force && isEditingTableFormulaText(editor2)) return;
43441
43918
  scheduledFormulaRecalculateRef.current = true;
@@ -43446,7 +43923,7 @@ var UEditor = import_react80.default.forwardRef(({
43446
43923
  }
43447
43924
  });
43448
43925
  }, []);
43449
- const resolvedUploadFile = (0, import_react80.useMemo)(() => {
43926
+ const resolvedUploadFile = (0, import_react82.useMemo)(() => {
43450
43927
  if (uploadFile) return uploadFile;
43451
43928
  if (uploadFileForSave) {
43452
43929
  return async (file) => {
@@ -43456,7 +43933,7 @@ var UEditor = import_react80.default.forwardRef(({
43456
43933
  }
43457
43934
  return uploadImage;
43458
43935
  }, [uploadFile, uploadFileForSave, uploadImage]);
43459
- const extensions = (0, import_react80.useMemo)(
43936
+ const extensions = (0, import_react82.useMemo)(
43460
43937
  () => [
43461
43938
  ...buildUEditorExtensions({
43462
43939
  placeholder: effectivePlaceholder,
@@ -43475,11 +43952,11 @@ var UEditor = import_react80.default.forwardRef(({
43475
43952
  ],
43476
43953
  [effectivePlaceholder, t, maxCharacters, uploadImage, resolvedUploadFile, imageInsertMode, maxImageFileSize, allowedImageMimeTypes, fallbackToDataUrl, editable, fetchMetadata, extraExtensions]
43477
43954
  );
43478
- const syncFormulaRangeHighlight = import_react80.default.useCallback((pickState) => {
43955
+ const syncFormulaRangeHighlight = import_react82.default.useCallback((pickState) => {
43479
43956
  const container = formulaRangeSurfaceRef.current;
43480
43957
  setFormulaRangeHighlight(container && pickState ? getFormulaRangePickHighlight(container, pickState) : null);
43481
43958
  }, []);
43482
- const editor = (0, import_react81.useEditor)({
43959
+ const editor = (0, import_react83.useEditor)({
43483
43960
  immediatelyRender: false,
43484
43961
  extensions,
43485
43962
  content,
@@ -43524,9 +44001,9 @@ var UEditor = import_react80.default.forwardRef(({
43524
44001
  if (formulaContext && event.key === "Enter") {
43525
44002
  event.preventDefault();
43526
44003
  event.stopPropagation();
43527
- const activeEditor = editorInstanceRef.current;
43528
- if (activeEditor && !isDraftTableFormula(formulaContext.formula)) {
43529
- recalculateActiveTableFormulas(activeEditor);
44004
+ const activeEditor2 = editorInstanceRef.current;
44005
+ if (activeEditor2 && !isDraftTableFormula(formulaContext.formula)) {
44006
+ recalculateActiveTableFormulas(activeEditor2);
43530
44007
  }
43531
44008
  return true;
43532
44009
  }
@@ -43536,6 +44013,13 @@ var UEditor = import_react80.default.forwardRef(({
43536
44013
  cancelFormulaEditing(_view);
43537
44014
  return true;
43538
44015
  }
44016
+ const activeEditor = editorInstanceRef.current;
44017
+ if (activeEditor && (event.key === "Backspace" || event.key === "Delete") && getSelectedTableFormulaCell(activeEditor)) {
44018
+ event.preventDefault();
44019
+ event.stopPropagation();
44020
+ clearSelectedTableCellFormula(activeEditor);
44021
+ return true;
44022
+ }
43539
44023
  if (event.key === "ArrowLeft" || event.key === "ArrowRight" || event.key === "ArrowUp" || event.key === "ArrowDown") {
43540
44024
  event.stopPropagation();
43541
44025
  }
@@ -43546,7 +44030,7 @@ var UEditor = import_react80.default.forwardRef(({
43546
44030
  if (event.button !== 0) return false;
43547
44031
  const target = resolveEventElement(event.target);
43548
44032
  const anchor = target?.closest?.("a[href]");
43549
- const href = anchor?.getAttribute("href") ?? "";
44033
+ const href = sanitizeUEditorUrl(anchor?.getAttribute("href") ?? "", "link");
43550
44034
  if (!href) return false;
43551
44035
  if (editable) return false;
43552
44036
  if (!view.state.selection.empty) return false;
@@ -43572,11 +44056,13 @@ var UEditor = import_react80.default.forwardRef(({
43572
44056
  onSelectionUpdate: ({ editor: editor2 }) => {
43573
44057
  scheduleFormulaRecalculate(editor2);
43574
44058
  },
43575
- onBlur: ({ editor: editor2 }) => {
44059
+ onBlur: ({ editor: editor2, event }) => {
44060
+ const nextTarget = event.relatedTarget;
44061
+ if (nextTarget instanceof Element && nextTarget.closest("[data-ueditor-formula-bar]")) return;
43576
44062
  scheduleFormulaRecalculate(editor2, { force: true });
43577
44063
  }
43578
44064
  });
43579
- (0, import_react80.useEffect)(() => {
44065
+ (0, import_react82.useEffect)(() => {
43580
44066
  editorInstanceRef.current = editor;
43581
44067
  return () => {
43582
44068
  if (editorInstanceRef.current === editor) editorInstanceRef.current = null;
@@ -43592,7 +44078,7 @@ var UEditor = import_react80.default.forwardRef(({
43592
44078
  apply: t("tableMenu.apply"),
43593
44079
  cancel: t("imageInput.cancelBtn")
43594
44080
  });
43595
- (0, import_react80.useImperativeHandle)(
44081
+ (0, import_react82.useImperativeHandle)(
43596
44082
  ref,
43597
44083
  () => ({
43598
44084
  editor,
@@ -43617,7 +44103,7 @@ var UEditor = import_react80.default.forwardRef(({
43617
44103
  }),
43618
44104
  [content, editor, uploadImageForSave, uploadFileForSave, uploadImageConcurrency]
43619
44105
  );
43620
- (0, import_react80.useEffect)(() => {
44106
+ (0, import_react82.useEffect)(() => {
43621
44107
  if (!editor) return;
43622
44108
  queueMicrotask(() => {
43623
44109
  if (!editor.isDestroyed) {
@@ -43625,7 +44111,7 @@ var UEditor = import_react80.default.forwardRef(({
43625
44111
  }
43626
44112
  });
43627
44113
  }, [editor]);
43628
- (0, import_react80.useEffect)(() => {
44114
+ (0, import_react82.useEffect)(() => {
43629
44115
  if (!editor) return;
43630
44116
  const nextContent = content ?? "";
43631
44117
  if (lastAppliedContentRef.current === nextContent) return;
@@ -43644,7 +44130,7 @@ var UEditor = import_react80.default.forwardRef(({
43644
44130
  }
43645
44131
  }, [content, editor]);
43646
44132
  if (!editor) {
43647
- return /* @__PURE__ */ (0, import_jsx_runtime100.jsx)(
44133
+ return /* @__PURE__ */ (0, import_jsx_runtime101.jsx)(
43648
44134
  "div",
43649
44135
  {
43650
44136
  className: cn("w-full border bg-background flex items-center justify-center text-muted-foreground", className),
@@ -43653,7 +44139,7 @@ var UEditor = import_react80.default.forwardRef(({
43653
44139
  }
43654
44140
  );
43655
44141
  }
43656
- return /* @__PURE__ */ (0, import_jsx_runtime100.jsxs)(
44142
+ return /* @__PURE__ */ (0, import_jsx_runtime101.jsxs)(
43657
44143
  "div",
43658
44144
  {
43659
44145
  className: cn(
@@ -43669,7 +44155,7 @@ var UEditor = import_react80.default.forwardRef(({
43669
44155
  className
43670
44156
  ),
43671
44157
  children: [
43672
- editable && showMenuBar && /* @__PURE__ */ (0, import_jsx_runtime100.jsx)(
44158
+ editable && showMenuBar && /* @__PURE__ */ (0, import_jsx_runtime101.jsx)(
43673
44159
  MenuBar,
43674
44160
  {
43675
44161
  editor,
@@ -43684,7 +44170,7 @@ var UEditor = import_react80.default.forwardRef(({
43684
44170
  onPreview
43685
44171
  }
43686
44172
  ),
43687
- editable && showToolbar && /* @__PURE__ */ (0, import_jsx_runtime100.jsx)(
44173
+ editable && showToolbar && /* @__PURE__ */ (0, import_jsx_runtime101.jsx)(
43688
44174
  EditorToolbar,
43689
44175
  {
43690
44176
  editor,
@@ -43699,7 +44185,8 @@ var UEditor = import_react80.default.forwardRef(({
43699
44185
  letterSpacings
43700
44186
  }
43701
44187
  ),
43702
- editable && showBubbleMenu && /* @__PURE__ */ (0, import_jsx_runtime100.jsx)(
44188
+ editable && /* @__PURE__ */ (0, import_jsx_runtime101.jsx)(TableFormulaBar, { editor }),
44189
+ editable && showBubbleMenu && /* @__PURE__ */ (0, import_jsx_runtime101.jsx)(
43703
44190
  CustomBubbleMenu,
43704
44191
  {
43705
44192
  editor,
@@ -43707,7 +44194,8 @@ var UEditor = import_react80.default.forwardRef(({
43707
44194
  lineHeights
43708
44195
  }
43709
44196
  ),
43710
- /* @__PURE__ */ (0, import_jsx_runtime100.jsxs)(
44197
+ editable && showFloatingMenu && /* @__PURE__ */ (0, import_jsx_runtime101.jsx)(CustomFloatingMenu, { editor }),
44198
+ /* @__PURE__ */ (0, import_jsx_runtime101.jsxs)(
43711
44199
  "div",
43712
44200
  {
43713
44201
  ref: (node) => {
@@ -43720,7 +44208,7 @@ var UEditor = import_react80.default.forwardRef(({
43720
44208
  maxHeight
43721
44209
  },
43722
44210
  children: [
43723
- /* @__PURE__ */ (0, import_jsx_runtime100.jsx)(
44211
+ /* @__PURE__ */ (0, import_jsx_runtime101.jsx)(
43724
44212
  "span",
43725
44213
  {
43726
44214
  ref: tableColumnGuideRef,
@@ -43728,7 +44216,7 @@ var UEditor = import_react80.default.forwardRef(({
43728
44216
  className: "pointer-events-none absolute z-20 bg-primary opacity-0 transition-opacity duration-100"
43729
44217
  }
43730
44218
  ),
43731
- /* @__PURE__ */ (0, import_jsx_runtime100.jsx)(
44219
+ /* @__PURE__ */ (0, import_jsx_runtime101.jsx)(
43732
44220
  "span",
43733
44221
  {
43734
44222
  ref: tableRowGuideRef,
@@ -43736,7 +44224,7 @@ var UEditor = import_react80.default.forwardRef(({
43736
44224
  className: "pointer-events-none absolute z-20 bg-primary opacity-0 transition-opacity duration-100"
43737
44225
  }
43738
44226
  ),
43739
- /* @__PURE__ */ (0, import_jsx_runtime100.jsx)(
44227
+ /* @__PURE__ */ (0, import_jsx_runtime101.jsx)(
43740
44228
  "span",
43741
44229
  {
43742
44230
  ref: activeTableCellHighlightRef,
@@ -43745,7 +44233,7 @@ var UEditor = import_react80.default.forwardRef(({
43745
44233
  className: "pointer-events-none hidden absolute z-20 rounded-[2px] border-2 border-primary bg-primary/10"
43746
44234
  }
43747
44235
  ),
43748
- /* @__PURE__ */ (0, import_jsx_runtime100.jsx)(
44236
+ /* @__PURE__ */ (0, import_jsx_runtime101.jsx)(
43749
44237
  "div",
43750
44238
  {
43751
44239
  ref: formulaCoordinateOverlayRef,
@@ -43753,7 +44241,7 @@ var UEditor = import_react80.default.forwardRef(({
43753
44241
  className: "pointer-events-none absolute inset-0 z-[21] hidden overflow-visible"
43754
44242
  }
43755
44243
  ),
43756
- formulaRangeHighlight && /* @__PURE__ */ (0, import_jsx_runtime100.jsx)(
44244
+ formulaRangeHighlight && /* @__PURE__ */ (0, import_jsx_runtime101.jsx)(
43757
44245
  "span",
43758
44246
  {
43759
44247
  "aria-hidden": "true",
@@ -43767,9 +44255,9 @@ var UEditor = import_react80.default.forwardRef(({
43767
44255
  }
43768
44256
  }
43769
44257
  ),
43770
- editable && /* @__PURE__ */ (0, import_jsx_runtime100.jsx)(TableControls, { editor, containerRef: editorContentRef }),
43771
- /* @__PURE__ */ (0, import_jsx_runtime100.jsx)(
43772
- import_react81.EditorContent,
44258
+ editable && /* @__PURE__ */ (0, import_jsx_runtime101.jsx)(TableControls, { editor, containerRef: editorContentRef }),
44259
+ /* @__PURE__ */ (0, import_jsx_runtime101.jsx)(
44260
+ import_react83.EditorContent,
43773
44261
  {
43774
44262
  editor,
43775
44263
  className: "min-h-full"
@@ -43778,7 +44266,7 @@ var UEditor = import_react80.default.forwardRef(({
43778
44266
  ]
43779
44267
  }
43780
44268
  ),
43781
- showFooter && showCharacterCount && /* @__PURE__ */ (0, import_jsx_runtime100.jsx)(CharacterCountDisplay, { editor, maxCharacters })
44269
+ showFooter && showCharacterCount && /* @__PURE__ */ (0, import_jsx_runtime101.jsx)(CharacterCountDisplay, { editor, maxCharacters })
43782
44270
  ]
43783
44271
  }
43784
44272
  );