@underverse-ui/underverse 1.0.100 → 1.0.102

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
@@ -23278,8 +23278,8 @@ function useLocale2() {
23278
23278
  }
23279
23279
 
23280
23280
  // src/components/UEditor/UEditor.tsx
23281
- var import_react52 = __toESM(require("react"), 1);
23282
- var import_react53 = require("@tiptap/react");
23281
+ var import_react53 = __toESM(require("react"), 1);
23282
+ var import_react54 = require("@tiptap/react");
23283
23283
 
23284
23284
  // src/components/UEditor/extensions.ts
23285
23285
  var import_extension_document = __toESM(require("@tiptap/extension-document"), 1);
@@ -23653,6 +23653,49 @@ var SlashCommand = import_core.Extension.create({
23653
23653
  // src/components/UEditor/clipboard-images.ts
23654
23654
  var import_core2 = require("@tiptap/core");
23655
23655
  var import_state = require("@tiptap/pm/state");
23656
+
23657
+ // src/components/UEditor/url-safety.ts
23658
+ var LINK_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:"]);
23659
+ var IMAGE_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
23660
+ function normalizeUrlInput(raw) {
23661
+ return raw.trim().replace(/[\u0000-\u001F\u007F\s]+/g, "");
23662
+ }
23663
+ function isProtocolRelativeUrl(value) {
23664
+ return value.startsWith("//");
23665
+ }
23666
+ function isRelativeUrl(value) {
23667
+ return value.startsWith("/") || value.startsWith("./") || value.startsWith("../") || value.startsWith("#");
23668
+ }
23669
+ function isDataImageUrl(value) {
23670
+ return /^data:image\/(?:png|jpe?g|gif|webp|svg\+xml|bmp|x-icon|avif);base64,/i.test(value);
23671
+ }
23672
+ function isSafeUEditorUrl(raw, kind) {
23673
+ const value = normalizeUrlInput(raw);
23674
+ if (!value) return false;
23675
+ if (kind === "image" && isDataImageUrl(value)) return true;
23676
+ if (isRelativeUrl(value)) return true;
23677
+ if (isProtocolRelativeUrl(value)) return false;
23678
+ try {
23679
+ const parsed = new URL(value);
23680
+ return kind === "image" ? IMAGE_PROTOCOLS.has(parsed.protocol) : LINK_PROTOCOLS.has(parsed.protocol);
23681
+ } catch {
23682
+ return false;
23683
+ }
23684
+ }
23685
+ function sanitizeUEditorUrl(raw, kind) {
23686
+ const value = raw.trim();
23687
+ if (!value) return "";
23688
+ if (isSafeUEditorUrl(value, kind)) return normalizeUrlInput(value);
23689
+ if (kind === "link" && !isProtocolRelativeUrl(value) && !/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(value)) {
23690
+ const withProtocol = `https://${value}`;
23691
+ return isSafeUEditorUrl(withProtocol, kind) ? withProtocol : "";
23692
+ }
23693
+ return "";
23694
+ }
23695
+
23696
+ // src/components/UEditor/clipboard-images.ts
23697
+ var DEFAULT_UEDITOR_IMAGE_MAX_FILE_SIZE = 10 * 1024 * 1024;
23698
+ var DEFAULT_UEDITOR_IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif", "image/svg+xml"];
23656
23699
  function getImageFiles(dataTransfer) {
23657
23700
  if (!dataTransfer) return [];
23658
23701
  const itemFiles = [];
@@ -23684,7 +23727,7 @@ async function resolveImageSrc(file, options) {
23684
23727
  if (options.insertMode === "upload" && options.upload) {
23685
23728
  try {
23686
23729
  const result = await options.upload(file);
23687
- const src = typeof result === "string" ? result : "";
23730
+ const src = typeof result === "string" ? sanitizeUEditorUrl(result, "image") : "";
23688
23731
  if (src) return src;
23689
23732
  } catch (err) {
23690
23733
  if (!options.fallbackToDataUrl) throw err;
@@ -23696,8 +23739,8 @@ var ClipboardImages = import_core2.Extension.create({
23696
23739
  name: "clipboardImages",
23697
23740
  addOptions() {
23698
23741
  return {
23699
- maxFileSize: 10 * 1024 * 1024,
23700
- allowedMimeTypes: ["image/png", "image/jpeg", "image/webp", "image/gif", "image/svg+xml"],
23742
+ maxFileSize: DEFAULT_UEDITOR_IMAGE_MAX_FILE_SIZE,
23743
+ allowedMimeTypes: DEFAULT_UEDITOR_IMAGE_MIME_TYPES,
23701
23744
  upload: void 0,
23702
23745
  fallbackToDataUrl: true,
23703
23746
  insertMode: "base64"
@@ -24556,6 +24599,9 @@ function buildUEditorExtensions({
24556
24599
  maxCharacters,
24557
24600
  uploadImage,
24558
24601
  imageInsertMode = "base64",
24602
+ maxImageFileSize,
24603
+ allowedImageMimeTypes,
24604
+ fallbackToDataUrl,
24559
24605
  editable = true
24560
24606
  }) {
24561
24607
  return [
@@ -24609,12 +24655,20 @@ function buildUEditorExtensions({
24609
24655
  import_extension_horizontal_rule.default,
24610
24656
  import_extension_link.default.configure({
24611
24657
  openOnClick: false,
24658
+ protocols: ["http", "https", "mailto", "tel"],
24659
+ isAllowedUri: (url) => isSafeUEditorUrl(url ?? "", "link"),
24612
24660
  HTMLAttributes: {
24613
24661
  class: "text-primary underline underline-offset-2 hover:text-primary/80 cursor-pointer"
24614
24662
  }
24615
24663
  }),
24616
24664
  resizable_image_default,
24617
- ClipboardImages.configure({ upload: uploadImage, insertMode: imageInsertMode }),
24665
+ ClipboardImages.configure({
24666
+ upload: uploadImage,
24667
+ insertMode: imageInsertMode,
24668
+ ...maxImageFileSize !== void 0 ? { maxFileSize: maxImageFileSize } : {},
24669
+ ...allowedImageMimeTypes ? { allowedMimeTypes: allowedImageMimeTypes } : {},
24670
+ ...fallbackToDataUrl !== void 0 ? { fallbackToDataUrl } : {}
24671
+ }),
24618
24672
  import_extension_text_style.TextStyle,
24619
24673
  font_family_default,
24620
24674
  font_size_default,
@@ -24833,11 +24887,7 @@ var import_react48 = require("react");
24833
24887
  var import_lucide_react43 = require("lucide-react");
24834
24888
  var import_jsx_runtime78 = require("react/jsx-runtime");
24835
24889
  function normalizeUrl(raw) {
24836
- const url = raw.trim();
24837
- if (!url) return "";
24838
- if (url.startsWith("#") || url.startsWith("/")) return url;
24839
- if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)) return url;
24840
- return `https://${url}`;
24890
+ return sanitizeUEditorUrl(raw, "link");
24841
24891
  }
24842
24892
  var LinkInput = ({
24843
24893
  onSubmit,
@@ -24882,8 +24932,9 @@ var ImageInput = ({ onSubmit, onCancel }) => {
24882
24932
  }, []);
24883
24933
  const handleSubmit = (e) => {
24884
24934
  e.preventDefault();
24885
- if (url) {
24886
- onSubmit(url, alt);
24935
+ const safeUrl = sanitizeUEditorUrl(url, "image");
24936
+ if (safeUrl) {
24937
+ onSubmit(safeUrl, alt);
24887
24938
  }
24888
24939
  };
24889
24940
  return /* @__PURE__ */ (0, import_jsx_runtime78.jsxs)("form", { onSubmit: handleSubmit, className: "p-3 space-y-3", children: [
@@ -25069,6 +25120,8 @@ var EditorToolbar = ({
25069
25120
  variant,
25070
25121
  uploadImage,
25071
25122
  imageInsertMode = "base64",
25123
+ maxImageFileSize = DEFAULT_UEDITOR_IMAGE_MAX_FILE_SIZE,
25124
+ allowedImageMimeTypes = DEFAULT_UEDITOR_IMAGE_MIME_TYPES,
25072
25125
  fontFamilies,
25073
25126
  fontSizes,
25074
25127
  lineHeights,
@@ -25136,10 +25189,13 @@ var EditorToolbar = ({
25136
25189
  setImageUploadError(null);
25137
25190
  for (const file of files) {
25138
25191
  if (!file.type.startsWith("image/")) continue;
25192
+ if (file.size > maxImageFileSize) continue;
25193
+ if (allowedImageMimeTypes.length > 0 && !allowedImageMimeTypes.includes(file.type)) continue;
25139
25194
  try {
25140
25195
  const src = imageInsertMode === "upload" && uploadImage ? await uploadImage(file) : await fileToDataUrl2(file);
25141
- if (!src) continue;
25142
- editor.chain().focus().setImage({ src, alt: file.name }).run();
25196
+ const safeSrc = sanitizeUEditorUrl(src, "image");
25197
+ if (!safeSrc) continue;
25198
+ editor.chain().focus().setImage({ src: safeSrc, alt: file.name }).run();
25143
25199
  editor.commands.createParagraphNear();
25144
25200
  } catch {
25145
25201
  setImageUploadError(t("imageInput.uploadError"));
@@ -26334,12 +26390,12 @@ var MIME_EXTENSION_MAP = {
26334
26390
  "image/x-icon": "ico",
26335
26391
  "image/avif": "avif"
26336
26392
  };
26337
- function isDataImageUrl(value) {
26393
+ function isDataImageUrl2(value) {
26338
26394
  return /^data:image\//i.test(value.trim());
26339
26395
  }
26340
26396
  function parseDataImageUrl(dataUrl) {
26341
26397
  const value = dataUrl.trim();
26342
- if (!isDataImageUrl(value)) return null;
26398
+ if (!isDataImageUrl2(value)) return null;
26343
26399
  const commaIndex = value.indexOf(",");
26344
26400
  if (commaIndex < 0) return null;
26345
26401
  const header = value.slice(5, commaIndex);
@@ -26373,19 +26429,33 @@ function createFileFromDataImageUrl(dataUrl, index) {
26373
26429
  }
26374
26430
  function normalizeUploadResult(result) {
26375
26431
  if (typeof result === "string") {
26376
- const url2 = result.trim();
26432
+ const url2 = sanitizeUEditorUrl(result, "image");
26377
26433
  if (!url2) throw new Error("Upload handler returned an empty URL.");
26378
26434
  return { url: url2 };
26379
26435
  }
26380
26436
  if (!result || typeof result !== "object") {
26381
26437
  throw new Error("Upload handler returned invalid result.");
26382
26438
  }
26383
- const url = typeof result.url === "string" ? result.url.trim() : "";
26439
+ const url = typeof result.url === "string" ? sanitizeUEditorUrl(result.url, "image") : "";
26384
26440
  if (!url) throw new Error("Upload handler object result is missing `url`.");
26385
26441
  const { url: _ignoredUrl, ...rest } = result;
26386
26442
  const meta = Object.keys(rest).length > 0 ? rest : void 0;
26387
26443
  return { url, meta };
26388
26444
  }
26445
+ async function runWithConcurrency(items, concurrency, worker) {
26446
+ const limit = Math.max(1, Math.floor(concurrency));
26447
+ const results = new Array(items.length);
26448
+ let nextIndex = 0;
26449
+ async function runNext() {
26450
+ while (nextIndex < items.length) {
26451
+ const currentIndex = nextIndex;
26452
+ nextIndex += 1;
26453
+ results[currentIndex] = await worker(items[currentIndex]);
26454
+ }
26455
+ }
26456
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, runNext));
26457
+ return results;
26458
+ }
26389
26459
  function getErrorReason(error) {
26390
26460
  if (error instanceof Error && error.message) return error.message;
26391
26461
  if (typeof error === "string" && error.trim()) return error;
@@ -26397,7 +26467,7 @@ function decodeHtmlEntities(value) {
26397
26467
  function normalizeImageUrl(url) {
26398
26468
  const input = decodeHtmlEntities(url.trim());
26399
26469
  if (!input) return "";
26400
- if (isDataImageUrl(input)) return input;
26470
+ if (isDataImageUrl2(input)) return input;
26401
26471
  const isAbsolute = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(input);
26402
26472
  if (!isAbsolute) {
26403
26473
  return input.split("#")[0] ?? input;
@@ -26481,7 +26551,8 @@ var UEditorPrepareContentForSaveError = class extends Error {
26481
26551
  };
26482
26552
  async function prepareUEditorContentForSave({
26483
26553
  html,
26484
- uploadImageForSave
26554
+ uploadImageForSave,
26555
+ uploadConcurrency = 3
26485
26556
  }) {
26486
26557
  if (!html || !html.includes("<img")) {
26487
26558
  return createResult({ html, uploaded: [], inlineUploaded: [], errors: [] });
@@ -26494,7 +26565,7 @@ async function prepareUEditorContentForSave({
26494
26565
  for (const match of imgMatches) {
26495
26566
  if (!match.srcAttr) continue;
26496
26567
  const src = match.srcAttr.value.trim();
26497
- if (!isDataImageUrl(src)) continue;
26568
+ if (!isDataImageUrl2(src)) continue;
26498
26569
  base64Candidates.push({
26499
26570
  id: `${match.start}:${match.end}`,
26500
26571
  match,
@@ -26520,8 +26591,10 @@ async function prepareUEditorContentForSave({
26520
26591
  const inlineUploaded = [];
26521
26592
  const errors = [];
26522
26593
  const replacements = /* @__PURE__ */ new Map();
26523
- const uploadResults = await Promise.all(
26524
- base64Candidates.map(async (candidate) => {
26594
+ const uploadResults = await runWithConcurrency(
26595
+ base64Candidates,
26596
+ uploadConcurrency,
26597
+ async (candidate) => {
26525
26598
  try {
26526
26599
  const file = createFileFromDataImageUrl(candidate.src, candidate.index);
26527
26600
  const uploadResult = await uploadImageForSave(file);
@@ -26530,7 +26603,7 @@ async function prepareUEditorContentForSave({
26530
26603
  } catch (error) {
26531
26604
  return { candidate, error: getErrorReason(error) };
26532
26605
  }
26533
- })
26606
+ }
26534
26607
  );
26535
26608
  for (const item of uploadResults) {
26536
26609
  if ("error" in item) {
@@ -30732,6 +30805,111 @@ var columnResizingPluginKey = new PluginKey3("tableColumnResizing");
30732
30805
 
30733
30806
  // src/components/UEditor/table-controls.tsx
30734
30807
  var import_lucide_react46 = require("lucide-react");
30808
+
30809
+ // src/components/UEditor/table-dom-utils.ts
30810
+ var MIN_TABLE_ROW_HEIGHT = 36;
30811
+ var COLUMN_RESIZE_LINE_THICKNESS = 2;
30812
+ var ROW_RESIZE_LINE_THICKNESS = 2;
30813
+ var UEDITOR_TABLE_LAYOUT_CHANGE_EVENT = "ueditor-table-layout-change";
30814
+ var TABLE_RESIZE_HIT_ZONE = 10;
30815
+ function findTableRowNodeInfo(view, rowElement) {
30816
+ const firstCell = rowElement.querySelector("th,td");
30817
+ if (!firstCell) return null;
30818
+ const cellPos = view.posAtDOM(firstCell, 0);
30819
+ const $pos = view.state.doc.resolve(cellPos);
30820
+ for (let depth = $pos.depth; depth > 0; depth -= 1) {
30821
+ const node = $pos.node(depth);
30822
+ if (node.type.name === "tableRow") {
30823
+ return {
30824
+ pos: $pos.before(depth),
30825
+ node
30826
+ };
30827
+ }
30828
+ }
30829
+ return null;
30830
+ }
30831
+ function resolveEventElement(target) {
30832
+ if (target instanceof Element) return target;
30833
+ if (target instanceof Node) return target.parentElement;
30834
+ return null;
30835
+ }
30836
+ function getSelectionTableCell(view) {
30837
+ const browserSelection = window.getSelection();
30838
+ const anchorElement = resolveEventElement(browserSelection?.anchorNode ?? null);
30839
+ const anchorCell = anchorElement?.closest?.("th,td");
30840
+ if (anchorCell instanceof HTMLElement) {
30841
+ return anchorCell;
30842
+ }
30843
+ const { from } = view.state.selection;
30844
+ const domAtPos = view.domAtPos(from);
30845
+ const element = resolveEventElement(domAtPos.node);
30846
+ const cell = element?.closest?.("th,td");
30847
+ return cell instanceof HTMLElement ? cell : null;
30848
+ }
30849
+ function isRowResizeHotspot(cell, clientX, clientY) {
30850
+ const rect = cell.getBoundingClientRect();
30851
+ const nearBottom = rect.bottom - clientY <= TABLE_RESIZE_HIT_ZONE;
30852
+ const nearRight = rect.right - clientX <= TABLE_RESIZE_HIT_ZONE;
30853
+ return nearBottom && !nearRight;
30854
+ }
30855
+ function isColumnResizeHotspot(cell, clientX, clientY) {
30856
+ const rect = cell.getBoundingClientRect();
30857
+ const nearRight = rect.right - clientX <= TABLE_RESIZE_HIT_ZONE;
30858
+ const nearBottom = rect.bottom - clientY <= TABLE_RESIZE_HIT_ZONE;
30859
+ return nearRight && !nearBottom;
30860
+ }
30861
+ function getRelativeBoundaryMetrics(surface, table, row, cell) {
30862
+ const surfaceRect = surface.getBoundingClientRect();
30863
+ const tableRect = table.getBoundingClientRect();
30864
+ const rowRect = row.getBoundingClientRect();
30865
+ const cellRect = cell.getBoundingClientRect();
30866
+ return {
30867
+ left: tableRect.left - surfaceRect.left + surface.scrollLeft,
30868
+ top: tableRect.top - surfaceRect.top + surface.scrollTop,
30869
+ width: tableRect.width,
30870
+ height: tableRect.height,
30871
+ rowBottom: rowRect.bottom - surfaceRect.top + surface.scrollTop,
30872
+ columnRight: cellRect.right - surfaceRect.left + surface.scrollLeft
30873
+ };
30874
+ }
30875
+ function getRelativeCellMetrics(surface, cell) {
30876
+ const surfaceRect = surface.getBoundingClientRect();
30877
+ const cellRect = cell.getBoundingClientRect();
30878
+ return {
30879
+ left: cellRect.left - surfaceRect.left + surface.scrollLeft,
30880
+ top: cellRect.top - surfaceRect.top + surface.scrollTop,
30881
+ width: cellRect.width,
30882
+ height: cellRect.height
30883
+ };
30884
+ }
30885
+ function getRelativeSelectedCellsMetrics(surface) {
30886
+ const selectedCells = Array.from(
30887
+ surface.querySelectorAll("td.selectedCell, th.selectedCell")
30888
+ );
30889
+ if (selectedCells.length === 0) {
30890
+ return null;
30891
+ }
30892
+ const surfaceRect = surface.getBoundingClientRect();
30893
+ let left = Number.POSITIVE_INFINITY;
30894
+ let top = Number.POSITIVE_INFINITY;
30895
+ let right = Number.NEGATIVE_INFINITY;
30896
+ let bottom = Number.NEGATIVE_INFINITY;
30897
+ selectedCells.forEach((cell) => {
30898
+ const rect = cell.getBoundingClientRect();
30899
+ left = Math.min(left, rect.left);
30900
+ top = Math.min(top, rect.top);
30901
+ right = Math.max(right, rect.right);
30902
+ bottom = Math.max(bottom, rect.bottom);
30903
+ });
30904
+ return {
30905
+ left: left - surfaceRect.left + surface.scrollLeft,
30906
+ top: top - surfaceRect.top + surface.scrollTop,
30907
+ width: right - left,
30908
+ height: bottom - top
30909
+ };
30910
+ }
30911
+
30912
+ // src/components/UEditor/table-controls.tsx
30735
30913
  var import_jsx_runtime83 = require("react/jsx-runtime");
30736
30914
  var FALLBACK_TABLE_ROW_HEIGHT = 44;
30737
30915
  var FALLBACK_TABLE_COLUMN_WIDTH = 160;
@@ -31031,6 +31209,7 @@ function TableControls({ editor, containerRef }) {
31031
31209
  surface.addEventListener("mouseover", handleSurfaceMouseMove);
31032
31210
  surface.addEventListener("mousemove", handleSurfaceMouseMove);
31033
31211
  surface.addEventListener("scroll", refreshCurrentLayout, { passive: true });
31212
+ surface.addEventListener(UEDITOR_TABLE_LAYOUT_CHANGE_EVENT, refreshCurrentLayout);
31034
31213
  window.addEventListener("resize", refreshCurrentLayout);
31035
31214
  editor.on("selectionUpdate", syncFromSelection);
31036
31215
  editor.on("update", refreshCurrentLayout);
@@ -31042,6 +31221,7 @@ function TableControls({ editor, containerRef }) {
31042
31221
  surface.removeEventListener("mouseover", handleSurfaceMouseMove);
31043
31222
  surface.removeEventListener("mousemove", handleSurfaceMouseMove);
31044
31223
  surface.removeEventListener("scroll", refreshCurrentLayout);
31224
+ surface.removeEventListener(UEDITOR_TABLE_LAYOUT_CHANGE_EVENT, refreshCurrentLayout);
31045
31225
  window.removeEventListener("resize", refreshCurrentLayout);
31046
31226
  editor.off("selectionUpdate", syncFromSelection);
31047
31227
  editor.off("update", refreshCurrentLayout);
@@ -31725,163 +31905,163 @@ function TableControls({ editor, containerRef }) {
31725
31905
  ] });
31726
31906
  }
31727
31907
 
31728
- // src/components/UEditor/UEditor.tsx
31729
- var import_jsx_runtime84 = require("react/jsx-runtime");
31730
- var TABLE_RESIZE_HIT_ZONE = 10;
31731
- var MIN_TABLE_ROW_HEIGHT = 36;
31732
- var COLUMN_RESIZE_LINE_THICKNESS = 2;
31733
- var ROW_RESIZE_LINE_THICKNESS = 2;
31734
- function applyPreviewRowHeight(rowElement, nextHeight) {
31735
- rowElement.style.height = `${nextHeight}px`;
31736
- rowElement.style.minHeight = `${nextHeight}px`;
31737
- rowElement.querySelectorAll("th,td").forEach((cell) => {
31738
- if (cell instanceof HTMLElement) {
31739
- cell.style.height = `${nextHeight}px`;
31740
- cell.style.minHeight = `${nextHeight}px`;
31741
- }
31742
- });
31743
- }
31744
- function clearPreviewRowHeight(rowElement) {
31745
- rowElement.style.height = "";
31746
- rowElement.style.minHeight = "";
31747
- rowElement.querySelectorAll("th,td").forEach((cell) => {
31748
- if (cell instanceof HTMLElement) {
31749
- cell.style.height = "";
31750
- cell.style.minHeight = "";
31751
- }
31752
- });
31753
- }
31754
- function findTableRowNodeInfo(view, rowElement) {
31755
- const firstCell = rowElement.querySelector("th,td");
31756
- if (!firstCell) return null;
31757
- const cellPos = view.posAtDOM(firstCell, 0);
31758
- const $pos = view.state.doc.resolve(cellPos);
31759
- for (let depth = $pos.depth; depth > 0; depth -= 1) {
31760
- const node = $pos.node(depth);
31761
- if (node.type.name === "tableRow") {
31762
- return {
31763
- pos: $pos.before(depth),
31764
- node
31765
- };
31766
- }
31767
- }
31768
- return null;
31769
- }
31770
- function resolveEventElement(target) {
31771
- if (target instanceof Element) return target;
31772
- if (target instanceof Node) return target.parentElement;
31773
- return null;
31774
- }
31775
- function getSelectionTableCell(view) {
31776
- const browserSelection = window.getSelection();
31777
- const anchorElement = resolveEventElement(browserSelection?.anchorNode ?? null);
31778
- const anchorCell = anchorElement?.closest?.("th,td");
31779
- if (anchorCell instanceof HTMLElement) {
31780
- return anchorCell;
31781
- }
31782
- const { from } = view.state.selection;
31783
- const domAtPos = view.domAtPos(from);
31784
- const element = resolveEventElement(domAtPos.node);
31785
- const cell = element?.closest?.("th,td");
31786
- return cell instanceof HTMLElement ? cell : null;
31787
- }
31788
- function isRowResizeHotspot(cell, clientX, clientY) {
31789
- const rect = cell.getBoundingClientRect();
31790
- const nearBottom = rect.bottom - clientY <= TABLE_RESIZE_HIT_ZONE;
31791
- const nearRight = rect.right - clientX <= TABLE_RESIZE_HIT_ZONE;
31792
- return nearBottom && !nearRight;
31793
- }
31794
- function isColumnResizeHotspot(cell, clientX, clientY) {
31795
- const rect = cell.getBoundingClientRect();
31796
- const nearRight = rect.right - clientX <= TABLE_RESIZE_HIT_ZONE;
31797
- const nearBottom = rect.bottom - clientY <= TABLE_RESIZE_HIT_ZONE;
31798
- return nearRight && !nearBottom;
31799
- }
31800
- function getRelativeBoundaryMetrics(surface, table, row, cell) {
31801
- const surfaceRect = surface.getBoundingClientRect();
31802
- const tableRect = table.getBoundingClientRect();
31803
- const rowRect = row.getBoundingClientRect();
31804
- const cellRect = cell.getBoundingClientRect();
31805
- return {
31806
- left: tableRect.left - surfaceRect.left + surface.scrollLeft,
31807
- top: tableRect.top - surfaceRect.top + surface.scrollTop,
31808
- width: tableRect.width,
31809
- height: tableRect.height,
31810
- rowBottom: rowRect.bottom - surfaceRect.top + surface.scrollTop,
31811
- columnRight: cellRect.right - surfaceRect.left + surface.scrollLeft
31812
- };
31813
- }
31814
- function getRelativeCellMetrics(surface, cell) {
31815
- const surfaceRect = surface.getBoundingClientRect();
31816
- const cellRect = cell.getBoundingClientRect();
31817
- return {
31818
- left: cellRect.left - surfaceRect.left + surface.scrollLeft,
31819
- top: cellRect.top - surfaceRect.top + surface.scrollTop,
31820
- width: cellRect.width,
31821
- height: cellRect.height
31822
- };
31823
- }
31824
- function getRelativeSelectedCellsMetrics(surface) {
31825
- const selectedCells = Array.from(
31826
- surface.querySelectorAll("td.selectedCell, th.selectedCell")
31827
- );
31828
- if (selectedCells.length === 0) {
31829
- return null;
31830
- }
31831
- const surfaceRect = surface.getBoundingClientRect();
31832
- let left = Number.POSITIVE_INFINITY;
31833
- let top = Number.POSITIVE_INFINITY;
31834
- let right = Number.NEGATIVE_INFINITY;
31835
- let bottom = Number.NEGATIVE_INFINITY;
31836
- selectedCells.forEach((cell) => {
31837
- const rect = cell.getBoundingClientRect();
31838
- left = Math.min(left, rect.left);
31839
- top = Math.min(top, rect.top);
31840
- right = Math.max(right, rect.right);
31841
- bottom = Math.max(bottom, rect.bottom);
31842
- });
31843
- return {
31844
- left: left - surfaceRect.left + surface.scrollLeft,
31845
- top: top - surfaceRect.top + surface.scrollTop,
31846
- width: right - left,
31847
- height: bottom - top
31848
- };
31849
- }
31850
- var UEditor = import_react52.default.forwardRef(({
31851
- content = "",
31852
- onChange,
31853
- onHtmlChange,
31854
- onJsonChange,
31855
- uploadImage,
31856
- uploadImageForSave,
31857
- imageInsertMode = "base64",
31858
- placeholder,
31859
- className,
31860
- editable = true,
31861
- autofocus = false,
31862
- showToolbar = true,
31863
- showBubbleMenu = true,
31864
- showFloatingMenu = false,
31865
- showCharacterCount = true,
31866
- maxCharacters,
31867
- minHeight = "200px",
31868
- maxHeight = "auto",
31869
- variant = "default",
31870
- fontFamilies,
31871
- fontSizes,
31872
- lineHeights,
31873
- letterSpacings
31874
- }, ref) => {
31875
- const t = useSmartTranslations("UEditor");
31876
- const effectivePlaceholder = placeholder ?? t("placeholder");
31877
- const inFlightPrepareRef = (0, import_react52.useRef)(null);
31878
- const lastAppliedContentRef = (0, import_react52.useRef)(content ?? "");
31908
+ // src/components/UEditor/editor-styles.ts
31909
+ var UEDITOR_PROSEMIRROR_CLASS_NAME = cn(
31910
+ "prose prose-sm sm:prose dark:prose-invert max-w-none",
31911
+ "focus:outline-none",
31912
+ "px-4 py-4",
31913
+ "[&_.is-editor-empty]:before:content-[attr(data-placeholder)]",
31914
+ "[&_.is-editor-empty]:before:text-muted-foreground/50",
31915
+ "[&_.is-editor-empty]:before:float-left",
31916
+ "[&_.is-editor-empty]:before:pointer-events-none",
31917
+ "[&_.is-editor-empty]:before:h-0",
31918
+ "[&_ul[data-type='taskList']]:list-none",
31919
+ "[&_ul[data-type='taskList']]:pl-0",
31920
+ "[&_ul[data-type='taskList']_li]:flex",
31921
+ "[&_ul[data-type='taskList']_li]:items-start",
31922
+ "[&_ul[data-type='taskList']_li]:gap-2",
31923
+ "[&_ul[data-type='taskList']_li>label]:mt-0.5",
31924
+ "[&_ul[data-type='taskList']_li>label>input]:w-4",
31925
+ "[&_ul[data-type='taskList']_li>label>input]:h-4",
31926
+ "[&_ul[data-type='taskList']_li>label>input]:rounded",
31927
+ "[&_ul[data-type='taskList']_li>label>input]:border-2",
31928
+ "[&_ul[data-type='taskList']_li>label>input]:border-primary/50",
31929
+ "[&_ul[data-type='taskList']_li>label>input]:accent-primary",
31930
+ "[&_pre]:bg-muted/40!",
31931
+ "[&_pre]:text-foreground!",
31932
+ "[&_pre]:border!",
31933
+ "[&_pre]:border-border/60!",
31934
+ "[&_pre_code]:bg-transparent!",
31935
+ "[&_.tableWrapper]:overflow-x-auto",
31936
+ "[&_.tableWrapper]:pb-1.5",
31937
+ "[&_.tableWrapper]:select-text",
31938
+ "[&_.tableWrapper]:[scrollbar-width:thin]",
31939
+ "[&_.tableWrapper]:[scrollbar-color:hsl(var(--border))_transparent]",
31940
+ "[&_.tableWrapper::-webkit-scrollbar]:h-2",
31941
+ "[&_.tableWrapper::-webkit-scrollbar]:w-2",
31942
+ "[&_.tableWrapper::-webkit-scrollbar-track]:rounded-full",
31943
+ "[&_.tableWrapper::-webkit-scrollbar-track]:bg-transparent",
31944
+ "[&_.tableWrapper::-webkit-scrollbar-thumb]:rounded-full",
31945
+ "[&_.tableWrapper::-webkit-scrollbar-thumb]:border",
31946
+ "[&_.tableWrapper::-webkit-scrollbar-thumb]:border-solid",
31947
+ "[&_.tableWrapper::-webkit-scrollbar-thumb]:border-transparent",
31948
+ "[&_.tableWrapper::-webkit-scrollbar-thumb]:bg-border/70",
31949
+ "[&_.tableWrapper::-webkit-scrollbar-thumb:hover]:bg-muted-foreground/45",
31950
+ "[&_table]:table-fixed",
31951
+ "[&_table]:overflow-hidden",
31952
+ "[&_table]:select-text",
31953
+ "[&_table[data-table-align]]:w-max",
31954
+ "[&_table[data-table-align]]:max-w-full",
31955
+ "[&_table[data-table-align='center']]:mx-auto",
31956
+ "[&_table[data-table-align='right']]:ml-auto",
31957
+ "[&_table[data-table-align='right']]:mr-0",
31958
+ "[&_td]:relative",
31959
+ "[&_td]:align-top",
31960
+ "[&_td]:box-border",
31961
+ "[&_td]:select-text",
31962
+ "[&_th]:relative",
31963
+ "[&_th]:align-top",
31964
+ "[&_th]:box-border",
31965
+ "[&_th]:select-text",
31966
+ "[&_.selectedCell]:after:content-['']",
31967
+ "[&_.selectedCell]:after:absolute",
31968
+ "[&_.selectedCell]:after:inset-0",
31969
+ "[&_.selectedCell]:after:z-[2]",
31970
+ "[&_.selectedCell]:after:bg-primary/15",
31971
+ "[&_.selectedCell]:after:pointer-events-none",
31972
+ "[&_.column-resize-handle]:pointer-events-auto",
31973
+ "[&_.column-resize-handle]:cursor-col-resize",
31974
+ "[&_.column-resize-handle]:absolute",
31975
+ "[&_.column-resize-handle]:top-[-1px]",
31976
+ "[&_.column-resize-handle]:bottom-[-1px]",
31977
+ "[&_.column-resize-handle]:right-[-5px]",
31978
+ "[&_.column-resize-handle]:z-10",
31979
+ "[&_.column-resize-handle]:w-2.5",
31980
+ "[&_.column-resize-handle]:bg-transparent",
31981
+ "[&_.column-resize-handle]:rounded-none",
31982
+ "[&_.column-resize-handle]:opacity-0",
31983
+ "[&_.column-resize-handle]:transition-opacity",
31984
+ "[&_.column-resize-handle]:after:absolute",
31985
+ "[&_.column-resize-handle]:after:top-0",
31986
+ "[&_.column-resize-handle]:after:bottom-0",
31987
+ "[&_.column-resize-handle]:after:left-1/2",
31988
+ "[&_.column-resize-handle]:after:w-0.5",
31989
+ "[&_.column-resize-handle]:after:-translate-x-1/2",
31990
+ "[&_.column-resize-handle]:after:rounded-full",
31991
+ "[&_.column-resize-handle]:after:bg-primary/75",
31992
+ "[&_.column-resize-handle]:after:content-['']",
31993
+ "[&.resize-cursor_.column-resize-handle]:opacity-100",
31994
+ "[&.resize-cursor_.column-resize-handle]:after:bg-primary",
31995
+ "[&.resize-cursor]:cursor-col-resize",
31996
+ "[&.resize-row-cursor]:cursor-row-resize",
31997
+ "[&_img.ProseMirror-selectednode]:ring-2",
31998
+ "[&_img.ProseMirror-selectednode]:ring-primary/60",
31999
+ "[&_img.ProseMirror-selectednode]:ring-offset-2",
32000
+ "[&_img.ProseMirror-selectednode]:ring-offset-background",
32001
+ "[&_hr]:border-t-2",
32002
+ "[&_hr]:border-primary/30",
32003
+ "[&_hr]:my-8",
32004
+ "[&_h1]:text-3xl",
32005
+ "[&_h1]:font-bold",
32006
+ "[&_h1]:mt-6",
32007
+ "[&_h1]:mb-4",
32008
+ "[&_h1]:text-foreground",
32009
+ "[&_h2]:text-2xl",
32010
+ "[&_h2]:font-semibold",
32011
+ "[&_h2]:mt-5",
32012
+ "[&_h2]:mb-3",
32013
+ "[&_h2]:text-foreground",
32014
+ "[&_h3]:text-xl",
32015
+ "[&_h3]:font-semibold",
32016
+ "[&_h3]:mt-4",
32017
+ "[&_h3]:mb-2",
32018
+ "[&_h3]:text-foreground",
32019
+ "[&_ul:not([data-type='taskList'])]:list-disc",
32020
+ "[&_ul:not([data-type='taskList'])]:pl-6",
32021
+ "[&_ul:not([data-type='taskList'])]:my-3",
32022
+ "[&_ol]:list-decimal",
32023
+ "[&_ol]:pl-6",
32024
+ "[&_ol]:my-3",
32025
+ "[&_li]:my-1",
32026
+ "[&_li]:pl-1",
32027
+ "[&_li_p]:my-0",
32028
+ "[&_blockquote]:border-l-4",
32029
+ "[&_blockquote]:border-primary",
32030
+ "[&_blockquote]:pl-4",
32031
+ "[&_blockquote]:py-2",
32032
+ "[&_blockquote]:my-4",
32033
+ "[&_blockquote]:bg-muted/30",
32034
+ "[&_blockquote]:rounded-r-lg",
32035
+ "[&_blockquote]:italic",
32036
+ "[&_blockquote]:text-muted-foreground",
32037
+ "[&_blockquote_p]:my-0",
32038
+ "[&_[data-image-layout='left']+p]:mt-1",
32039
+ "[&_[data-image-layout='left']+p]:min-h-[5rem]",
32040
+ "[&_[data-image-layout='right']+p]:mt-1",
32041
+ "[&_[data-image-layout='right']+p]:min-h-[5rem]",
32042
+ "max-md:[&_[data-image-layout='left']]:float-none",
32043
+ "max-md:[&_[data-image-layout='left']]:mr-0",
32044
+ "max-md:[&_[data-image-layout='left']]:ml-0",
32045
+ "max-md:[&_[data-image-layout='left']]:max-w-full",
32046
+ "max-md:[&_[data-image-layout='right']]:float-none",
32047
+ "max-md:[&_[data-image-layout='right']]:mr-0",
32048
+ "max-md:[&_[data-image-layout='right']]:ml-0",
32049
+ "max-md:[&_[data-image-layout='right']]:max-w-full",
32050
+ "max-md:[&_[data-image-layout='left']+p]:min-h-0",
32051
+ "max-md:[&_[data-image-layout='right']+p]:min-h-0"
32052
+ );
32053
+
32054
+ // src/components/UEditor/use-table-interactions.ts
32055
+ var import_react52 = __toESM(require("react"), 1);
32056
+ function useUEditorTableInteractions(editor) {
31879
32057
  const editorContentRef = (0, import_react52.useRef)(null);
31880
32058
  const tableColumnGuideRef = (0, import_react52.useRef)(null);
31881
32059
  const tableRowGuideRef = (0, import_react52.useRef)(null);
31882
32060
  const activeTableCellHighlightRef = (0, import_react52.useRef)(null);
31883
32061
  const hoveredTableCellRef = (0, import_react52.useRef)(null);
31884
32062
  const activeTableCellRef = (0, import_react52.useRef)(null);
32063
+ const tableLayoutSyncFrameRef = (0, import_react52.useRef)(null);
32064
+ const rowResizeCommitFrameRef = (0, import_react52.useRef)(null);
31885
32065
  const rowResizeStateRef = (0, import_react52.useRef)(null);
31886
32066
  const setEditorResizeCursor = import_react52.default.useCallback((cursor) => {
31887
32067
  const proseMirror = editorContentRef.current?.querySelector(".ProseMirror");
@@ -31923,6 +32103,14 @@ var UEditor = import_react52.default.forwardRef(({
31923
32103
  highlight.style.width = `${metrics.width}px`;
31924
32104
  highlight.style.height = `${metrics.height}px`;
31925
32105
  }, []);
32106
+ const scheduleTableLayoutSync = import_react52.default.useCallback(() => {
32107
+ if (tableLayoutSyncFrameRef.current !== null) return;
32108
+ tableLayoutSyncFrameRef.current = window.requestAnimationFrame(() => {
32109
+ tableLayoutSyncFrameRef.current = null;
32110
+ updateActiveCellHighlight(activeTableCellRef.current);
32111
+ editorContentRef.current?.dispatchEvent(new CustomEvent(UEDITOR_TABLE_LAYOUT_CHANGE_EVENT));
32112
+ });
32113
+ }, [updateActiveCellHighlight]);
31926
32114
  const setActiveTableCell = import_react52.default.useCallback((cell) => {
31927
32115
  if (activeTableCellRef.current === cell) return;
31928
32116
  activeTableCellRef.current = cell;
@@ -31964,228 +32152,49 @@ var UEditor = import_react52.default.forwardRef(({
31964
32152
  surface.classList.add("resize-row-cursor");
31965
32153
  setEditorResizeCursor("row-resize");
31966
32154
  }, [setEditorResizeCursor]);
31967
- const extensions = (0, import_react52.useMemo)(
31968
- () => buildUEditorExtensions({ placeholder: effectivePlaceholder, translate: t, maxCharacters, uploadImage, imageInsertMode, editable }),
31969
- [effectivePlaceholder, t, maxCharacters, uploadImage, imageInsertMode, editable]
31970
- );
31971
- const editor = (0, import_react53.useEditor)({
31972
- immediatelyRender: false,
31973
- extensions,
31974
- content,
31975
- editable,
31976
- autofocus,
31977
- editorProps: {
31978
- handleDOMEvents: {
31979
- keydown: (_view, event) => {
31980
- if (!(event instanceof KeyboardEvent)) return false;
31981
- if (event.key === "ArrowLeft" || event.key === "ArrowRight" || event.key === "ArrowUp" || event.key === "ArrowDown") {
31982
- event.stopPropagation();
31983
- }
31984
- return false;
31985
- },
31986
- click: (view, event) => {
31987
- if (!(event instanceof MouseEvent)) return false;
31988
- if (event.button !== 0) return false;
31989
- const target = resolveEventElement(event.target);
31990
- const anchor = target?.closest?.("a[href]");
31991
- const href = anchor?.getAttribute("href") ?? "";
31992
- if (!href) return false;
31993
- if (!view.state.selection.empty) return false;
31994
- event.preventDefault();
31995
- event.stopPropagation();
31996
- window.open(href, "_blank", "noopener,noreferrer");
31997
- return true;
31998
- }
31999
- },
32000
- attributes: {
32001
- class: cn(
32002
- "prose prose-sm sm:prose dark:prose-invert max-w-none",
32003
- "focus:outline-none",
32004
- "px-4 py-4",
32005
- "[&_.is-editor-empty]:before:content-[attr(data-placeholder)]",
32006
- "[&_.is-editor-empty]:before:text-muted-foreground/50",
32007
- "[&_.is-editor-empty]:before:float-left",
32008
- "[&_.is-editor-empty]:before:pointer-events-none",
32009
- "[&_.is-editor-empty]:before:h-0",
32010
- "[&_ul[data-type='taskList']]:list-none",
32011
- "[&_ul[data-type='taskList']]:pl-0",
32012
- "[&_ul[data-type='taskList']_li]:flex",
32013
- "[&_ul[data-type='taskList']_li]:items-start",
32014
- "[&_ul[data-type='taskList']_li]:gap-2",
32015
- "[&_ul[data-type='taskList']_li>label]:mt-0.5",
32016
- "[&_ul[data-type='taskList']_li>label>input]:w-4",
32017
- "[&_ul[data-type='taskList']_li>label>input]:h-4",
32018
- "[&_ul[data-type='taskList']_li>label>input]:rounded",
32019
- "[&_ul[data-type='taskList']_li>label>input]:border-2",
32020
- "[&_ul[data-type='taskList']_li>label>input]:border-primary/50",
32021
- "[&_ul[data-type='taskList']_li>label>input]:accent-primary",
32022
- "[&_pre]:bg-muted/40!",
32023
- "[&_pre]:text-foreground!",
32024
- "[&_pre]:border!",
32025
- "[&_pre]:border-border/60!",
32026
- "[&_pre_code]:bg-transparent!",
32027
- "[&_.tableWrapper]:overflow-x-auto",
32028
- "[&_.tableWrapper]:pb-1.5",
32029
- "[&_.tableWrapper]:select-text",
32030
- "[&_.tableWrapper]:[scrollbar-width:thin]",
32031
- "[&_.tableWrapper]:[scrollbar-color:hsl(var(--border))_transparent]",
32032
- "[&_.tableWrapper::-webkit-scrollbar]:h-2",
32033
- "[&_.tableWrapper::-webkit-scrollbar]:w-2",
32034
- "[&_.tableWrapper::-webkit-scrollbar-track]:rounded-full",
32035
- "[&_.tableWrapper::-webkit-scrollbar-track]:bg-transparent",
32036
- "[&_.tableWrapper::-webkit-scrollbar-thumb]:rounded-full",
32037
- "[&_.tableWrapper::-webkit-scrollbar-thumb]:border",
32038
- "[&_.tableWrapper::-webkit-scrollbar-thumb]:border-solid",
32039
- "[&_.tableWrapper::-webkit-scrollbar-thumb]:border-transparent",
32040
- "[&_.tableWrapper::-webkit-scrollbar-thumb]:bg-border/70",
32041
- "[&_.tableWrapper::-webkit-scrollbar-thumb:hover]:bg-muted-foreground/45",
32042
- "[&_table]:table-fixed",
32043
- "[&_table]:overflow-hidden",
32044
- "[&_table]:select-text",
32045
- "[&_table[data-table-align]]:w-max",
32046
- "[&_table[data-table-align]]:max-w-full",
32047
- "[&_table[data-table-align='center']]:mx-auto",
32048
- "[&_table[data-table-align='right']]:ml-auto",
32049
- "[&_table[data-table-align='right']]:mr-0",
32050
- "[&_td]:relative",
32051
- "[&_td]:align-top",
32052
- "[&_td]:box-border",
32053
- "[&_td]:select-text",
32054
- "[&_th]:relative",
32055
- "[&_th]:align-top",
32056
- "[&_th]:box-border",
32057
- "[&_th]:select-text",
32058
- "[&_.selectedCell]:after:content-['']",
32059
- "[&_.selectedCell]:after:absolute",
32060
- "[&_.selectedCell]:after:inset-0",
32061
- "[&_.selectedCell]:after:z-[2]",
32062
- "[&_.selectedCell]:after:bg-primary/15",
32063
- "[&_.selectedCell]:after:pointer-events-none",
32064
- "[&_.column-resize-handle]:pointer-events-auto",
32065
- "[&_.column-resize-handle]:cursor-col-resize",
32066
- "[&_.column-resize-handle]:absolute",
32067
- "[&_.column-resize-handle]:top-[-1px]",
32068
- "[&_.column-resize-handle]:bottom-[-1px]",
32069
- "[&_.column-resize-handle]:right-[-5px]",
32070
- "[&_.column-resize-handle]:z-10",
32071
- "[&_.column-resize-handle]:w-2.5",
32072
- "[&_.column-resize-handle]:bg-transparent",
32073
- "[&_.column-resize-handle]:rounded-none",
32074
- "[&_.column-resize-handle]:opacity-0",
32075
- "[&_.column-resize-handle]:transition-opacity",
32076
- "[&_.column-resize-handle]:after:absolute",
32077
- "[&_.column-resize-handle]:after:top-0",
32078
- "[&_.column-resize-handle]:after:bottom-0",
32079
- "[&_.column-resize-handle]:after:left-1/2",
32080
- "[&_.column-resize-handle]:after:w-0.5",
32081
- "[&_.column-resize-handle]:after:-translate-x-1/2",
32082
- "[&_.column-resize-handle]:after:rounded-full",
32083
- "[&_.column-resize-handle]:after:bg-primary/75",
32084
- "[&_.column-resize-handle]:after:content-['']",
32085
- "[&.resize-cursor_.column-resize-handle]:opacity-100",
32086
- "[&.resize-cursor_.column-resize-handle]:after:bg-primary",
32087
- "[&.resize-cursor]:cursor-col-resize",
32088
- "[&.resize-row-cursor]:cursor-row-resize",
32089
- "[&_img.ProseMirror-selectednode]:ring-2",
32090
- "[&_img.ProseMirror-selectednode]:ring-primary/60",
32091
- "[&_img.ProseMirror-selectednode]:ring-offset-2",
32092
- "[&_img.ProseMirror-selectednode]:ring-offset-background",
32093
- "[&_hr]:border-t-2",
32094
- "[&_hr]:border-primary/30",
32095
- "[&_hr]:my-8",
32096
- "[&_h1]:text-3xl",
32097
- "[&_h1]:font-bold",
32098
- "[&_h1]:mt-6",
32099
- "[&_h1]:mb-4",
32100
- "[&_h1]:text-foreground",
32101
- "[&_h2]:text-2xl",
32102
- "[&_h2]:font-semibold",
32103
- "[&_h2]:mt-5",
32104
- "[&_h2]:mb-3",
32105
- "[&_h2]:text-foreground",
32106
- "[&_h3]:text-xl",
32107
- "[&_h3]:font-semibold",
32108
- "[&_h3]:mt-4",
32109
- "[&_h3]:mb-2",
32110
- "[&_h3]:text-foreground",
32111
- "[&_ul:not([data-type='taskList'])]:list-disc",
32112
- "[&_ul:not([data-type='taskList'])]:pl-6",
32113
- "[&_ul:not([data-type='taskList'])]:my-3",
32114
- "[&_ol]:list-decimal",
32115
- "[&_ol]:pl-6",
32116
- "[&_ol]:my-3",
32117
- "[&_li]:my-1",
32118
- "[&_li]:pl-1",
32119
- "[&_li_p]:my-0",
32120
- "[&_blockquote]:border-l-4",
32121
- "[&_blockquote]:border-primary",
32122
- "[&_blockquote]:pl-4",
32123
- "[&_blockquote]:py-2",
32124
- "[&_blockquote]:my-4",
32125
- "[&_blockquote]:bg-muted/30",
32126
- "[&_blockquote]:rounded-r-lg",
32127
- "[&_blockquote]:italic",
32128
- "[&_blockquote]:text-muted-foreground",
32129
- "[&_blockquote_p]:my-0",
32130
- "[&_[data-image-layout='left']+p]:mt-1",
32131
- "[&_[data-image-layout='left']+p]:min-h-[5rem]",
32132
- "[&_[data-image-layout='right']+p]:mt-1",
32133
- "[&_[data-image-layout='right']+p]:min-h-[5rem]",
32134
- "max-md:[&_[data-image-layout='left']]:float-none",
32135
- "max-md:[&_[data-image-layout='left']]:mr-0",
32136
- "max-md:[&_[data-image-layout='left']]:ml-0",
32137
- "max-md:[&_[data-image-layout='left']]:max-w-full",
32138
- "max-md:[&_[data-image-layout='right']]:float-none",
32139
- "max-md:[&_[data-image-layout='right']]:mr-0",
32140
- "max-md:[&_[data-image-layout='right']]:ml-0",
32141
- "max-md:[&_[data-image-layout='right']]:max-w-full",
32142
- "max-md:[&_[data-image-layout='left']+p]:min-h-0",
32143
- "max-md:[&_[data-image-layout='right']+p]:min-h-0"
32144
- )
32145
- }
32146
- },
32147
- onUpdate: ({ editor: editor2 }) => {
32148
- const html = editor2.getHTML();
32149
- onChange?.(html);
32150
- onHtmlChange?.(html);
32151
- onJsonChange?.(editor2.getJSON());
32155
+ const commitRowResizePreview = import_react52.default.useCallback(() => {
32156
+ if (!editor) return;
32157
+ const state = rowResizeStateRef.current;
32158
+ if (!state) return;
32159
+ const nextHeight = state.pendingHeight;
32160
+ if (nextHeight === state.previewHeight) {
32161
+ document.body.style.cursor = "row-resize";
32162
+ showRowGuide(state.tableElement, state.rowElement, state.cellElement);
32163
+ scheduleTableLayoutSync();
32164
+ return;
32152
32165
  }
32153
- });
32166
+ state.previewHeight = nextHeight;
32167
+ const tr = editor.view.state.tr;
32168
+ tr.setNodeMarkup(state.rowPos, void 0, {
32169
+ ...state.rowNode.attrs,
32170
+ rowHeight: nextHeight
32171
+ });
32172
+ tr.setMeta("addToHistory", false);
32173
+ editor.view.dispatch(tr);
32174
+ state.rowNode = editor.view.state.doc.nodeAt(state.rowPos) ?? state.rowNode;
32175
+ const refreshedRow = state.tableElement.rows.item(state.rowElement.rowIndex);
32176
+ if (refreshedRow instanceof HTMLTableRowElement) {
32177
+ state.rowElement = refreshedRow;
32178
+ const refreshedCell = refreshedRow.cells.item(state.cellIndex);
32179
+ if (refreshedCell instanceof HTMLTableCellElement) {
32180
+ state.cellElement = refreshedCell;
32181
+ }
32182
+ }
32183
+ document.body.style.cursor = "row-resize";
32184
+ showRowGuide(state.tableElement, state.rowElement, state.cellElement);
32185
+ scheduleTableLayoutSync();
32186
+ }, [editor, scheduleTableLayoutSync, showRowGuide]);
32187
+ const scheduleRowResizeCommit = import_react52.default.useCallback(() => {
32188
+ if (rowResizeCommitFrameRef.current !== null) return;
32189
+ rowResizeCommitFrameRef.current = window.requestAnimationFrame(() => {
32190
+ rowResizeCommitFrameRef.current = null;
32191
+ commitRowResizePreview();
32192
+ });
32193
+ }, [commitRowResizePreview]);
32154
32194
  const syncActiveTableCellFromSelection = import_react52.default.useCallback(() => {
32155
32195
  if (!editor) return;
32156
32196
  setActiveTableCell(getSelectionTableCell(editor.view));
32157
32197
  }, [editor, setActiveTableCell]);
32158
- (0, import_react52.useImperativeHandle)(
32159
- ref,
32160
- () => ({
32161
- prepareContentForSave: async ({ throwOnError = false } = {}) => {
32162
- if (!inFlightPrepareRef.current) {
32163
- const htmlSnapshot = editor?.getHTML() ?? content ?? "";
32164
- inFlightPrepareRef.current = prepareUEditorContentForSave({
32165
- html: htmlSnapshot,
32166
- uploadImageForSave
32167
- }).finally(() => {
32168
- inFlightPrepareRef.current = null;
32169
- });
32170
- }
32171
- const result = await inFlightPrepareRef.current;
32172
- if (throwOnError && result.errors.length > 0) {
32173
- throw new UEditorPrepareContentForSaveError(result);
32174
- }
32175
- return result;
32176
- }
32177
- }),
32178
- [content, editor, uploadImageForSave]
32179
- );
32180
- (0, import_react52.useEffect)(() => {
32181
- if (!editor) return;
32182
- const nextContent = content ?? "";
32183
- if (lastAppliedContentRef.current === nextContent) return;
32184
- lastAppliedContentRef.current = nextContent;
32185
- if (editor.getHTML() !== nextContent) {
32186
- editor.commands.setContent(nextContent, { emitUpdate: false });
32187
- }
32188
- }, [content, editor]);
32189
32198
  (0, import_react52.useEffect)(() => {
32190
32199
  if (!editor) return void 0;
32191
32200
  const proseMirror = editor.view.dom;
@@ -32275,6 +32284,7 @@ var UEditor = import_react52.default.forwardRef(({
32275
32284
  setHoveredTableCell(cell);
32276
32285
  const rowInfo = findTableRowNodeInfo(editor.view, row);
32277
32286
  if (!rowInfo) return;
32287
+ const startHeight = row.getBoundingClientRect().height;
32278
32288
  rowResizeStateRef.current = {
32279
32289
  rowElement: row,
32280
32290
  tableElement: table,
@@ -32283,8 +32293,9 @@ var UEditor = import_react52.default.forwardRef(({
32283
32293
  rowPos: rowInfo.pos,
32284
32294
  rowNode: rowInfo.node,
32285
32295
  startY: event.clientY,
32286
- startHeight: row.getBoundingClientRect().height,
32287
- previewHeight: row.getBoundingClientRect().height
32296
+ startHeight,
32297
+ previewHeight: startHeight,
32298
+ pendingHeight: startHeight
32288
32299
  };
32289
32300
  showRowGuide(table, row, cell);
32290
32301
  document.body.style.cursor = "row-resize";
@@ -32298,30 +32309,14 @@ var UEditor = import_react52.default.forwardRef(({
32298
32309
  MIN_TABLE_ROW_HEIGHT,
32299
32310
  Math.round(state.startHeight + (event.clientY - state.startY))
32300
32311
  );
32301
- if (nextHeight === state.previewHeight) {
32312
+ if (nextHeight === state.pendingHeight) {
32302
32313
  document.body.style.cursor = "row-resize";
32303
32314
  showRowGuide(state.tableElement, state.rowElement, state.cellElement);
32304
32315
  return;
32305
32316
  }
32306
- state.previewHeight = nextHeight;
32307
- applyPreviewRowHeight(state.rowElement, nextHeight);
32308
- const tr = editor.view.state.tr;
32309
- tr.setNodeMarkup(state.rowPos, void 0, {
32310
- ...state.rowNode.attrs,
32311
- rowHeight: nextHeight
32312
- });
32313
- editor.view.dispatch(tr);
32314
- state.rowNode = editor.view.state.doc.nodeAt(state.rowPos) ?? state.rowNode;
32315
- const refreshedRow = state.tableElement.rows.item(state.rowElement.rowIndex);
32316
- if (refreshedRow instanceof HTMLTableRowElement) {
32317
- state.rowElement = refreshedRow;
32318
- const refreshedCell = refreshedRow.cells.item(state.cellIndex);
32319
- if (refreshedCell instanceof HTMLTableCellElement) {
32320
- state.cellElement = refreshedCell;
32321
- }
32322
- }
32317
+ state.pendingHeight = nextHeight;
32323
32318
  document.body.style.cursor = "row-resize";
32324
- showRowGuide(state.tableElement, state.rowElement, state.cellElement);
32319
+ scheduleRowResizeCommit();
32325
32320
  };
32326
32321
  const handlePointerUp = (event) => {
32327
32322
  const state = rowResizeStateRef.current;
@@ -32330,20 +32325,40 @@ var UEditor = import_react52.default.forwardRef(({
32330
32325
  MIN_TABLE_ROW_HEIGHT,
32331
32326
  Math.round(state.startHeight + (event.clientY - state.startY))
32332
32327
  );
32333
- clearPreviewRowHeight(state.rowElement);
32328
+ state.pendingHeight = nextHeight;
32329
+ if (rowResizeCommitFrameRef.current !== null) {
32330
+ window.cancelAnimationFrame(rowResizeCommitFrameRef.current);
32331
+ rowResizeCommitFrameRef.current = null;
32332
+ }
32333
+ commitRowResizePreview();
32334
+ const latestState = rowResizeStateRef.current ?? state;
32335
+ const rowNode = editor.view.state.doc.nodeAt(latestState.rowPos) ?? latestState.rowNode;
32336
+ if (rowNode.attrs.rowHeight !== nextHeight) {
32337
+ const tr = editor.view.state.tr;
32338
+ tr.setNodeMarkup(latestState.rowPos, void 0, {
32339
+ ...rowNode.attrs,
32340
+ rowHeight: nextHeight
32341
+ });
32342
+ editor.view.dispatch(tr);
32343
+ }
32334
32344
  rowResizeStateRef.current = null;
32335
32345
  document.body.style.cursor = "";
32336
32346
  clearHoveredTableCell();
32337
32347
  clearAllTableResizeHover();
32348
+ scheduleTableLayoutSync();
32338
32349
  };
32339
32350
  const handleWindowBlur = () => {
32340
32351
  const state = rowResizeStateRef.current;
32341
32352
  if (!state) return;
32342
- clearPreviewRowHeight(state.rowElement);
32353
+ if (rowResizeCommitFrameRef.current !== null) {
32354
+ window.cancelAnimationFrame(rowResizeCommitFrameRef.current);
32355
+ rowResizeCommitFrameRef.current = null;
32356
+ }
32343
32357
  rowResizeStateRef.current = null;
32344
32358
  document.body.style.cursor = "";
32345
32359
  clearHoveredTableCell();
32346
32360
  clearAllTableResizeHover();
32361
+ scheduleTableLayoutSync();
32347
32362
  };
32348
32363
  proseMirror.addEventListener("mousemove", handleEditorMouseMove);
32349
32364
  proseMirror.addEventListener("mouseleave", handleEditorMouseLeave);
@@ -32382,13 +32397,156 @@ var UEditor = import_react52.default.forwardRef(({
32382
32397
  editor.off("selectionUpdate", syncActiveTableCellFromSelection);
32383
32398
  editor.off("focus", syncActiveTableCellFromSelection);
32384
32399
  window.clearTimeout(selectionSyncTimeoutId);
32400
+ if (tableLayoutSyncFrameRef.current !== null) {
32401
+ window.cancelAnimationFrame(tableLayoutSyncFrameRef.current);
32402
+ tableLayoutSyncFrameRef.current = null;
32403
+ }
32404
+ if (rowResizeCommitFrameRef.current !== null) {
32405
+ window.cancelAnimationFrame(rowResizeCommitFrameRef.current);
32406
+ rowResizeCommitFrameRef.current = null;
32407
+ }
32385
32408
  document.body.style.cursor = "";
32386
32409
  clearActiveTableCell();
32387
32410
  clearHoveredTableCell();
32388
32411
  clearAllTableResizeHover();
32389
32412
  rowResizeStateRef.current = null;
32390
32413
  };
32391
- }, [clearActiveTableCell, clearAllTableResizeHover, clearHoveredTableCell, editor, hideColumnGuide, hideRowGuide, setHoveredTableCell, showColumnGuide, showRowGuide, syncActiveTableCellFromSelection, updateActiveCellHighlight]);
32414
+ }, [clearActiveTableCell, clearAllTableResizeHover, clearHoveredTableCell, commitRowResizePreview, editor, hideColumnGuide, hideRowGuide, scheduleRowResizeCommit, scheduleTableLayoutSync, setHoveredTableCell, showColumnGuide, showRowGuide, syncActiveTableCellFromSelection, updateActiveCellHighlight]);
32415
+ return {
32416
+ editorContentRef,
32417
+ tableColumnGuideRef,
32418
+ tableRowGuideRef,
32419
+ activeTableCellHighlightRef
32420
+ };
32421
+ }
32422
+
32423
+ // src/components/UEditor/UEditor.tsx
32424
+ var import_jsx_runtime84 = require("react/jsx-runtime");
32425
+ var UEditor = import_react53.default.forwardRef(({
32426
+ content = "",
32427
+ onChange,
32428
+ onHtmlChange,
32429
+ onJsonChange,
32430
+ uploadImage,
32431
+ uploadImageForSave,
32432
+ uploadImageConcurrency = 3,
32433
+ imageInsertMode = "base64",
32434
+ maxImageFileSize,
32435
+ allowedImageMimeTypes,
32436
+ fallbackToDataUrl,
32437
+ placeholder,
32438
+ className,
32439
+ editable = true,
32440
+ autofocus = false,
32441
+ showToolbar = true,
32442
+ showBubbleMenu = true,
32443
+ showFloatingMenu = false,
32444
+ showCharacterCount = true,
32445
+ maxCharacters,
32446
+ minHeight = "200px",
32447
+ maxHeight = "auto",
32448
+ variant = "default",
32449
+ fontFamilies,
32450
+ fontSizes,
32451
+ lineHeights,
32452
+ letterSpacings
32453
+ }, ref) => {
32454
+ const t = useSmartTranslations("UEditor");
32455
+ const effectivePlaceholder = placeholder ?? t("placeholder");
32456
+ const inFlightPrepareRef = (0, import_react53.useRef)(null);
32457
+ const lastAppliedContentRef = (0, import_react53.useRef)(content ?? "");
32458
+ const extensions = (0, import_react53.useMemo)(
32459
+ () => buildUEditorExtensions({
32460
+ placeholder: effectivePlaceholder,
32461
+ translate: t,
32462
+ maxCharacters,
32463
+ uploadImage,
32464
+ imageInsertMode,
32465
+ maxImageFileSize,
32466
+ allowedImageMimeTypes,
32467
+ fallbackToDataUrl,
32468
+ editable
32469
+ }),
32470
+ [effectivePlaceholder, t, maxCharacters, uploadImage, imageInsertMode, maxImageFileSize, allowedImageMimeTypes, fallbackToDataUrl, editable]
32471
+ );
32472
+ const editor = (0, import_react54.useEditor)({
32473
+ immediatelyRender: false,
32474
+ extensions,
32475
+ content,
32476
+ editable,
32477
+ autofocus,
32478
+ editorProps: {
32479
+ handleDOMEvents: {
32480
+ keydown: (_view, event) => {
32481
+ if (!(event instanceof KeyboardEvent)) return false;
32482
+ if (event.key === "ArrowLeft" || event.key === "ArrowRight" || event.key === "ArrowUp" || event.key === "ArrowDown") {
32483
+ event.stopPropagation();
32484
+ }
32485
+ return false;
32486
+ },
32487
+ click: (view, event) => {
32488
+ if (!(event instanceof MouseEvent)) return false;
32489
+ if (event.button !== 0) return false;
32490
+ const target = resolveEventElement(event.target);
32491
+ const anchor = target?.closest?.("a[href]");
32492
+ const href = anchor?.getAttribute("href") ?? "";
32493
+ if (!href) return false;
32494
+ if (!view.state.selection.empty) return false;
32495
+ event.preventDefault();
32496
+ event.stopPropagation();
32497
+ window.open(href, "_blank", "noopener,noreferrer");
32498
+ return true;
32499
+ }
32500
+ },
32501
+ attributes: {
32502
+ class: UEDITOR_PROSEMIRROR_CLASS_NAME
32503
+ }
32504
+ },
32505
+ onUpdate: ({ editor: editor2 }) => {
32506
+ const html = editor2.getHTML();
32507
+ onChange?.(html);
32508
+ onHtmlChange?.(html);
32509
+ onJsonChange?.(editor2.getJSON());
32510
+ }
32511
+ });
32512
+ const {
32513
+ editorContentRef,
32514
+ tableColumnGuideRef,
32515
+ tableRowGuideRef,
32516
+ activeTableCellHighlightRef
32517
+ } = useUEditorTableInteractions(editor);
32518
+ (0, import_react53.useImperativeHandle)(
32519
+ ref,
32520
+ () => ({
32521
+ prepareContentForSave: async ({ throwOnError = false } = {}) => {
32522
+ if (!inFlightPrepareRef.current) {
32523
+ const htmlSnapshot = editor?.getHTML() ?? content ?? "";
32524
+ inFlightPrepareRef.current = prepareUEditorContentForSave({
32525
+ html: htmlSnapshot,
32526
+ uploadImageForSave,
32527
+ uploadConcurrency: uploadImageConcurrency
32528
+ }).finally(() => {
32529
+ inFlightPrepareRef.current = null;
32530
+ });
32531
+ }
32532
+ const result = await inFlightPrepareRef.current;
32533
+ if (throwOnError && result.errors.length > 0) {
32534
+ throw new UEditorPrepareContentForSaveError(result);
32535
+ }
32536
+ return result;
32537
+ }
32538
+ }),
32539
+ [content, editor, uploadImageForSave, uploadImageConcurrency]
32540
+ );
32541
+ (0, import_react53.useEffect)(() => {
32542
+ if (!editor) return;
32543
+ const nextContent = content ?? "";
32544
+ if (lastAppliedContentRef.current === nextContent) return;
32545
+ lastAppliedContentRef.current = nextContent;
32546
+ if (editor.getHTML() !== nextContent) {
32547
+ editor.commands.setContent(nextContent, { emitUpdate: false });
32548
+ }
32549
+ }, [content, editor]);
32392
32550
  if (!editor) {
32393
32551
  return /* @__PURE__ */ (0, import_jsx_runtime84.jsx)(
32394
32552
  "div",
@@ -32418,6 +32576,8 @@ var UEditor = import_react52.default.forwardRef(({
32418
32576
  variant,
32419
32577
  uploadImage,
32420
32578
  imageInsertMode,
32579
+ maxImageFileSize,
32580
+ allowedImageMimeTypes,
32421
32581
  fontFamilies,
32422
32582
  fontSizes,
32423
32583
  lineHeights,
@@ -32469,7 +32629,7 @@ var UEditor = import_react52.default.forwardRef(({
32469
32629
  ),
32470
32630
  editable && /* @__PURE__ */ (0, import_jsx_runtime84.jsx)(TableControls, { editor, containerRef: editorContentRef }),
32471
32631
  /* @__PURE__ */ (0, import_jsx_runtime84.jsx)(
32472
- import_react53.EditorContent,
32632
+ import_react54.EditorContent,
32473
32633
  {
32474
32634
  editor,
32475
32635
  className: "min-h-full"