@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.js CHANGED
@@ -23109,7 +23109,7 @@ function useLocale2() {
23109
23109
  }
23110
23110
 
23111
23111
  // src/components/UEditor/UEditor.tsx
23112
- import React74, { useEffect as useEffect35, useImperativeHandle as useImperativeHandle3, useMemo as useMemo24, useRef as useRef31 } from "react";
23112
+ import React75, { useEffect as useEffect36, useImperativeHandle as useImperativeHandle3, useMemo as useMemo24, useRef as useRef32 } from "react";
23113
23113
  import { useEditor, EditorContent } from "@tiptap/react";
23114
23114
 
23115
23115
  // src/components/UEditor/extensions.ts
@@ -23496,6 +23496,49 @@ var SlashCommand = Extension.create({
23496
23496
  // src/components/UEditor/clipboard-images.ts
23497
23497
  import { Extension as Extension2 } from "@tiptap/core";
23498
23498
  import { Plugin } from "@tiptap/pm/state";
23499
+
23500
+ // src/components/UEditor/url-safety.ts
23501
+ var LINK_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:"]);
23502
+ var IMAGE_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
23503
+ function normalizeUrlInput(raw) {
23504
+ return raw.trim().replace(/[\u0000-\u001F\u007F\s]+/g, "");
23505
+ }
23506
+ function isProtocolRelativeUrl(value) {
23507
+ return value.startsWith("//");
23508
+ }
23509
+ function isRelativeUrl(value) {
23510
+ return value.startsWith("/") || value.startsWith("./") || value.startsWith("../") || value.startsWith("#");
23511
+ }
23512
+ function isDataImageUrl(value) {
23513
+ return /^data:image\/(?:png|jpe?g|gif|webp|svg\+xml|bmp|x-icon|avif);base64,/i.test(value);
23514
+ }
23515
+ function isSafeUEditorUrl(raw, kind) {
23516
+ const value = normalizeUrlInput(raw);
23517
+ if (!value) return false;
23518
+ if (kind === "image" && isDataImageUrl(value)) return true;
23519
+ if (isRelativeUrl(value)) return true;
23520
+ if (isProtocolRelativeUrl(value)) return false;
23521
+ try {
23522
+ const parsed = new URL(value);
23523
+ return kind === "image" ? IMAGE_PROTOCOLS.has(parsed.protocol) : LINK_PROTOCOLS.has(parsed.protocol);
23524
+ } catch {
23525
+ return false;
23526
+ }
23527
+ }
23528
+ function sanitizeUEditorUrl(raw, kind) {
23529
+ const value = raw.trim();
23530
+ if (!value) return "";
23531
+ if (isSafeUEditorUrl(value, kind)) return normalizeUrlInput(value);
23532
+ if (kind === "link" && !isProtocolRelativeUrl(value) && !/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(value)) {
23533
+ const withProtocol = `https://${value}`;
23534
+ return isSafeUEditorUrl(withProtocol, kind) ? withProtocol : "";
23535
+ }
23536
+ return "";
23537
+ }
23538
+
23539
+ // src/components/UEditor/clipboard-images.ts
23540
+ var DEFAULT_UEDITOR_IMAGE_MAX_FILE_SIZE = 10 * 1024 * 1024;
23541
+ var DEFAULT_UEDITOR_IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif", "image/svg+xml"];
23499
23542
  function getImageFiles(dataTransfer) {
23500
23543
  if (!dataTransfer) return [];
23501
23544
  const itemFiles = [];
@@ -23527,7 +23570,7 @@ async function resolveImageSrc(file, options) {
23527
23570
  if (options.insertMode === "upload" && options.upload) {
23528
23571
  try {
23529
23572
  const result = await options.upload(file);
23530
- const src = typeof result === "string" ? result : "";
23573
+ const src = typeof result === "string" ? sanitizeUEditorUrl(result, "image") : "";
23531
23574
  if (src) return src;
23532
23575
  } catch (err) {
23533
23576
  if (!options.fallbackToDataUrl) throw err;
@@ -23539,8 +23582,8 @@ var ClipboardImages = Extension2.create({
23539
23582
  name: "clipboardImages",
23540
23583
  addOptions() {
23541
23584
  return {
23542
- maxFileSize: 10 * 1024 * 1024,
23543
- allowedMimeTypes: ["image/png", "image/jpeg", "image/webp", "image/gif", "image/svg+xml"],
23585
+ maxFileSize: DEFAULT_UEDITOR_IMAGE_MAX_FILE_SIZE,
23586
+ allowedMimeTypes: DEFAULT_UEDITOR_IMAGE_MIME_TYPES,
23544
23587
  upload: void 0,
23545
23588
  fallbackToDataUrl: true,
23546
23589
  insertMode: "base64"
@@ -24399,6 +24442,9 @@ function buildUEditorExtensions({
24399
24442
  maxCharacters,
24400
24443
  uploadImage,
24401
24444
  imageInsertMode = "base64",
24445
+ maxImageFileSize,
24446
+ allowedImageMimeTypes,
24447
+ fallbackToDataUrl,
24402
24448
  editable = true
24403
24449
  }) {
24404
24450
  return [
@@ -24452,12 +24498,20 @@ function buildUEditorExtensions({
24452
24498
  HorizontalRule,
24453
24499
  Link.configure({
24454
24500
  openOnClick: false,
24501
+ protocols: ["http", "https", "mailto", "tel"],
24502
+ isAllowedUri: (url) => isSafeUEditorUrl(url ?? "", "link"),
24455
24503
  HTMLAttributes: {
24456
24504
  class: "text-primary underline underline-offset-2 hover:text-primary/80 cursor-pointer"
24457
24505
  }
24458
24506
  }),
24459
24507
  resizable_image_default,
24460
- ClipboardImages.configure({ upload: uploadImage, insertMode: imageInsertMode }),
24508
+ ClipboardImages.configure({
24509
+ upload: uploadImage,
24510
+ insertMode: imageInsertMode,
24511
+ ...maxImageFileSize !== void 0 ? { maxFileSize: maxImageFileSize } : {},
24512
+ ...allowedImageMimeTypes ? { allowedMimeTypes: allowedImageMimeTypes } : {},
24513
+ ...fallbackToDataUrl !== void 0 ? { fallbackToDataUrl } : {}
24514
+ }),
24461
24515
  TextStyle,
24462
24516
  font_family_default,
24463
24517
  font_size_default,
@@ -24713,11 +24767,7 @@ import { useEffect as useEffect33, useRef as useRef28, useState as useState43 }
24713
24767
  import { Check as Check10, X as X19 } from "lucide-react";
24714
24768
  import { jsx as jsx78, jsxs as jsxs66 } from "react/jsx-runtime";
24715
24769
  function normalizeUrl(raw) {
24716
- const url = raw.trim();
24717
- if (!url) return "";
24718
- if (url.startsWith("#") || url.startsWith("/")) return url;
24719
- if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)) return url;
24720
- return `https://${url}`;
24770
+ return sanitizeUEditorUrl(raw, "link");
24721
24771
  }
24722
24772
  var LinkInput = ({
24723
24773
  onSubmit,
@@ -24762,8 +24812,9 @@ var ImageInput = ({ onSubmit, onCancel }) => {
24762
24812
  }, []);
24763
24813
  const handleSubmit = (e) => {
24764
24814
  e.preventDefault();
24765
- if (url) {
24766
- onSubmit(url, alt);
24815
+ const safeUrl = sanitizeUEditorUrl(url, "image");
24816
+ if (safeUrl) {
24817
+ onSubmit(safeUrl, alt);
24767
24818
  }
24768
24819
  };
24769
24820
  return /* @__PURE__ */ jsxs66("form", { onSubmit: handleSubmit, className: "p-3 space-y-3", children: [
@@ -24949,6 +25000,8 @@ var EditorToolbar = ({
24949
25000
  variant,
24950
25001
  uploadImage,
24951
25002
  imageInsertMode = "base64",
25003
+ maxImageFileSize = DEFAULT_UEDITOR_IMAGE_MAX_FILE_SIZE,
25004
+ allowedImageMimeTypes = DEFAULT_UEDITOR_IMAGE_MIME_TYPES,
24952
25005
  fontFamilies,
24953
25006
  fontSizes,
24954
25007
  lineHeights,
@@ -25016,10 +25069,13 @@ var EditorToolbar = ({
25016
25069
  setImageUploadError(null);
25017
25070
  for (const file of files) {
25018
25071
  if (!file.type.startsWith("image/")) continue;
25072
+ if (file.size > maxImageFileSize) continue;
25073
+ if (allowedImageMimeTypes.length > 0 && !allowedImageMimeTypes.includes(file.type)) continue;
25019
25074
  try {
25020
25075
  const src = imageInsertMode === "upload" && uploadImage ? await uploadImage(file) : await fileToDataUrl2(file);
25021
- if (!src) continue;
25022
- editor.chain().focus().setImage({ src, alt: file.name }).run();
25076
+ const safeSrc = sanitizeUEditorUrl(src, "image");
25077
+ if (!safeSrc) continue;
25078
+ editor.chain().focus().setImage({ src: safeSrc, alt: file.name }).run();
25023
25079
  editor.commands.createParagraphNear();
25024
25080
  } catch {
25025
25081
  setImageUploadError(t("imageInput.uploadError"));
@@ -26230,12 +26286,12 @@ var MIME_EXTENSION_MAP = {
26230
26286
  "image/x-icon": "ico",
26231
26287
  "image/avif": "avif"
26232
26288
  };
26233
- function isDataImageUrl(value) {
26289
+ function isDataImageUrl2(value) {
26234
26290
  return /^data:image\//i.test(value.trim());
26235
26291
  }
26236
26292
  function parseDataImageUrl(dataUrl) {
26237
26293
  const value = dataUrl.trim();
26238
- if (!isDataImageUrl(value)) return null;
26294
+ if (!isDataImageUrl2(value)) return null;
26239
26295
  const commaIndex = value.indexOf(",");
26240
26296
  if (commaIndex < 0) return null;
26241
26297
  const header = value.slice(5, commaIndex);
@@ -26269,19 +26325,33 @@ function createFileFromDataImageUrl(dataUrl, index) {
26269
26325
  }
26270
26326
  function normalizeUploadResult(result) {
26271
26327
  if (typeof result === "string") {
26272
- const url2 = result.trim();
26328
+ const url2 = sanitizeUEditorUrl(result, "image");
26273
26329
  if (!url2) throw new Error("Upload handler returned an empty URL.");
26274
26330
  return { url: url2 };
26275
26331
  }
26276
26332
  if (!result || typeof result !== "object") {
26277
26333
  throw new Error("Upload handler returned invalid result.");
26278
26334
  }
26279
- const url = typeof result.url === "string" ? result.url.trim() : "";
26335
+ const url = typeof result.url === "string" ? sanitizeUEditorUrl(result.url, "image") : "";
26280
26336
  if (!url) throw new Error("Upload handler object result is missing `url`.");
26281
26337
  const { url: _ignoredUrl, ...rest } = result;
26282
26338
  const meta = Object.keys(rest).length > 0 ? rest : void 0;
26283
26339
  return { url, meta };
26284
26340
  }
26341
+ async function runWithConcurrency(items, concurrency, worker) {
26342
+ const limit = Math.max(1, Math.floor(concurrency));
26343
+ const results = new Array(items.length);
26344
+ let nextIndex = 0;
26345
+ async function runNext() {
26346
+ while (nextIndex < items.length) {
26347
+ const currentIndex = nextIndex;
26348
+ nextIndex += 1;
26349
+ results[currentIndex] = await worker(items[currentIndex]);
26350
+ }
26351
+ }
26352
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, runNext));
26353
+ return results;
26354
+ }
26285
26355
  function getErrorReason(error) {
26286
26356
  if (error instanceof Error && error.message) return error.message;
26287
26357
  if (typeof error === "string" && error.trim()) return error;
@@ -26293,7 +26363,7 @@ function decodeHtmlEntities(value) {
26293
26363
  function normalizeImageUrl(url) {
26294
26364
  const input = decodeHtmlEntities(url.trim());
26295
26365
  if (!input) return "";
26296
- if (isDataImageUrl(input)) return input;
26366
+ if (isDataImageUrl2(input)) return input;
26297
26367
  const isAbsolute = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(input);
26298
26368
  if (!isAbsolute) {
26299
26369
  return input.split("#")[0] ?? input;
@@ -26377,7 +26447,8 @@ var UEditorPrepareContentForSaveError = class extends Error {
26377
26447
  };
26378
26448
  async function prepareUEditorContentForSave({
26379
26449
  html,
26380
- uploadImageForSave
26450
+ uploadImageForSave,
26451
+ uploadConcurrency = 3
26381
26452
  }) {
26382
26453
  if (!html || !html.includes("<img")) {
26383
26454
  return createResult({ html, uploaded: [], inlineUploaded: [], errors: [] });
@@ -26390,7 +26461,7 @@ async function prepareUEditorContentForSave({
26390
26461
  for (const match of imgMatches) {
26391
26462
  if (!match.srcAttr) continue;
26392
26463
  const src = match.srcAttr.value.trim();
26393
- if (!isDataImageUrl(src)) continue;
26464
+ if (!isDataImageUrl2(src)) continue;
26394
26465
  base64Candidates.push({
26395
26466
  id: `${match.start}:${match.end}`,
26396
26467
  match,
@@ -26416,8 +26487,10 @@ async function prepareUEditorContentForSave({
26416
26487
  const inlineUploaded = [];
26417
26488
  const errors = [];
26418
26489
  const replacements = /* @__PURE__ */ new Map();
26419
- const uploadResults = await Promise.all(
26420
- base64Candidates.map(async (candidate) => {
26490
+ const uploadResults = await runWithConcurrency(
26491
+ base64Candidates,
26492
+ uploadConcurrency,
26493
+ async (candidate) => {
26421
26494
  try {
26422
26495
  const file = createFileFromDataImageUrl(candidate.src, candidate.index);
26423
26496
  const uploadResult = await uploadImageForSave(file);
@@ -26426,7 +26499,7 @@ async function prepareUEditorContentForSave({
26426
26499
  } catch (error) {
26427
26500
  return { candidate, error: getErrorReason(error) };
26428
26501
  }
26429
- })
26502
+ }
26430
26503
  );
26431
26504
  for (const item of uploadResults) {
26432
26505
  if ("error" in item) {
@@ -30642,6 +30715,111 @@ import {
30642
30715
  Table as TableIcon2,
30643
30716
  Trash2 as Trash24
30644
30717
  } from "lucide-react";
30718
+
30719
+ // src/components/UEditor/table-dom-utils.ts
30720
+ var MIN_TABLE_ROW_HEIGHT = 36;
30721
+ var COLUMN_RESIZE_LINE_THICKNESS = 2;
30722
+ var ROW_RESIZE_LINE_THICKNESS = 2;
30723
+ var UEDITOR_TABLE_LAYOUT_CHANGE_EVENT = "ueditor-table-layout-change";
30724
+ var TABLE_RESIZE_HIT_ZONE = 10;
30725
+ function findTableRowNodeInfo(view, rowElement) {
30726
+ const firstCell = rowElement.querySelector("th,td");
30727
+ if (!firstCell) return null;
30728
+ const cellPos = view.posAtDOM(firstCell, 0);
30729
+ const $pos = view.state.doc.resolve(cellPos);
30730
+ for (let depth = $pos.depth; depth > 0; depth -= 1) {
30731
+ const node = $pos.node(depth);
30732
+ if (node.type.name === "tableRow") {
30733
+ return {
30734
+ pos: $pos.before(depth),
30735
+ node
30736
+ };
30737
+ }
30738
+ }
30739
+ return null;
30740
+ }
30741
+ function resolveEventElement(target) {
30742
+ if (target instanceof Element) return target;
30743
+ if (target instanceof Node) return target.parentElement;
30744
+ return null;
30745
+ }
30746
+ function getSelectionTableCell(view) {
30747
+ const browserSelection = window.getSelection();
30748
+ const anchorElement = resolveEventElement(browserSelection?.anchorNode ?? null);
30749
+ const anchorCell = anchorElement?.closest?.("th,td");
30750
+ if (anchorCell instanceof HTMLElement) {
30751
+ return anchorCell;
30752
+ }
30753
+ const { from } = view.state.selection;
30754
+ const domAtPos = view.domAtPos(from);
30755
+ const element = resolveEventElement(domAtPos.node);
30756
+ const cell = element?.closest?.("th,td");
30757
+ return cell instanceof HTMLElement ? cell : null;
30758
+ }
30759
+ function isRowResizeHotspot(cell, clientX, clientY) {
30760
+ const rect = cell.getBoundingClientRect();
30761
+ const nearBottom = rect.bottom - clientY <= TABLE_RESIZE_HIT_ZONE;
30762
+ const nearRight = rect.right - clientX <= TABLE_RESIZE_HIT_ZONE;
30763
+ return nearBottom && !nearRight;
30764
+ }
30765
+ function isColumnResizeHotspot(cell, clientX, clientY) {
30766
+ const rect = cell.getBoundingClientRect();
30767
+ const nearRight = rect.right - clientX <= TABLE_RESIZE_HIT_ZONE;
30768
+ const nearBottom = rect.bottom - clientY <= TABLE_RESIZE_HIT_ZONE;
30769
+ return nearRight && !nearBottom;
30770
+ }
30771
+ function getRelativeBoundaryMetrics(surface, table, row, cell) {
30772
+ const surfaceRect = surface.getBoundingClientRect();
30773
+ const tableRect = table.getBoundingClientRect();
30774
+ const rowRect = row.getBoundingClientRect();
30775
+ const cellRect = cell.getBoundingClientRect();
30776
+ return {
30777
+ left: tableRect.left - surfaceRect.left + surface.scrollLeft,
30778
+ top: tableRect.top - surfaceRect.top + surface.scrollTop,
30779
+ width: tableRect.width,
30780
+ height: tableRect.height,
30781
+ rowBottom: rowRect.bottom - surfaceRect.top + surface.scrollTop,
30782
+ columnRight: cellRect.right - surfaceRect.left + surface.scrollLeft
30783
+ };
30784
+ }
30785
+ function getRelativeCellMetrics(surface, cell) {
30786
+ const surfaceRect = surface.getBoundingClientRect();
30787
+ const cellRect = cell.getBoundingClientRect();
30788
+ return {
30789
+ left: cellRect.left - surfaceRect.left + surface.scrollLeft,
30790
+ top: cellRect.top - surfaceRect.top + surface.scrollTop,
30791
+ width: cellRect.width,
30792
+ height: cellRect.height
30793
+ };
30794
+ }
30795
+ function getRelativeSelectedCellsMetrics(surface) {
30796
+ const selectedCells = Array.from(
30797
+ surface.querySelectorAll("td.selectedCell, th.selectedCell")
30798
+ );
30799
+ if (selectedCells.length === 0) {
30800
+ return null;
30801
+ }
30802
+ const surfaceRect = surface.getBoundingClientRect();
30803
+ let left = Number.POSITIVE_INFINITY;
30804
+ let top = Number.POSITIVE_INFINITY;
30805
+ let right = Number.NEGATIVE_INFINITY;
30806
+ let bottom = Number.NEGATIVE_INFINITY;
30807
+ selectedCells.forEach((cell) => {
30808
+ const rect = cell.getBoundingClientRect();
30809
+ left = Math.min(left, rect.left);
30810
+ top = Math.min(top, rect.top);
30811
+ right = Math.max(right, rect.right);
30812
+ bottom = Math.max(bottom, rect.bottom);
30813
+ });
30814
+ return {
30815
+ left: left - surfaceRect.left + surface.scrollLeft,
30816
+ top: top - surfaceRect.top + surface.scrollTop,
30817
+ width: right - left,
30818
+ height: bottom - top
30819
+ };
30820
+ }
30821
+
30822
+ // src/components/UEditor/table-controls.tsx
30645
30823
  import { Fragment as Fragment27, jsx as jsx82, jsxs as jsxs70 } from "react/jsx-runtime";
30646
30824
  var FALLBACK_TABLE_ROW_HEIGHT = 44;
30647
30825
  var FALLBACK_TABLE_COLUMN_WIDTH = 160;
@@ -30941,6 +31119,7 @@ function TableControls({ editor, containerRef }) {
30941
31119
  surface.addEventListener("mouseover", handleSurfaceMouseMove);
30942
31120
  surface.addEventListener("mousemove", handleSurfaceMouseMove);
30943
31121
  surface.addEventListener("scroll", refreshCurrentLayout, { passive: true });
31122
+ surface.addEventListener(UEDITOR_TABLE_LAYOUT_CHANGE_EVENT, refreshCurrentLayout);
30944
31123
  window.addEventListener("resize", refreshCurrentLayout);
30945
31124
  editor.on("selectionUpdate", syncFromSelection);
30946
31125
  editor.on("update", refreshCurrentLayout);
@@ -30952,6 +31131,7 @@ function TableControls({ editor, containerRef }) {
30952
31131
  surface.removeEventListener("mouseover", handleSurfaceMouseMove);
30953
31132
  surface.removeEventListener("mousemove", handleSurfaceMouseMove);
30954
31133
  surface.removeEventListener("scroll", refreshCurrentLayout);
31134
+ surface.removeEventListener(UEDITOR_TABLE_LAYOUT_CHANGE_EVENT, refreshCurrentLayout);
30955
31135
  window.removeEventListener("resize", refreshCurrentLayout);
30956
31136
  editor.off("selectionUpdate", syncFromSelection);
30957
31137
  editor.off("update", refreshCurrentLayout);
@@ -31635,163 +31815,163 @@ function TableControls({ editor, containerRef }) {
31635
31815
  ] });
31636
31816
  }
31637
31817
 
31638
- // src/components/UEditor/UEditor.tsx
31639
- import { jsx as jsx83, jsxs as jsxs71 } from "react/jsx-runtime";
31640
- var TABLE_RESIZE_HIT_ZONE = 10;
31641
- var MIN_TABLE_ROW_HEIGHT = 36;
31642
- var COLUMN_RESIZE_LINE_THICKNESS = 2;
31643
- var ROW_RESIZE_LINE_THICKNESS = 2;
31644
- function applyPreviewRowHeight(rowElement, nextHeight) {
31645
- rowElement.style.height = `${nextHeight}px`;
31646
- rowElement.style.minHeight = `${nextHeight}px`;
31647
- rowElement.querySelectorAll("th,td").forEach((cell) => {
31648
- if (cell instanceof HTMLElement) {
31649
- cell.style.height = `${nextHeight}px`;
31650
- cell.style.minHeight = `${nextHeight}px`;
31651
- }
31652
- });
31653
- }
31654
- function clearPreviewRowHeight(rowElement) {
31655
- rowElement.style.height = "";
31656
- rowElement.style.minHeight = "";
31657
- rowElement.querySelectorAll("th,td").forEach((cell) => {
31658
- if (cell instanceof HTMLElement) {
31659
- cell.style.height = "";
31660
- cell.style.minHeight = "";
31661
- }
31662
- });
31663
- }
31664
- function findTableRowNodeInfo(view, rowElement) {
31665
- const firstCell = rowElement.querySelector("th,td");
31666
- if (!firstCell) return null;
31667
- const cellPos = view.posAtDOM(firstCell, 0);
31668
- const $pos = view.state.doc.resolve(cellPos);
31669
- for (let depth = $pos.depth; depth > 0; depth -= 1) {
31670
- const node = $pos.node(depth);
31671
- if (node.type.name === "tableRow") {
31672
- return {
31673
- pos: $pos.before(depth),
31674
- node
31675
- };
31676
- }
31677
- }
31678
- return null;
31679
- }
31680
- function resolveEventElement(target) {
31681
- if (target instanceof Element) return target;
31682
- if (target instanceof Node) return target.parentElement;
31683
- return null;
31684
- }
31685
- function getSelectionTableCell(view) {
31686
- const browserSelection = window.getSelection();
31687
- const anchorElement = resolveEventElement(browserSelection?.anchorNode ?? null);
31688
- const anchorCell = anchorElement?.closest?.("th,td");
31689
- if (anchorCell instanceof HTMLElement) {
31690
- return anchorCell;
31691
- }
31692
- const { from } = view.state.selection;
31693
- const domAtPos = view.domAtPos(from);
31694
- const element = resolveEventElement(domAtPos.node);
31695
- const cell = element?.closest?.("th,td");
31696
- return cell instanceof HTMLElement ? cell : null;
31697
- }
31698
- function isRowResizeHotspot(cell, clientX, clientY) {
31699
- const rect = cell.getBoundingClientRect();
31700
- const nearBottom = rect.bottom - clientY <= TABLE_RESIZE_HIT_ZONE;
31701
- const nearRight = rect.right - clientX <= TABLE_RESIZE_HIT_ZONE;
31702
- return nearBottom && !nearRight;
31703
- }
31704
- function isColumnResizeHotspot(cell, clientX, clientY) {
31705
- const rect = cell.getBoundingClientRect();
31706
- const nearRight = rect.right - clientX <= TABLE_RESIZE_HIT_ZONE;
31707
- const nearBottom = rect.bottom - clientY <= TABLE_RESIZE_HIT_ZONE;
31708
- return nearRight && !nearBottom;
31709
- }
31710
- function getRelativeBoundaryMetrics(surface, table, row, cell) {
31711
- const surfaceRect = surface.getBoundingClientRect();
31712
- const tableRect = table.getBoundingClientRect();
31713
- const rowRect = row.getBoundingClientRect();
31714
- const cellRect = cell.getBoundingClientRect();
31715
- return {
31716
- left: tableRect.left - surfaceRect.left + surface.scrollLeft,
31717
- top: tableRect.top - surfaceRect.top + surface.scrollTop,
31718
- width: tableRect.width,
31719
- height: tableRect.height,
31720
- rowBottom: rowRect.bottom - surfaceRect.top + surface.scrollTop,
31721
- columnRight: cellRect.right - surfaceRect.left + surface.scrollLeft
31722
- };
31723
- }
31724
- function getRelativeCellMetrics(surface, cell) {
31725
- const surfaceRect = surface.getBoundingClientRect();
31726
- const cellRect = cell.getBoundingClientRect();
31727
- return {
31728
- left: cellRect.left - surfaceRect.left + surface.scrollLeft,
31729
- top: cellRect.top - surfaceRect.top + surface.scrollTop,
31730
- width: cellRect.width,
31731
- height: cellRect.height
31732
- };
31733
- }
31734
- function getRelativeSelectedCellsMetrics(surface) {
31735
- const selectedCells = Array.from(
31736
- surface.querySelectorAll("td.selectedCell, th.selectedCell")
31737
- );
31738
- if (selectedCells.length === 0) {
31739
- return null;
31740
- }
31741
- const surfaceRect = surface.getBoundingClientRect();
31742
- let left = Number.POSITIVE_INFINITY;
31743
- let top = Number.POSITIVE_INFINITY;
31744
- let right = Number.NEGATIVE_INFINITY;
31745
- let bottom = Number.NEGATIVE_INFINITY;
31746
- selectedCells.forEach((cell) => {
31747
- const rect = cell.getBoundingClientRect();
31748
- left = Math.min(left, rect.left);
31749
- top = Math.min(top, rect.top);
31750
- right = Math.max(right, rect.right);
31751
- bottom = Math.max(bottom, rect.bottom);
31752
- });
31753
- return {
31754
- left: left - surfaceRect.left + surface.scrollLeft,
31755
- top: top - surfaceRect.top + surface.scrollTop,
31756
- width: right - left,
31757
- height: bottom - top
31758
- };
31759
- }
31760
- var UEditor = React74.forwardRef(({
31761
- content = "",
31762
- onChange,
31763
- onHtmlChange,
31764
- onJsonChange,
31765
- uploadImage,
31766
- uploadImageForSave,
31767
- imageInsertMode = "base64",
31768
- placeholder,
31769
- className,
31770
- editable = true,
31771
- autofocus = false,
31772
- showToolbar = true,
31773
- showBubbleMenu = true,
31774
- showFloatingMenu = false,
31775
- showCharacterCount = true,
31776
- maxCharacters,
31777
- minHeight = "200px",
31778
- maxHeight = "auto",
31779
- variant = "default",
31780
- fontFamilies,
31781
- fontSizes,
31782
- lineHeights,
31783
- letterSpacings
31784
- }, ref) => {
31785
- const t = useSmartTranslations("UEditor");
31786
- const effectivePlaceholder = placeholder ?? t("placeholder");
31787
- const inFlightPrepareRef = useRef31(null);
31788
- const lastAppliedContentRef = useRef31(content ?? "");
31818
+ // src/components/UEditor/editor-styles.ts
31819
+ var UEDITOR_PROSEMIRROR_CLASS_NAME = cn(
31820
+ "prose prose-sm sm:prose dark:prose-invert max-w-none",
31821
+ "focus:outline-none",
31822
+ "px-4 py-4",
31823
+ "[&_.is-editor-empty]:before:content-[attr(data-placeholder)]",
31824
+ "[&_.is-editor-empty]:before:text-muted-foreground/50",
31825
+ "[&_.is-editor-empty]:before:float-left",
31826
+ "[&_.is-editor-empty]:before:pointer-events-none",
31827
+ "[&_.is-editor-empty]:before:h-0",
31828
+ "[&_ul[data-type='taskList']]:list-none",
31829
+ "[&_ul[data-type='taskList']]:pl-0",
31830
+ "[&_ul[data-type='taskList']_li]:flex",
31831
+ "[&_ul[data-type='taskList']_li]:items-start",
31832
+ "[&_ul[data-type='taskList']_li]:gap-2",
31833
+ "[&_ul[data-type='taskList']_li>label]:mt-0.5",
31834
+ "[&_ul[data-type='taskList']_li>label>input]:w-4",
31835
+ "[&_ul[data-type='taskList']_li>label>input]:h-4",
31836
+ "[&_ul[data-type='taskList']_li>label>input]:rounded",
31837
+ "[&_ul[data-type='taskList']_li>label>input]:border-2",
31838
+ "[&_ul[data-type='taskList']_li>label>input]:border-primary/50",
31839
+ "[&_ul[data-type='taskList']_li>label>input]:accent-primary",
31840
+ "[&_pre]:bg-muted/40!",
31841
+ "[&_pre]:text-foreground!",
31842
+ "[&_pre]:border!",
31843
+ "[&_pre]:border-border/60!",
31844
+ "[&_pre_code]:bg-transparent!",
31845
+ "[&_.tableWrapper]:overflow-x-auto",
31846
+ "[&_.tableWrapper]:pb-1.5",
31847
+ "[&_.tableWrapper]:select-text",
31848
+ "[&_.tableWrapper]:[scrollbar-width:thin]",
31849
+ "[&_.tableWrapper]:[scrollbar-color:hsl(var(--border))_transparent]",
31850
+ "[&_.tableWrapper::-webkit-scrollbar]:h-2",
31851
+ "[&_.tableWrapper::-webkit-scrollbar]:w-2",
31852
+ "[&_.tableWrapper::-webkit-scrollbar-track]:rounded-full",
31853
+ "[&_.tableWrapper::-webkit-scrollbar-track]:bg-transparent",
31854
+ "[&_.tableWrapper::-webkit-scrollbar-thumb]:rounded-full",
31855
+ "[&_.tableWrapper::-webkit-scrollbar-thumb]:border",
31856
+ "[&_.tableWrapper::-webkit-scrollbar-thumb]:border-solid",
31857
+ "[&_.tableWrapper::-webkit-scrollbar-thumb]:border-transparent",
31858
+ "[&_.tableWrapper::-webkit-scrollbar-thumb]:bg-border/70",
31859
+ "[&_.tableWrapper::-webkit-scrollbar-thumb:hover]:bg-muted-foreground/45",
31860
+ "[&_table]:table-fixed",
31861
+ "[&_table]:overflow-hidden",
31862
+ "[&_table]:select-text",
31863
+ "[&_table[data-table-align]]:w-max",
31864
+ "[&_table[data-table-align]]:max-w-full",
31865
+ "[&_table[data-table-align='center']]:mx-auto",
31866
+ "[&_table[data-table-align='right']]:ml-auto",
31867
+ "[&_table[data-table-align='right']]:mr-0",
31868
+ "[&_td]:relative",
31869
+ "[&_td]:align-top",
31870
+ "[&_td]:box-border",
31871
+ "[&_td]:select-text",
31872
+ "[&_th]:relative",
31873
+ "[&_th]:align-top",
31874
+ "[&_th]:box-border",
31875
+ "[&_th]:select-text",
31876
+ "[&_.selectedCell]:after:content-['']",
31877
+ "[&_.selectedCell]:after:absolute",
31878
+ "[&_.selectedCell]:after:inset-0",
31879
+ "[&_.selectedCell]:after:z-[2]",
31880
+ "[&_.selectedCell]:after:bg-primary/15",
31881
+ "[&_.selectedCell]:after:pointer-events-none",
31882
+ "[&_.column-resize-handle]:pointer-events-auto",
31883
+ "[&_.column-resize-handle]:cursor-col-resize",
31884
+ "[&_.column-resize-handle]:absolute",
31885
+ "[&_.column-resize-handle]:top-[-1px]",
31886
+ "[&_.column-resize-handle]:bottom-[-1px]",
31887
+ "[&_.column-resize-handle]:right-[-5px]",
31888
+ "[&_.column-resize-handle]:z-10",
31889
+ "[&_.column-resize-handle]:w-2.5",
31890
+ "[&_.column-resize-handle]:bg-transparent",
31891
+ "[&_.column-resize-handle]:rounded-none",
31892
+ "[&_.column-resize-handle]:opacity-0",
31893
+ "[&_.column-resize-handle]:transition-opacity",
31894
+ "[&_.column-resize-handle]:after:absolute",
31895
+ "[&_.column-resize-handle]:after:top-0",
31896
+ "[&_.column-resize-handle]:after:bottom-0",
31897
+ "[&_.column-resize-handle]:after:left-1/2",
31898
+ "[&_.column-resize-handle]:after:w-0.5",
31899
+ "[&_.column-resize-handle]:after:-translate-x-1/2",
31900
+ "[&_.column-resize-handle]:after:rounded-full",
31901
+ "[&_.column-resize-handle]:after:bg-primary/75",
31902
+ "[&_.column-resize-handle]:after:content-['']",
31903
+ "[&.resize-cursor_.column-resize-handle]:opacity-100",
31904
+ "[&.resize-cursor_.column-resize-handle]:after:bg-primary",
31905
+ "[&.resize-cursor]:cursor-col-resize",
31906
+ "[&.resize-row-cursor]:cursor-row-resize",
31907
+ "[&_img.ProseMirror-selectednode]:ring-2",
31908
+ "[&_img.ProseMirror-selectednode]:ring-primary/60",
31909
+ "[&_img.ProseMirror-selectednode]:ring-offset-2",
31910
+ "[&_img.ProseMirror-selectednode]:ring-offset-background",
31911
+ "[&_hr]:border-t-2",
31912
+ "[&_hr]:border-primary/30",
31913
+ "[&_hr]:my-8",
31914
+ "[&_h1]:text-3xl",
31915
+ "[&_h1]:font-bold",
31916
+ "[&_h1]:mt-6",
31917
+ "[&_h1]:mb-4",
31918
+ "[&_h1]:text-foreground",
31919
+ "[&_h2]:text-2xl",
31920
+ "[&_h2]:font-semibold",
31921
+ "[&_h2]:mt-5",
31922
+ "[&_h2]:mb-3",
31923
+ "[&_h2]:text-foreground",
31924
+ "[&_h3]:text-xl",
31925
+ "[&_h3]:font-semibold",
31926
+ "[&_h3]:mt-4",
31927
+ "[&_h3]:mb-2",
31928
+ "[&_h3]:text-foreground",
31929
+ "[&_ul:not([data-type='taskList'])]:list-disc",
31930
+ "[&_ul:not([data-type='taskList'])]:pl-6",
31931
+ "[&_ul:not([data-type='taskList'])]:my-3",
31932
+ "[&_ol]:list-decimal",
31933
+ "[&_ol]:pl-6",
31934
+ "[&_ol]:my-3",
31935
+ "[&_li]:my-1",
31936
+ "[&_li]:pl-1",
31937
+ "[&_li_p]:my-0",
31938
+ "[&_blockquote]:border-l-4",
31939
+ "[&_blockquote]:border-primary",
31940
+ "[&_blockquote]:pl-4",
31941
+ "[&_blockquote]:py-2",
31942
+ "[&_blockquote]:my-4",
31943
+ "[&_blockquote]:bg-muted/30",
31944
+ "[&_blockquote]:rounded-r-lg",
31945
+ "[&_blockquote]:italic",
31946
+ "[&_blockquote]:text-muted-foreground",
31947
+ "[&_blockquote_p]:my-0",
31948
+ "[&_[data-image-layout='left']+p]:mt-1",
31949
+ "[&_[data-image-layout='left']+p]:min-h-[5rem]",
31950
+ "[&_[data-image-layout='right']+p]:mt-1",
31951
+ "[&_[data-image-layout='right']+p]:min-h-[5rem]",
31952
+ "max-md:[&_[data-image-layout='left']]:float-none",
31953
+ "max-md:[&_[data-image-layout='left']]:mr-0",
31954
+ "max-md:[&_[data-image-layout='left']]:ml-0",
31955
+ "max-md:[&_[data-image-layout='left']]:max-w-full",
31956
+ "max-md:[&_[data-image-layout='right']]:float-none",
31957
+ "max-md:[&_[data-image-layout='right']]:mr-0",
31958
+ "max-md:[&_[data-image-layout='right']]:ml-0",
31959
+ "max-md:[&_[data-image-layout='right']]:max-w-full",
31960
+ "max-md:[&_[data-image-layout='left']+p]:min-h-0",
31961
+ "max-md:[&_[data-image-layout='right']+p]:min-h-0"
31962
+ );
31963
+
31964
+ // src/components/UEditor/use-table-interactions.ts
31965
+ import React74, { useEffect as useEffect35, useRef as useRef31 } from "react";
31966
+ function useUEditorTableInteractions(editor) {
31789
31967
  const editorContentRef = useRef31(null);
31790
31968
  const tableColumnGuideRef = useRef31(null);
31791
31969
  const tableRowGuideRef = useRef31(null);
31792
31970
  const activeTableCellHighlightRef = useRef31(null);
31793
31971
  const hoveredTableCellRef = useRef31(null);
31794
31972
  const activeTableCellRef = useRef31(null);
31973
+ const tableLayoutSyncFrameRef = useRef31(null);
31974
+ const rowResizeCommitFrameRef = useRef31(null);
31795
31975
  const rowResizeStateRef = useRef31(null);
31796
31976
  const setEditorResizeCursor = React74.useCallback((cursor) => {
31797
31977
  const proseMirror = editorContentRef.current?.querySelector(".ProseMirror");
@@ -31833,6 +32013,14 @@ var UEditor = React74.forwardRef(({
31833
32013
  highlight.style.width = `${metrics.width}px`;
31834
32014
  highlight.style.height = `${metrics.height}px`;
31835
32015
  }, []);
32016
+ const scheduleTableLayoutSync = React74.useCallback(() => {
32017
+ if (tableLayoutSyncFrameRef.current !== null) return;
32018
+ tableLayoutSyncFrameRef.current = window.requestAnimationFrame(() => {
32019
+ tableLayoutSyncFrameRef.current = null;
32020
+ updateActiveCellHighlight(activeTableCellRef.current);
32021
+ editorContentRef.current?.dispatchEvent(new CustomEvent(UEDITOR_TABLE_LAYOUT_CHANGE_EVENT));
32022
+ });
32023
+ }, [updateActiveCellHighlight]);
31836
32024
  const setActiveTableCell = React74.useCallback((cell) => {
31837
32025
  if (activeTableCellRef.current === cell) return;
31838
32026
  activeTableCellRef.current = cell;
@@ -31874,228 +32062,49 @@ var UEditor = React74.forwardRef(({
31874
32062
  surface.classList.add("resize-row-cursor");
31875
32063
  setEditorResizeCursor("row-resize");
31876
32064
  }, [setEditorResizeCursor]);
31877
- const extensions = useMemo24(
31878
- () => buildUEditorExtensions({ placeholder: effectivePlaceholder, translate: t, maxCharacters, uploadImage, imageInsertMode, editable }),
31879
- [effectivePlaceholder, t, maxCharacters, uploadImage, imageInsertMode, editable]
31880
- );
31881
- const editor = useEditor({
31882
- immediatelyRender: false,
31883
- extensions,
31884
- content,
31885
- editable,
31886
- autofocus,
31887
- editorProps: {
31888
- handleDOMEvents: {
31889
- keydown: (_view, event) => {
31890
- if (!(event instanceof KeyboardEvent)) return false;
31891
- if (event.key === "ArrowLeft" || event.key === "ArrowRight" || event.key === "ArrowUp" || event.key === "ArrowDown") {
31892
- event.stopPropagation();
31893
- }
31894
- return false;
31895
- },
31896
- click: (view, event) => {
31897
- if (!(event instanceof MouseEvent)) return false;
31898
- if (event.button !== 0) return false;
31899
- const target = resolveEventElement(event.target);
31900
- const anchor = target?.closest?.("a[href]");
31901
- const href = anchor?.getAttribute("href") ?? "";
31902
- if (!href) return false;
31903
- if (!view.state.selection.empty) return false;
31904
- event.preventDefault();
31905
- event.stopPropagation();
31906
- window.open(href, "_blank", "noopener,noreferrer");
31907
- return true;
31908
- }
31909
- },
31910
- attributes: {
31911
- class: cn(
31912
- "prose prose-sm sm:prose dark:prose-invert max-w-none",
31913
- "focus:outline-none",
31914
- "px-4 py-4",
31915
- "[&_.is-editor-empty]:before:content-[attr(data-placeholder)]",
31916
- "[&_.is-editor-empty]:before:text-muted-foreground/50",
31917
- "[&_.is-editor-empty]:before:float-left",
31918
- "[&_.is-editor-empty]:before:pointer-events-none",
31919
- "[&_.is-editor-empty]:before:h-0",
31920
- "[&_ul[data-type='taskList']]:list-none",
31921
- "[&_ul[data-type='taskList']]:pl-0",
31922
- "[&_ul[data-type='taskList']_li]:flex",
31923
- "[&_ul[data-type='taskList']_li]:items-start",
31924
- "[&_ul[data-type='taskList']_li]:gap-2",
31925
- "[&_ul[data-type='taskList']_li>label]:mt-0.5",
31926
- "[&_ul[data-type='taskList']_li>label>input]:w-4",
31927
- "[&_ul[data-type='taskList']_li>label>input]:h-4",
31928
- "[&_ul[data-type='taskList']_li>label>input]:rounded",
31929
- "[&_ul[data-type='taskList']_li>label>input]:border-2",
31930
- "[&_ul[data-type='taskList']_li>label>input]:border-primary/50",
31931
- "[&_ul[data-type='taskList']_li>label>input]:accent-primary",
31932
- "[&_pre]:bg-muted/40!",
31933
- "[&_pre]:text-foreground!",
31934
- "[&_pre]:border!",
31935
- "[&_pre]:border-border/60!",
31936
- "[&_pre_code]:bg-transparent!",
31937
- "[&_.tableWrapper]:overflow-x-auto",
31938
- "[&_.tableWrapper]:pb-1.5",
31939
- "[&_.tableWrapper]:select-text",
31940
- "[&_.tableWrapper]:[scrollbar-width:thin]",
31941
- "[&_.tableWrapper]:[scrollbar-color:hsl(var(--border))_transparent]",
31942
- "[&_.tableWrapper::-webkit-scrollbar]:h-2",
31943
- "[&_.tableWrapper::-webkit-scrollbar]:w-2",
31944
- "[&_.tableWrapper::-webkit-scrollbar-track]:rounded-full",
31945
- "[&_.tableWrapper::-webkit-scrollbar-track]:bg-transparent",
31946
- "[&_.tableWrapper::-webkit-scrollbar-thumb]:rounded-full",
31947
- "[&_.tableWrapper::-webkit-scrollbar-thumb]:border",
31948
- "[&_.tableWrapper::-webkit-scrollbar-thumb]:border-solid",
31949
- "[&_.tableWrapper::-webkit-scrollbar-thumb]:border-transparent",
31950
- "[&_.tableWrapper::-webkit-scrollbar-thumb]:bg-border/70",
31951
- "[&_.tableWrapper::-webkit-scrollbar-thumb:hover]:bg-muted-foreground/45",
31952
- "[&_table]:table-fixed",
31953
- "[&_table]:overflow-hidden",
31954
- "[&_table]:select-text",
31955
- "[&_table[data-table-align]]:w-max",
31956
- "[&_table[data-table-align]]:max-w-full",
31957
- "[&_table[data-table-align='center']]:mx-auto",
31958
- "[&_table[data-table-align='right']]:ml-auto",
31959
- "[&_table[data-table-align='right']]:mr-0",
31960
- "[&_td]:relative",
31961
- "[&_td]:align-top",
31962
- "[&_td]:box-border",
31963
- "[&_td]:select-text",
31964
- "[&_th]:relative",
31965
- "[&_th]:align-top",
31966
- "[&_th]:box-border",
31967
- "[&_th]:select-text",
31968
- "[&_.selectedCell]:after:content-['']",
31969
- "[&_.selectedCell]:after:absolute",
31970
- "[&_.selectedCell]:after:inset-0",
31971
- "[&_.selectedCell]:after:z-[2]",
31972
- "[&_.selectedCell]:after:bg-primary/15",
31973
- "[&_.selectedCell]:after:pointer-events-none",
31974
- "[&_.column-resize-handle]:pointer-events-auto",
31975
- "[&_.column-resize-handle]:cursor-col-resize",
31976
- "[&_.column-resize-handle]:absolute",
31977
- "[&_.column-resize-handle]:top-[-1px]",
31978
- "[&_.column-resize-handle]:bottom-[-1px]",
31979
- "[&_.column-resize-handle]:right-[-5px]",
31980
- "[&_.column-resize-handle]:z-10",
31981
- "[&_.column-resize-handle]:w-2.5",
31982
- "[&_.column-resize-handle]:bg-transparent",
31983
- "[&_.column-resize-handle]:rounded-none",
31984
- "[&_.column-resize-handle]:opacity-0",
31985
- "[&_.column-resize-handle]:transition-opacity",
31986
- "[&_.column-resize-handle]:after:absolute",
31987
- "[&_.column-resize-handle]:after:top-0",
31988
- "[&_.column-resize-handle]:after:bottom-0",
31989
- "[&_.column-resize-handle]:after:left-1/2",
31990
- "[&_.column-resize-handle]:after:w-0.5",
31991
- "[&_.column-resize-handle]:after:-translate-x-1/2",
31992
- "[&_.column-resize-handle]:after:rounded-full",
31993
- "[&_.column-resize-handle]:after:bg-primary/75",
31994
- "[&_.column-resize-handle]:after:content-['']",
31995
- "[&.resize-cursor_.column-resize-handle]:opacity-100",
31996
- "[&.resize-cursor_.column-resize-handle]:after:bg-primary",
31997
- "[&.resize-cursor]:cursor-col-resize",
31998
- "[&.resize-row-cursor]:cursor-row-resize",
31999
- "[&_img.ProseMirror-selectednode]:ring-2",
32000
- "[&_img.ProseMirror-selectednode]:ring-primary/60",
32001
- "[&_img.ProseMirror-selectednode]:ring-offset-2",
32002
- "[&_img.ProseMirror-selectednode]:ring-offset-background",
32003
- "[&_hr]:border-t-2",
32004
- "[&_hr]:border-primary/30",
32005
- "[&_hr]:my-8",
32006
- "[&_h1]:text-3xl",
32007
- "[&_h1]:font-bold",
32008
- "[&_h1]:mt-6",
32009
- "[&_h1]:mb-4",
32010
- "[&_h1]:text-foreground",
32011
- "[&_h2]:text-2xl",
32012
- "[&_h2]:font-semibold",
32013
- "[&_h2]:mt-5",
32014
- "[&_h2]:mb-3",
32015
- "[&_h2]:text-foreground",
32016
- "[&_h3]:text-xl",
32017
- "[&_h3]:font-semibold",
32018
- "[&_h3]:mt-4",
32019
- "[&_h3]:mb-2",
32020
- "[&_h3]:text-foreground",
32021
- "[&_ul:not([data-type='taskList'])]:list-disc",
32022
- "[&_ul:not([data-type='taskList'])]:pl-6",
32023
- "[&_ul:not([data-type='taskList'])]:my-3",
32024
- "[&_ol]:list-decimal",
32025
- "[&_ol]:pl-6",
32026
- "[&_ol]:my-3",
32027
- "[&_li]:my-1",
32028
- "[&_li]:pl-1",
32029
- "[&_li_p]:my-0",
32030
- "[&_blockquote]:border-l-4",
32031
- "[&_blockquote]:border-primary",
32032
- "[&_blockquote]:pl-4",
32033
- "[&_blockquote]:py-2",
32034
- "[&_blockquote]:my-4",
32035
- "[&_blockquote]:bg-muted/30",
32036
- "[&_blockquote]:rounded-r-lg",
32037
- "[&_blockquote]:italic",
32038
- "[&_blockquote]:text-muted-foreground",
32039
- "[&_blockquote_p]:my-0",
32040
- "[&_[data-image-layout='left']+p]:mt-1",
32041
- "[&_[data-image-layout='left']+p]:min-h-[5rem]",
32042
- "[&_[data-image-layout='right']+p]:mt-1",
32043
- "[&_[data-image-layout='right']+p]:min-h-[5rem]",
32044
- "max-md:[&_[data-image-layout='left']]:float-none",
32045
- "max-md:[&_[data-image-layout='left']]:mr-0",
32046
- "max-md:[&_[data-image-layout='left']]:ml-0",
32047
- "max-md:[&_[data-image-layout='left']]:max-w-full",
32048
- "max-md:[&_[data-image-layout='right']]:float-none",
32049
- "max-md:[&_[data-image-layout='right']]:mr-0",
32050
- "max-md:[&_[data-image-layout='right']]:ml-0",
32051
- "max-md:[&_[data-image-layout='right']]:max-w-full",
32052
- "max-md:[&_[data-image-layout='left']+p]:min-h-0",
32053
- "max-md:[&_[data-image-layout='right']+p]:min-h-0"
32054
- )
32055
- }
32056
- },
32057
- onUpdate: ({ editor: editor2 }) => {
32058
- const html = editor2.getHTML();
32059
- onChange?.(html);
32060
- onHtmlChange?.(html);
32061
- onJsonChange?.(editor2.getJSON());
32065
+ const commitRowResizePreview = React74.useCallback(() => {
32066
+ if (!editor) return;
32067
+ const state = rowResizeStateRef.current;
32068
+ if (!state) return;
32069
+ const nextHeight = state.pendingHeight;
32070
+ if (nextHeight === state.previewHeight) {
32071
+ document.body.style.cursor = "row-resize";
32072
+ showRowGuide(state.tableElement, state.rowElement, state.cellElement);
32073
+ scheduleTableLayoutSync();
32074
+ return;
32062
32075
  }
32063
- });
32076
+ state.previewHeight = nextHeight;
32077
+ const tr = editor.view.state.tr;
32078
+ tr.setNodeMarkup(state.rowPos, void 0, {
32079
+ ...state.rowNode.attrs,
32080
+ rowHeight: nextHeight
32081
+ });
32082
+ tr.setMeta("addToHistory", false);
32083
+ editor.view.dispatch(tr);
32084
+ state.rowNode = editor.view.state.doc.nodeAt(state.rowPos) ?? state.rowNode;
32085
+ const refreshedRow = state.tableElement.rows.item(state.rowElement.rowIndex);
32086
+ if (refreshedRow instanceof HTMLTableRowElement) {
32087
+ state.rowElement = refreshedRow;
32088
+ const refreshedCell = refreshedRow.cells.item(state.cellIndex);
32089
+ if (refreshedCell instanceof HTMLTableCellElement) {
32090
+ state.cellElement = refreshedCell;
32091
+ }
32092
+ }
32093
+ document.body.style.cursor = "row-resize";
32094
+ showRowGuide(state.tableElement, state.rowElement, state.cellElement);
32095
+ scheduleTableLayoutSync();
32096
+ }, [editor, scheduleTableLayoutSync, showRowGuide]);
32097
+ const scheduleRowResizeCommit = React74.useCallback(() => {
32098
+ if (rowResizeCommitFrameRef.current !== null) return;
32099
+ rowResizeCommitFrameRef.current = window.requestAnimationFrame(() => {
32100
+ rowResizeCommitFrameRef.current = null;
32101
+ commitRowResizePreview();
32102
+ });
32103
+ }, [commitRowResizePreview]);
32064
32104
  const syncActiveTableCellFromSelection = React74.useCallback(() => {
32065
32105
  if (!editor) return;
32066
32106
  setActiveTableCell(getSelectionTableCell(editor.view));
32067
32107
  }, [editor, setActiveTableCell]);
32068
- useImperativeHandle3(
32069
- ref,
32070
- () => ({
32071
- prepareContentForSave: async ({ throwOnError = false } = {}) => {
32072
- if (!inFlightPrepareRef.current) {
32073
- const htmlSnapshot = editor?.getHTML() ?? content ?? "";
32074
- inFlightPrepareRef.current = prepareUEditorContentForSave({
32075
- html: htmlSnapshot,
32076
- uploadImageForSave
32077
- }).finally(() => {
32078
- inFlightPrepareRef.current = null;
32079
- });
32080
- }
32081
- const result = await inFlightPrepareRef.current;
32082
- if (throwOnError && result.errors.length > 0) {
32083
- throw new UEditorPrepareContentForSaveError(result);
32084
- }
32085
- return result;
32086
- }
32087
- }),
32088
- [content, editor, uploadImageForSave]
32089
- );
32090
- useEffect35(() => {
32091
- if (!editor) return;
32092
- const nextContent = content ?? "";
32093
- if (lastAppliedContentRef.current === nextContent) return;
32094
- lastAppliedContentRef.current = nextContent;
32095
- if (editor.getHTML() !== nextContent) {
32096
- editor.commands.setContent(nextContent, { emitUpdate: false });
32097
- }
32098
- }, [content, editor]);
32099
32108
  useEffect35(() => {
32100
32109
  if (!editor) return void 0;
32101
32110
  const proseMirror = editor.view.dom;
@@ -32185,6 +32194,7 @@ var UEditor = React74.forwardRef(({
32185
32194
  setHoveredTableCell(cell);
32186
32195
  const rowInfo = findTableRowNodeInfo(editor.view, row);
32187
32196
  if (!rowInfo) return;
32197
+ const startHeight = row.getBoundingClientRect().height;
32188
32198
  rowResizeStateRef.current = {
32189
32199
  rowElement: row,
32190
32200
  tableElement: table,
@@ -32193,8 +32203,9 @@ var UEditor = React74.forwardRef(({
32193
32203
  rowPos: rowInfo.pos,
32194
32204
  rowNode: rowInfo.node,
32195
32205
  startY: event.clientY,
32196
- startHeight: row.getBoundingClientRect().height,
32197
- previewHeight: row.getBoundingClientRect().height
32206
+ startHeight,
32207
+ previewHeight: startHeight,
32208
+ pendingHeight: startHeight
32198
32209
  };
32199
32210
  showRowGuide(table, row, cell);
32200
32211
  document.body.style.cursor = "row-resize";
@@ -32208,30 +32219,14 @@ var UEditor = React74.forwardRef(({
32208
32219
  MIN_TABLE_ROW_HEIGHT,
32209
32220
  Math.round(state.startHeight + (event.clientY - state.startY))
32210
32221
  );
32211
- if (nextHeight === state.previewHeight) {
32222
+ if (nextHeight === state.pendingHeight) {
32212
32223
  document.body.style.cursor = "row-resize";
32213
32224
  showRowGuide(state.tableElement, state.rowElement, state.cellElement);
32214
32225
  return;
32215
32226
  }
32216
- state.previewHeight = nextHeight;
32217
- applyPreviewRowHeight(state.rowElement, nextHeight);
32218
- const tr = editor.view.state.tr;
32219
- tr.setNodeMarkup(state.rowPos, void 0, {
32220
- ...state.rowNode.attrs,
32221
- rowHeight: nextHeight
32222
- });
32223
- editor.view.dispatch(tr);
32224
- state.rowNode = editor.view.state.doc.nodeAt(state.rowPos) ?? state.rowNode;
32225
- const refreshedRow = state.tableElement.rows.item(state.rowElement.rowIndex);
32226
- if (refreshedRow instanceof HTMLTableRowElement) {
32227
- state.rowElement = refreshedRow;
32228
- const refreshedCell = refreshedRow.cells.item(state.cellIndex);
32229
- if (refreshedCell instanceof HTMLTableCellElement) {
32230
- state.cellElement = refreshedCell;
32231
- }
32232
- }
32227
+ state.pendingHeight = nextHeight;
32233
32228
  document.body.style.cursor = "row-resize";
32234
- showRowGuide(state.tableElement, state.rowElement, state.cellElement);
32229
+ scheduleRowResizeCommit();
32235
32230
  };
32236
32231
  const handlePointerUp = (event) => {
32237
32232
  const state = rowResizeStateRef.current;
@@ -32240,20 +32235,40 @@ var UEditor = React74.forwardRef(({
32240
32235
  MIN_TABLE_ROW_HEIGHT,
32241
32236
  Math.round(state.startHeight + (event.clientY - state.startY))
32242
32237
  );
32243
- clearPreviewRowHeight(state.rowElement);
32238
+ state.pendingHeight = nextHeight;
32239
+ if (rowResizeCommitFrameRef.current !== null) {
32240
+ window.cancelAnimationFrame(rowResizeCommitFrameRef.current);
32241
+ rowResizeCommitFrameRef.current = null;
32242
+ }
32243
+ commitRowResizePreview();
32244
+ const latestState = rowResizeStateRef.current ?? state;
32245
+ const rowNode = editor.view.state.doc.nodeAt(latestState.rowPos) ?? latestState.rowNode;
32246
+ if (rowNode.attrs.rowHeight !== nextHeight) {
32247
+ const tr = editor.view.state.tr;
32248
+ tr.setNodeMarkup(latestState.rowPos, void 0, {
32249
+ ...rowNode.attrs,
32250
+ rowHeight: nextHeight
32251
+ });
32252
+ editor.view.dispatch(tr);
32253
+ }
32244
32254
  rowResizeStateRef.current = null;
32245
32255
  document.body.style.cursor = "";
32246
32256
  clearHoveredTableCell();
32247
32257
  clearAllTableResizeHover();
32258
+ scheduleTableLayoutSync();
32248
32259
  };
32249
32260
  const handleWindowBlur = () => {
32250
32261
  const state = rowResizeStateRef.current;
32251
32262
  if (!state) return;
32252
- clearPreviewRowHeight(state.rowElement);
32263
+ if (rowResizeCommitFrameRef.current !== null) {
32264
+ window.cancelAnimationFrame(rowResizeCommitFrameRef.current);
32265
+ rowResizeCommitFrameRef.current = null;
32266
+ }
32253
32267
  rowResizeStateRef.current = null;
32254
32268
  document.body.style.cursor = "";
32255
32269
  clearHoveredTableCell();
32256
32270
  clearAllTableResizeHover();
32271
+ scheduleTableLayoutSync();
32257
32272
  };
32258
32273
  proseMirror.addEventListener("mousemove", handleEditorMouseMove);
32259
32274
  proseMirror.addEventListener("mouseleave", handleEditorMouseLeave);
@@ -32292,13 +32307,156 @@ var UEditor = React74.forwardRef(({
32292
32307
  editor.off("selectionUpdate", syncActiveTableCellFromSelection);
32293
32308
  editor.off("focus", syncActiveTableCellFromSelection);
32294
32309
  window.clearTimeout(selectionSyncTimeoutId);
32310
+ if (tableLayoutSyncFrameRef.current !== null) {
32311
+ window.cancelAnimationFrame(tableLayoutSyncFrameRef.current);
32312
+ tableLayoutSyncFrameRef.current = null;
32313
+ }
32314
+ if (rowResizeCommitFrameRef.current !== null) {
32315
+ window.cancelAnimationFrame(rowResizeCommitFrameRef.current);
32316
+ rowResizeCommitFrameRef.current = null;
32317
+ }
32295
32318
  document.body.style.cursor = "";
32296
32319
  clearActiveTableCell();
32297
32320
  clearHoveredTableCell();
32298
32321
  clearAllTableResizeHover();
32299
32322
  rowResizeStateRef.current = null;
32300
32323
  };
32301
- }, [clearActiveTableCell, clearAllTableResizeHover, clearHoveredTableCell, editor, hideColumnGuide, hideRowGuide, setHoveredTableCell, showColumnGuide, showRowGuide, syncActiveTableCellFromSelection, updateActiveCellHighlight]);
32324
+ }, [clearActiveTableCell, clearAllTableResizeHover, clearHoveredTableCell, commitRowResizePreview, editor, hideColumnGuide, hideRowGuide, scheduleRowResizeCommit, scheduleTableLayoutSync, setHoveredTableCell, showColumnGuide, showRowGuide, syncActiveTableCellFromSelection, updateActiveCellHighlight]);
32325
+ return {
32326
+ editorContentRef,
32327
+ tableColumnGuideRef,
32328
+ tableRowGuideRef,
32329
+ activeTableCellHighlightRef
32330
+ };
32331
+ }
32332
+
32333
+ // src/components/UEditor/UEditor.tsx
32334
+ import { jsx as jsx83, jsxs as jsxs71 } from "react/jsx-runtime";
32335
+ var UEditor = React75.forwardRef(({
32336
+ content = "",
32337
+ onChange,
32338
+ onHtmlChange,
32339
+ onJsonChange,
32340
+ uploadImage,
32341
+ uploadImageForSave,
32342
+ uploadImageConcurrency = 3,
32343
+ imageInsertMode = "base64",
32344
+ maxImageFileSize,
32345
+ allowedImageMimeTypes,
32346
+ fallbackToDataUrl,
32347
+ placeholder,
32348
+ className,
32349
+ editable = true,
32350
+ autofocus = false,
32351
+ showToolbar = true,
32352
+ showBubbleMenu = true,
32353
+ showFloatingMenu = false,
32354
+ showCharacterCount = true,
32355
+ maxCharacters,
32356
+ minHeight = "200px",
32357
+ maxHeight = "auto",
32358
+ variant = "default",
32359
+ fontFamilies,
32360
+ fontSizes,
32361
+ lineHeights,
32362
+ letterSpacings
32363
+ }, ref) => {
32364
+ const t = useSmartTranslations("UEditor");
32365
+ const effectivePlaceholder = placeholder ?? t("placeholder");
32366
+ const inFlightPrepareRef = useRef32(null);
32367
+ const lastAppliedContentRef = useRef32(content ?? "");
32368
+ const extensions = useMemo24(
32369
+ () => buildUEditorExtensions({
32370
+ placeholder: effectivePlaceholder,
32371
+ translate: t,
32372
+ maxCharacters,
32373
+ uploadImage,
32374
+ imageInsertMode,
32375
+ maxImageFileSize,
32376
+ allowedImageMimeTypes,
32377
+ fallbackToDataUrl,
32378
+ editable
32379
+ }),
32380
+ [effectivePlaceholder, t, maxCharacters, uploadImage, imageInsertMode, maxImageFileSize, allowedImageMimeTypes, fallbackToDataUrl, editable]
32381
+ );
32382
+ const editor = useEditor({
32383
+ immediatelyRender: false,
32384
+ extensions,
32385
+ content,
32386
+ editable,
32387
+ autofocus,
32388
+ editorProps: {
32389
+ handleDOMEvents: {
32390
+ keydown: (_view, event) => {
32391
+ if (!(event instanceof KeyboardEvent)) return false;
32392
+ if (event.key === "ArrowLeft" || event.key === "ArrowRight" || event.key === "ArrowUp" || event.key === "ArrowDown") {
32393
+ event.stopPropagation();
32394
+ }
32395
+ return false;
32396
+ },
32397
+ click: (view, event) => {
32398
+ if (!(event instanceof MouseEvent)) return false;
32399
+ if (event.button !== 0) return false;
32400
+ const target = resolveEventElement(event.target);
32401
+ const anchor = target?.closest?.("a[href]");
32402
+ const href = anchor?.getAttribute("href") ?? "";
32403
+ if (!href) return false;
32404
+ if (!view.state.selection.empty) return false;
32405
+ event.preventDefault();
32406
+ event.stopPropagation();
32407
+ window.open(href, "_blank", "noopener,noreferrer");
32408
+ return true;
32409
+ }
32410
+ },
32411
+ attributes: {
32412
+ class: UEDITOR_PROSEMIRROR_CLASS_NAME
32413
+ }
32414
+ },
32415
+ onUpdate: ({ editor: editor2 }) => {
32416
+ const html = editor2.getHTML();
32417
+ onChange?.(html);
32418
+ onHtmlChange?.(html);
32419
+ onJsonChange?.(editor2.getJSON());
32420
+ }
32421
+ });
32422
+ const {
32423
+ editorContentRef,
32424
+ tableColumnGuideRef,
32425
+ tableRowGuideRef,
32426
+ activeTableCellHighlightRef
32427
+ } = useUEditorTableInteractions(editor);
32428
+ useImperativeHandle3(
32429
+ ref,
32430
+ () => ({
32431
+ prepareContentForSave: async ({ throwOnError = false } = {}) => {
32432
+ if (!inFlightPrepareRef.current) {
32433
+ const htmlSnapshot = editor?.getHTML() ?? content ?? "";
32434
+ inFlightPrepareRef.current = prepareUEditorContentForSave({
32435
+ html: htmlSnapshot,
32436
+ uploadImageForSave,
32437
+ uploadConcurrency: uploadImageConcurrency
32438
+ }).finally(() => {
32439
+ inFlightPrepareRef.current = null;
32440
+ });
32441
+ }
32442
+ const result = await inFlightPrepareRef.current;
32443
+ if (throwOnError && result.errors.length > 0) {
32444
+ throw new UEditorPrepareContentForSaveError(result);
32445
+ }
32446
+ return result;
32447
+ }
32448
+ }),
32449
+ [content, editor, uploadImageForSave, uploadImageConcurrency]
32450
+ );
32451
+ useEffect36(() => {
32452
+ if (!editor) return;
32453
+ const nextContent = content ?? "";
32454
+ if (lastAppliedContentRef.current === nextContent) return;
32455
+ lastAppliedContentRef.current = nextContent;
32456
+ if (editor.getHTML() !== nextContent) {
32457
+ editor.commands.setContent(nextContent, { emitUpdate: false });
32458
+ }
32459
+ }, [content, editor]);
32302
32460
  if (!editor) {
32303
32461
  return /* @__PURE__ */ jsx83(
32304
32462
  "div",
@@ -32328,6 +32486,8 @@ var UEditor = React74.forwardRef(({
32328
32486
  variant,
32329
32487
  uploadImage,
32330
32488
  imageInsertMode,
32489
+ maxImageFileSize,
32490
+ allowedImageMimeTypes,
32331
32491
  fontFamilies,
32332
32492
  fontSizes,
32333
32493
  lineHeights,