smartrte-react 0.2.6 → 0.2.8

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.
@@ -50,6 +50,11 @@ type ClassicEditorProps = {
50
50
  * - "dark": Uses the built-in dark theme.
51
51
  */
52
52
  theme?: SrteTheme;
53
+ /**
54
+ * Show the inline font-size dropdown in the toolbar.
55
+ * Defaults to true. Heading block controls are independent of this option.
56
+ */
57
+ showFontSize?: boolean;
53
58
  /**
54
59
  * Additional CSS class name(s) to apply to the editor's root element.
55
60
  * Useful for custom theming by overriding CSS custom properties.
@@ -57,5 +62,5 @@ type ClassicEditorProps = {
57
62
  */
58
63
  className?: string;
59
64
  };
60
- export declare function ClassicEditor({ value, onChange, placeholder, minHeight, maxHeight, readOnly, table, media, formula, mediaManager, fonts, defaultFont, preserveFontFamily, preserveColors, preserveDocxStyles, theme, className, }: ClassicEditorProps): import("react/jsx-runtime").JSX.Element;
65
+ export declare function ClassicEditor({ value, onChange, placeholder, minHeight, maxHeight, readOnly, table, media, formula, mediaManager, fonts, defaultFont, preserveFontFamily, preserveColors, preserveDocxStyles, theme, showFontSize, className, }: ClassicEditorProps): import("react/jsx-runtime").JSX.Element;
61
66
  export {};
@@ -17,7 +17,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
17
17
  { name: "Times New Roman", value: "'Times New Roman', Times, serif" },
18
18
  { name: "Verdana", value: "Verdana, Geneva, sans-serif" },
19
19
  { name: "Courier New", value: "'Courier New', Courier, monospace" },
20
- ], defaultFont, preserveFontFamily = false, preserveColors = false, preserveDocxStyles = true, theme = "light", className, }) {
20
+ ], defaultFont, preserveFontFamily = false, preserveColors = false, preserveDocxStyles = true, theme = "light", showFontSize = true, className, }) {
21
21
  ensureStyleSheet();
22
22
  const editableRef = useRef(null);
23
23
  const editorScrollRef = useRef(null);
@@ -37,6 +37,9 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
37
37
  const [imageOverlay, setImageOverlay] = useState(null);
38
38
  const resizingRef = useRef(null);
39
39
  const draggedImageRef = useRef(null);
40
+ const draggedBlockRef = useRef(null);
41
+ const dragHandleHideTimerRef = useRef(null);
42
+ const [dragHandle, setDragHandle] = useState(null);
40
43
  const tableResizeRef = useRef(null);
41
44
  const [showTableDialog, setShowTableDialog] = useState(false);
42
45
  const [tableRows, setTableRows] = useState(3);
@@ -58,6 +61,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
58
61
  });
59
62
  const [currentFontSize, setCurrentFontSize] = useState("");
60
63
  const [currentFont, setCurrentFont] = useState("");
64
+ const [currentBlockType, setCurrentBlockType] = useState("p");
61
65
  const [activeState, setActiveState] = useState({
62
66
  bold: false,
63
67
  italic: false,
@@ -108,6 +112,9 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
108
112
  if (node.nodeType === Node.TEXT_NODE)
109
113
  node = node.parentNode;
110
114
  const element = node instanceof HTMLElement ? node : null;
115
+ const block = element?.closest("p,h1,h2,h3,h4,h5,h6,li,blockquote,pre,div");
116
+ const tag = block?.tagName.toLowerCase();
117
+ setCurrentBlockType(tag === "h1" || tag === "h2" || tag === "h3" ? tag : "p");
111
118
  setActiveState({
112
119
  bold: document.queryCommandState("bold"),
113
120
  italic: document.queryCommandState("italic"),
@@ -172,7 +179,22 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
172
179
  catch { }
173
180
  };
174
181
  const applyFormatBlock = (blockName) => {
175
- exec("formatBlock", blockName);
182
+ try {
183
+ if (!restoreSavedSelection()) {
184
+ safeSelectRange(getSelectionRangeInEditor());
185
+ }
186
+ if (applyFormatBlockFallback(blockName)) {
187
+ const tag = normalizeBlockTag(blockName);
188
+ if (tag === "p" || tag === "h1" || tag === "h2" || tag === "h3") {
189
+ setCurrentBlockType(tag);
190
+ }
191
+ handleInput();
192
+ requestAnimationFrame(updateActiveState);
193
+ return;
194
+ }
195
+ exec("formatBlock", blockName);
196
+ }
197
+ catch { }
176
198
  };
177
199
  const emitChange = () => {
178
200
  const el = editableRef.current;
@@ -386,19 +408,71 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
386
408
  cell.parentElement?.replaceChild(replacement, cell);
387
409
  return replacement;
388
410
  };
389
- const applyFormatBlockFallback = (blockName) => {
390
- const editor = editableRef.current;
391
- const block = getCurrentBlock();
411
+ const normalizeBlockTag = (blockName) => {
392
412
  const tag = blockName.replace(/[<>]/g, "").toLowerCase() || "p";
393
- if (!editor || !block || block === editor || !/^(p|h1|h2|h3|h4|h5|h6|pre|blockquote)$/.test(tag))
394
- return;
413
+ return /^(p|h1|h2|h3|h4|h5|h6|pre|blockquote)$/.test(tag) ? tag : null;
414
+ };
415
+ const sortInDocumentOrder = (elements) => {
416
+ return [...elements].sort((a, b) => {
417
+ if (a === b)
418
+ return 0;
419
+ const position = a.compareDocumentPosition(b);
420
+ return position & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
421
+ });
422
+ };
423
+ const replaceBlockTag = (block, tag) => {
424
+ if (block.tagName.toLowerCase() === tag)
425
+ return block;
395
426
  const replacement = document.createElement(tag);
396
427
  copyCellOrBlockStyles(block, replacement);
397
428
  block.parentElement?.replaceChild(replacement, block);
398
- const range = document.createRange();
399
- range.selectNodeContents(replacement);
400
- range.collapse(false);
401
- safeSelectRange(range);
429
+ return replacement;
430
+ };
431
+ const applyFormatBlockFallback = (blockName) => {
432
+ const editor = editableRef.current;
433
+ const range = getSelectionRangeInEditor();
434
+ const tag = normalizeBlockTag(blockName);
435
+ if (!editor || !range || !tag)
436
+ return false;
437
+ if (!editor.innerHTML.trim()) {
438
+ const block = document.createElement(tag);
439
+ block.innerHTML = "<br>";
440
+ editor.appendChild(block);
441
+ focusElementEnd(block);
442
+ return true;
443
+ }
444
+ if (range.collapsed) {
445
+ const block = getCurrentBlock();
446
+ if (!block || block === editor || block.closest("ul,ol") || !block.parentElement)
447
+ return false;
448
+ const replacement = replaceBlockTag(block, tag);
449
+ focusElementEnd(replacement);
450
+ return true;
451
+ }
452
+ const selectedBlocks = sortInDocumentOrder(getSelectedBlocks(range)).filter((block) => {
453
+ if (!editor.contains(block) || block === editor)
454
+ return false;
455
+ if (block.closest("ul,ol"))
456
+ return false;
457
+ return Boolean(block.parentElement);
458
+ });
459
+ if (selectedBlocks.length > 0) {
460
+ let lastReplacement = null;
461
+ selectedBlocks.forEach((block) => {
462
+ lastReplacement = replaceBlockTag(block, tag);
463
+ });
464
+ if (lastReplacement)
465
+ focusElementEnd(lastReplacement);
466
+ return true;
467
+ }
468
+ const block = document.createElement(tag);
469
+ const contents = range.extractContents();
470
+ block.appendChild(contents);
471
+ if (!block.innerHTML.trim())
472
+ block.innerHTML = "<br>";
473
+ range.insertNode(block);
474
+ focusElementEnd(block);
475
+ return true;
402
476
  };
403
477
  const cloneListShell = (list, tagName) => {
404
478
  const clone = document.createElement(tagName || list.tagName.toLowerCase());
@@ -609,7 +683,50 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
609
683
  };
610
684
  const toggleBlockquote = () => {
611
685
  try {
686
+ if (!restoreSavedSelection())
687
+ safeSelectRange(getSelectionRangeInEditor());
612
688
  const range = getSelectionRangeInEditor();
689
+ const editor = editableRef.current;
690
+ if (!editor || !range)
691
+ return;
692
+ const unwrapQuote = (quote) => {
693
+ const parent = quote.parentElement;
694
+ if (!parent)
695
+ return false;
696
+ const moved = [];
697
+ while (quote.firstChild) {
698
+ const child = quote.firstChild;
699
+ parent.insertBefore(child, quote);
700
+ if (child instanceof HTMLElement)
701
+ moved.push(child);
702
+ }
703
+ quote.remove();
704
+ focusElementEnd(moved[moved.length - 1] || parent);
705
+ return true;
706
+ };
707
+ const wrapBlocks = (blocks) => {
708
+ const selected = sortInDocumentOrder(blocks).filter((block) => {
709
+ if (!editor.contains(block) || block === editor)
710
+ return false;
711
+ if (block.closest("ul,ol"))
712
+ return false;
713
+ if (block.tagName.toLowerCase() === "blockquote")
714
+ return false;
715
+ return Boolean(block.parentElement);
716
+ });
717
+ if (selected.length === 0)
718
+ return false;
719
+ let lastWrapped = null;
720
+ selected.forEach((block) => {
721
+ const quote = document.createElement("blockquote");
722
+ block.parentElement?.insertBefore(quote, block);
723
+ quote.appendChild(block);
724
+ lastWrapped = block;
725
+ });
726
+ if (lastWrapped)
727
+ focusElementEnd(lastWrapped);
728
+ return true;
729
+ };
613
730
  if (!range)
614
731
  return;
615
732
  let node = range.commonAncestorContainer;
@@ -618,16 +735,36 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
618
735
  const element = node;
619
736
  const quote = element?.closest?.('blockquote');
620
737
  if (quote && editableRef.current?.contains(quote)) {
621
- const replacement = document.createElement('p');
622
- replacement.innerHTML = quote.innerHTML || '<br>';
623
- quote.parentElement?.replaceChild(replacement, quote);
624
- const nextRange = document.createRange();
625
- nextRange.selectNodeContents(replacement);
626
- nextRange.collapse(false);
627
- safeSelectRange(nextRange);
628
- handleInput();
629
- return;
738
+ pushEditorHistory();
739
+ if (unwrapQuote(quote)) {
740
+ handleInput();
741
+ requestAnimationFrame(updateActiveState);
742
+ return;
743
+ }
630
744
  }
745
+ if (range.collapsed) {
746
+ const block = getCurrentBlock();
747
+ if (block) {
748
+ pushEditorHistory();
749
+ if (!wrapBlocks([block]))
750
+ return;
751
+ handleInput();
752
+ requestAnimationFrame(updateActiveState);
753
+ return;
754
+ }
755
+ }
756
+ else {
757
+ const blocks = getSelectedBlocks(range);
758
+ if (blocks.length > 0) {
759
+ pushEditorHistory();
760
+ if (!wrapBlocks(blocks))
761
+ return;
762
+ handleInput();
763
+ requestAnimationFrame(updateActiveState);
764
+ return;
765
+ }
766
+ }
767
+ pushEditorHistory();
631
768
  exec("formatBlock", "<blockquote>");
632
769
  }
633
770
  catch { }
@@ -750,8 +887,158 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
750
887
  const applyTextColor = (color) => {
751
888
  exec("foreColor", color);
752
889
  };
890
+ const parseCssColor = (value) => {
891
+ const normalized = value.trim().toLowerCase();
892
+ if (!normalized || normalized === "transparent")
893
+ return null;
894
+ const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(normalized);
895
+ if (hex) {
896
+ const raw = hex[1].length === 3
897
+ ? hex[1].split("").map((part) => part + part).join("")
898
+ : hex[1];
899
+ return {
900
+ r: parseInt(raw.slice(0, 2), 16),
901
+ g: parseInt(raw.slice(2, 4), 16),
902
+ b: parseInt(raw.slice(4, 6), 16),
903
+ };
904
+ }
905
+ const rgb = /^rgba?\(([^)]+)\)$/.exec(normalized);
906
+ if (!rgb)
907
+ return null;
908
+ const parts = rgb[1].split(",").map((part) => Number(part.trim()));
909
+ if (parts.length < 3 || parts.some((part) => !Number.isFinite(part)))
910
+ return null;
911
+ if (parts.length >= 4 && parts[3] === 0)
912
+ return null;
913
+ return {
914
+ r: Math.max(0, Math.min(255, parts[0])),
915
+ g: Math.max(0, Math.min(255, parts[1])),
916
+ b: Math.max(0, Math.min(255, parts[2])),
917
+ };
918
+ };
919
+ const cssColorToHex = (value) => {
920
+ const color = parseCssColor(value);
921
+ if (!color)
922
+ return "";
923
+ const toHex = (part) => Math.round(part).toString(16).padStart(2, "0");
924
+ return `#${toHex(color.r)}${toHex(color.g)}${toHex(color.b)}`;
925
+ };
926
+ const getRangeStartElement = (range) => {
927
+ const editor = editableRef.current;
928
+ if (!editor || !range)
929
+ return null;
930
+ let node = range.startContainer;
931
+ if (node === editor && editor.childNodes[range.startOffset]) {
932
+ node = editor.childNodes[range.startOffset];
933
+ }
934
+ if (node.nodeType === Node.TEXT_NODE)
935
+ node = node.parentElement;
936
+ return node instanceof HTMLElement && editor.contains(node) ? node : null;
937
+ };
938
+ const getInlineBackgroundHex = (element) => {
939
+ const editor = editableRef.current;
940
+ let current = element;
941
+ while (current && current !== editor) {
942
+ const inlineHex = cssColorToHex(current.style.backgroundColor || current.style.background);
943
+ if (inlineHex)
944
+ return inlineHex;
945
+ current = current.parentElement;
946
+ }
947
+ return "";
948
+ };
949
+ const currentPickerColorHex = () => {
950
+ if (typeof window === "undefined")
951
+ return colorPickerType === "text" ? "#000000" : "#ffffff";
952
+ const editor = editableRef.current;
953
+ if (!editor)
954
+ return colorPickerType === "text" ? "#000000" : "#ffffff";
955
+ const element = getRangeStartElement(getSelectionRangeInEditor());
956
+ if (colorPickerType === "background") {
957
+ return getInlineBackgroundHex(element) || "#ffffff";
958
+ }
959
+ const target = element || editor;
960
+ return cssColorToHex(window.getComputedStyle(target).color) || "#000000";
961
+ };
962
+ const relativeLuminance = (color) => {
963
+ const channel = (value) => {
964
+ const next = value / 255;
965
+ return next <= 0.03928 ? next / 12.92 : Math.pow((next + 0.055) / 1.055, 2.4);
966
+ };
967
+ return 0.2126 * channel(color.r) + 0.7152 * channel(color.g) + 0.0722 * channel(color.b);
968
+ };
969
+ const contrastRatio = (fg, bg) => {
970
+ const lighter = Math.max(relativeLuminance(fg), relativeLuminance(bg));
971
+ const darker = Math.min(relativeLuminance(fg), relativeLuminance(bg));
972
+ return (lighter + 0.05) / (darker + 0.05);
973
+ };
974
+ const readableTextColorForBackground = (background) => {
975
+ const bg = parseCssColor(background);
976
+ if (!bg)
977
+ return "";
978
+ const candidates = ["#111827", "#f9fafb", "#1f2937", "#ffffff", "#000000", "#374151", "#e5e7eb"];
979
+ return candidates
980
+ .map((candidate) => ({
981
+ color: candidate,
982
+ ratio: contrastRatio(parseCssColor(candidate), bg),
983
+ }))
984
+ .sort((a, b) => b.ratio - a.ratio)[0]?.color || "";
985
+ };
753
986
  const applyBackgroundColor = (color) => {
754
- exec("hiliteColor", color);
987
+ try {
988
+ const editor = editableRef.current;
989
+ if (!editor)
990
+ return;
991
+ if (!restoreSavedSelection())
992
+ safeSelectRange(getSelectionRangeInEditor());
993
+ const range = getSelectionRangeInEditor();
994
+ const readableColor = readableTextColorForBackground(color);
995
+ if (!range)
996
+ return;
997
+ const applyToElement = (element) => {
998
+ element.style.backgroundColor = color;
999
+ if (readableColor)
1000
+ element.style.color = readableColor;
1001
+ };
1002
+ if (range.collapsed) {
1003
+ const span = document.createElement("span");
1004
+ applyToElement(span);
1005
+ span.textContent = "\u200B";
1006
+ range.insertNode(span);
1007
+ const nextRange = document.createRange();
1008
+ nextRange.setStart(span.firstChild || span, 1);
1009
+ nextRange.collapse(true);
1010
+ safeSelectRange(nextRange);
1011
+ savedRangeRef.current = nextRange.cloneRange();
1012
+ handleInput();
1013
+ return;
1014
+ }
1015
+ const selectedCells = Array.from(editor.querySelectorAll("td,th"))
1016
+ .filter((cell) => {
1017
+ try {
1018
+ return range.intersectsNode(cell);
1019
+ }
1020
+ catch {
1021
+ return false;
1022
+ }
1023
+ });
1024
+ if (selectedCells.length > 0) {
1025
+ selectedCells.forEach(applyToElement);
1026
+ handleInput();
1027
+ return;
1028
+ }
1029
+ const span = document.createElement("span");
1030
+ applyToElement(span);
1031
+ const fragment = range.extractContents();
1032
+ span.appendChild(fragment);
1033
+ range.insertNode(span);
1034
+ range.selectNodeContents(span);
1035
+ safeSelectRange(range);
1036
+ savedRangeRef.current = range.cloneRange();
1037
+ handleInput();
1038
+ }
1039
+ catch {
1040
+ exec("hiliteColor", color);
1041
+ }
755
1042
  };
756
1043
  const insertImage = () => {
757
1044
  if (!media)
@@ -2300,6 +2587,35 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2300
2587
  console.error("Error wrapping tables", e);
2301
2588
  }
2302
2589
  };
2590
+ const isCaretBoundaryBlock = (node) => {
2591
+ if (!(node instanceof HTMLElement))
2592
+ return false;
2593
+ const tag = node.tagName.toLowerCase();
2594
+ return (tag === "blockquote" ||
2595
+ tag === "table" ||
2596
+ node.getAttribute("data-table-wrapper") === "true");
2597
+ };
2598
+ const isEmptyCaretParagraph = (node) => {
2599
+ if (!(node instanceof HTMLParagraphElement))
2600
+ return false;
2601
+ return !node.textContent?.trim();
2602
+ };
2603
+ const createCaretParagraph = () => {
2604
+ const paragraph = document.createElement("p");
2605
+ paragraph.innerHTML = "<br>";
2606
+ paragraph.setAttribute("data-srte-caret-boundary", "true");
2607
+ return paragraph;
2608
+ };
2609
+ const ensureCaretBoundaryParagraphs = (root) => {
2610
+ const first = root.firstElementChild;
2611
+ if (isCaretBoundaryBlock(first) && !isEmptyCaretParagraph(first.previousSibling)) {
2612
+ root.insertBefore(createCaretParagraph(), first);
2613
+ }
2614
+ const last = root.lastElementChild;
2615
+ if (isCaretBoundaryBlock(last) && !isEmptyCaretParagraph(last.nextSibling)) {
2616
+ root.appendChild(createCaretParagraph());
2617
+ }
2618
+ };
2303
2619
  const handleInput = () => {
2304
2620
  if (isComposingRef.current)
2305
2621
  return;
@@ -2310,6 +2626,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2310
2626
  fixNegativeMargins(el);
2311
2627
  // Ensure tables are wrapped for horizontal scrolling
2312
2628
  ensureTableWrappers(el);
2629
+ // Keep a reachable typing position around isolating blocks at document edges
2630
+ ensureCaretBoundaryParagraphs(el);
2313
2631
  // Add resize handles to tables
2314
2632
  addTableResizeHandles();
2315
2633
  if (!onChange)
@@ -2340,8 +2658,9 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2340
2658
  const el = editableRef.current;
2341
2659
  if (!el)
2342
2660
  return;
2343
- // Ensure editor is focused and selection is inside
2344
- el.focus();
2661
+ if (!restoreSavedSelection()) {
2662
+ el.focus();
2663
+ }
2345
2664
  let sel = window.getSelection();
2346
2665
  let range = sel && sel.rangeCount > 0 ? sel.getRangeAt(0) : null;
2347
2666
  if (!range || !el.contains(range.commonAncestorContainer)) {
@@ -2360,10 +2679,14 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2360
2679
  const node = wrapper.firstChild;
2361
2680
  if (!node || !range)
2362
2681
  return;
2682
+ pushEditorHistory();
2683
+ if (!range.collapsed)
2684
+ range.deleteContents();
2363
2685
  range.insertNode(node);
2364
2686
  // Add resize handles to the new table
2365
- if (node instanceof HTMLTableElement) {
2366
- const tbody = node.querySelector('tbody');
2687
+ const insertedTable = node instanceof HTMLTableElement ? node : node.querySelector("table");
2688
+ if (insertedTable) {
2689
+ const tbody = insertedTable.querySelector('tbody');
2367
2690
  if (tbody) {
2368
2691
  const rows = Array.from(tbody.querySelectorAll('tr'));
2369
2692
  rows.forEach((row, index) => {
@@ -2706,19 +3029,43 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2706
3029
  handleInput();
2707
3030
  };
2708
3031
  const applyBgToSelection = (hex, fallbackCell) => {
3032
+ const readableColor = readableTextColorForBackground(hex);
3033
+ const applyFill = (cell) => {
3034
+ cell.style.background = hex;
3035
+ if (readableColor)
3036
+ cell.style.color = readableColor;
3037
+ };
2709
3038
  const sel = shouldUseTableSelection(fallbackCell) ? selectionRef.current : null;
2710
3039
  if (sel) {
2711
3040
  const cells = getCellsInGridRect(sel.tbody, sel.sr, sel.sc, sel.er, sel.ec);
2712
3041
  clearSelectionDecor();
2713
- cells.forEach((cell) => {
2714
- cell.style.background = hex;
2715
- });
3042
+ cells.forEach(applyFill);
2716
3043
  }
2717
3044
  else if (fallbackCell) {
2718
3045
  clearSelectionDecor();
2719
- fallbackCell.style.background = hex;
3046
+ applyFill(fallbackCell);
2720
3047
  }
2721
3048
  };
3049
+ const tableCellFillHex = (cell) => {
3050
+ const storedSelectionBackground = cell.__rtePrevBg;
3051
+ const inlineBackground = typeof storedSelectionBackground === "string"
3052
+ ? storedSelectionBackground
3053
+ : cell.style.backgroundColor || cell.style.background;
3054
+ const inlineHex = cssColorToHex(inlineBackground || "");
3055
+ if (inlineHex)
3056
+ return inlineHex;
3057
+ const computedHex = cssColorToHex(window.getComputedStyle(cell).backgroundColor);
3058
+ return computedHex || "#ffffff";
3059
+ };
3060
+ const tableMenuFillHex = (cell) => {
3061
+ const sel = shouldUseTableSelection(cell) ? selectionRef.current : null;
3062
+ if (!sel)
3063
+ return tableCellFillHex(cell);
3064
+ const cells = getCellsInGridRect(sel.tbody, sel.sr, sel.sc, sel.er, sel.ec);
3065
+ const first = cells[0] ? tableCellFillHex(cells[0]) : tableCellFillHex(cell);
3066
+ const allSame = cells.every((candidate) => tableCellFillHex(candidate) === first);
3067
+ return allSame ? first : tableCellFillHex(cell);
3068
+ };
2722
3069
  const toggleBorderSelection = (fallbackCell) => {
2723
3070
  const applyToggle = (cell) => {
2724
3071
  const cur = cell.style.border;
@@ -2837,6 +3184,184 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2837
3184
  handleInput();
2838
3185
  }
2839
3186
  };
3187
+ const getRangeFromPoint = (x, y) => {
3188
+ let range = null;
3189
+ // @ts-ignore
3190
+ if (document.caretRangeFromPoint) {
3191
+ // @ts-ignore
3192
+ range = document.caretRangeFromPoint(x, y);
3193
+ }
3194
+ else if (document.caretPositionFromPoint) {
3195
+ const pos = document.caretPositionFromPoint(x, y);
3196
+ if (pos) {
3197
+ range = document.createRange();
3198
+ range.setStart(pos.offsetNode, pos.offset);
3199
+ }
3200
+ }
3201
+ return range;
3202
+ };
3203
+ const getMovableElementFromNode = (node) => {
3204
+ const editor = editableRef.current;
3205
+ if (!editor || !node)
3206
+ return null;
3207
+ let element = node instanceof HTMLElement ? node : node.parentElement;
3208
+ if (!element || !editor.contains(element))
3209
+ return null;
3210
+ const image = element.closest("img");
3211
+ if (image && editor.contains(image)) {
3212
+ return (image.parentElement?.tagName === "A" ? image.parentElement : image);
3213
+ }
3214
+ const tableElement = element.closest("table");
3215
+ if (tableElement && editor.contains(tableElement)) {
3216
+ return (tableElement.closest('[data-table-wrapper="true"]') || tableElement);
3217
+ }
3218
+ const listElement = element.closest("ul,ol");
3219
+ if (listElement && editor.contains(listElement))
3220
+ return listElement;
3221
+ const block = element.closest('[data-table-wrapper="true"],blockquote,pre,p,h1,h2,h3,h4,h5,h6,div');
3222
+ if (!block || block === editor || !editor.contains(block))
3223
+ return null;
3224
+ if (block.getAttribute("data-srte-caret-boundary") === "true")
3225
+ return null;
3226
+ return block;
3227
+ };
3228
+ const updateDragHandleForTarget = (target) => {
3229
+ const scroller = editorScrollRef.current;
3230
+ if (dragHandleHideTimerRef.current != null) {
3231
+ window.clearTimeout(dragHandleHideTimerRef.current);
3232
+ dragHandleHideTimerRef.current = null;
3233
+ }
3234
+ if (!target || !scroller || !editableRef.current?.contains(target)) {
3235
+ setDragHandle(null);
3236
+ return;
3237
+ }
3238
+ const targetRect = target.getBoundingClientRect();
3239
+ const scrollRect = scroller.getBoundingClientRect();
3240
+ const next = {
3241
+ left: Math.max(4, targetRect.left - scrollRect.left + scroller.scrollLeft - 30),
3242
+ top: targetRect.top - scrollRect.top + scroller.scrollTop,
3243
+ height: Math.max(24, targetRect.height),
3244
+ target,
3245
+ };
3246
+ setDragHandle((current) => {
3247
+ if (current?.target === next.target &&
3248
+ Math.abs(current.left - next.left) < 1 &&
3249
+ Math.abs(current.top - next.top) < 1 &&
3250
+ Math.abs(current.height - next.height) < 1) {
3251
+ return current;
3252
+ }
3253
+ return next;
3254
+ });
3255
+ };
3256
+ const scheduleDragHandleHide = () => {
3257
+ if (dragHandleHideTimerRef.current != null) {
3258
+ window.clearTimeout(dragHandleHideTimerRef.current);
3259
+ }
3260
+ dragHandleHideTimerRef.current = window.setTimeout(() => {
3261
+ dragHandleHideTimerRef.current = null;
3262
+ if (!draggedBlockRef.current)
3263
+ setDragHandle(null);
3264
+ }, 120);
3265
+ };
3266
+ const getImageFromMovableBlock = (block) => {
3267
+ if (block.tagName === "IMG")
3268
+ return block;
3269
+ return block.querySelector("img");
3270
+ };
3271
+ const dropBlockAtPoint = (block, x, y) => {
3272
+ const editor = editableRef.current;
3273
+ if (!editor || !editor.contains(block))
3274
+ return false;
3275
+ const under = document.elementFromPoint(x, y);
3276
+ const draggedImage = getImageFromMovableBlock(block);
3277
+ const targetCell = draggedImage ? getClosestCell(under) : null;
3278
+ if (draggedImage && targetCell && !block.contains(targetCell)) {
3279
+ if (targetCell.contains(block))
3280
+ return false;
3281
+ const range = getRangeFromPoint(x, y);
3282
+ if (range && targetCell.contains(range.commonAncestorContainer)) {
3283
+ range.insertNode(block);
3284
+ }
3285
+ else {
3286
+ targetCell.appendChild(block);
3287
+ }
3288
+ return true;
3289
+ }
3290
+ const target = getMovableElementFromNode(under);
3291
+ if (target && target !== block && !block.contains(target) && !target.contains(block)) {
3292
+ const rect = target.getBoundingClientRect();
3293
+ const parent = target.parentElement;
3294
+ if (!parent)
3295
+ return false;
3296
+ if (y < rect.top + rect.height / 2)
3297
+ parent.insertBefore(block, target);
3298
+ else
3299
+ parent.insertBefore(block, target.nextSibling);
3300
+ return true;
3301
+ }
3302
+ const range = getRangeFromPoint(x, y);
3303
+ if (range && editor.contains(range.commonAncestorContainer)) {
3304
+ if (block.contains(range.commonAncestorContainer))
3305
+ return false;
3306
+ range.insertNode(block);
3307
+ return true;
3308
+ }
3309
+ editor.appendChild(block);
3310
+ return true;
3311
+ };
3312
+ const getTopLevelMovableElement = () => {
3313
+ const editor = editableRef.current;
3314
+ if (!editor)
3315
+ return null;
3316
+ const imageTarget = selectedImage?.parentElement?.tagName === "A"
3317
+ ? selectedImage.parentElement
3318
+ : selectedImage;
3319
+ if (imageTarget && editor.contains(imageTarget))
3320
+ return imageTarget;
3321
+ const range = getSelectionRangeInEditor();
3322
+ let node = range?.commonAncestorContainer || null;
3323
+ if (node && node.nodeType === Node.TEXT_NODE)
3324
+ node = node.parentNode;
3325
+ let element = node instanceof HTMLElement ? node : null;
3326
+ if (!element)
3327
+ return null;
3328
+ return getMovableElementFromNode(element);
3329
+ };
3330
+ const elementSibling = (element, direction) => {
3331
+ let sibling = direction === "previous" ? element.previousSibling : element.nextSibling;
3332
+ while (sibling && sibling.nodeType === Node.TEXT_NODE && !sibling.textContent?.trim()) {
3333
+ sibling = direction === "previous" ? sibling.previousSibling : sibling.nextSibling;
3334
+ }
3335
+ return sibling;
3336
+ };
3337
+ const moveCurrentElement = (direction) => {
3338
+ const editor = editableRef.current;
3339
+ const target = getTopLevelMovableElement();
3340
+ if (!editor || !target)
3341
+ return;
3342
+ pushEditorHistory();
3343
+ if (direction === "up") {
3344
+ const previous = elementSibling(target, "previous");
3345
+ if (previous)
3346
+ target.parentElement?.insertBefore(target, previous);
3347
+ }
3348
+ else if (direction === "down") {
3349
+ const next = elementSibling(target, "next");
3350
+ if (next)
3351
+ target.parentElement?.insertBefore(next, target);
3352
+ }
3353
+ else {
3354
+ const current = parseInt(target.style.marginLeft || "0", 10) || 0;
3355
+ const nextMargin = direction === "right"
3356
+ ? Math.min(current + 24, 240)
3357
+ : Math.max(current - 24, 0);
3358
+ target.style.marginLeft = nextMargin ? `${nextMargin}px` : "";
3359
+ }
3360
+ focusElementEnd(target);
3361
+ setSelectedImage(target.tagName === "IMG" ? target : target.querySelector("img"));
3362
+ scheduleImageOverlay();
3363
+ handleInput();
3364
+ };
2840
3365
  const addTableResizeHandles = () => {
2841
3366
  if (!table)
2842
3367
  return;
@@ -2894,7 +3419,24 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2894
3419
  background: "var(--srte-bg)",
2895
3420
  color: "var(--srte-text)",
2896
3421
  boxSizing: "border-box"
2897
- }, children: [_jsxs("div", { onMouseDown: preserveToolbarMouseDown, style: {
3422
+ }, children: [_jsxs("div", { "aria-disabled": readOnly, onMouseDown: (event) => {
3423
+ if (readOnly) {
3424
+ event.preventDefault();
3425
+ event.stopPropagation();
3426
+ return;
3427
+ }
3428
+ preserveToolbarMouseDown(event);
3429
+ }, onClick: (event) => {
3430
+ if (!readOnly)
3431
+ return;
3432
+ event.preventDefault();
3433
+ event.stopPropagation();
3434
+ }, onKeyDown: (event) => {
3435
+ if (!readOnly)
3436
+ return;
3437
+ event.preventDefault();
3438
+ event.stopPropagation();
3439
+ }, style: {
2898
3440
  display: "flex",
2899
3441
  flexWrap: "wrap",
2900
3442
  maxWidth: "100%",
@@ -2905,6 +3447,9 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2905
3447
  position: "sticky",
2906
3448
  top: 0,
2907
3449
  zIndex: 1,
3450
+ opacity: readOnly ? 0.55 : 1,
3451
+ pointerEvents: readOnly ? "none" : "auto",
3452
+ userSelect: readOnly ? "none" : undefined,
2908
3453
  }, children: [media && (_jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", multiple: true, style: { display: "none" }, onChange: (e) => {
2909
3454
  const list = e.currentTarget.files;
2910
3455
  if (list && list.length) {
@@ -2943,7 +3488,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2943
3488
  } }), _jsx("input", { ref: mdInputRef, type: "file", accept: ".md,.markdown,text/markdown,text/plain", style: { display: "none" }, onChange: (e) => {
2944
3489
  importTextFile(e.currentTarget.files, "md");
2945
3490
  e.currentTarget.value = "";
2946
- } }), _jsxs("select", { defaultValue: "p", onMouseDown: preserveEditorSelection, onChange: (e) => {
3491
+ } }), _jsxs("select", { value: currentBlockType, onMouseDown: preserveEditorSelection, onChange: (e) => {
2947
3492
  const val = e.target.value;
2948
3493
  if (val === "p")
2949
3494
  applyFormatBlock("<p>");
@@ -2960,7 +3505,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2960
3505
  borderRadius: 6,
2961
3506
  background: "var(--srte-input-bg)",
2962
3507
  color: "var(--srte-input-text)",
2963
- }, children: [_jsx("option", { value: "p", children: "Paragraph" }), _jsx("option", { value: "h1", children: "Heading 1" }), _jsx("option", { value: "h2", children: "Heading 2" }), _jsx("option", { value: "h3", children: "Heading 3" })] }), _jsx("button", { title: "Bold", onClick: () => exec("bold"), "aria-pressed": activeState.bold, style: activeButtonStyle(activeState.bold), children: _jsx("span", { style: { fontWeight: 700 }, children: "B" }) }), _jsx("button", { title: "Italic", onClick: () => exec("italic"), "aria-pressed": activeState.italic, style: activeButtonStyle(activeState.italic, { fontStyle: "italic" }), children: "I" }), _jsx("button", { title: "Underline", onClick: () => exec("underline"), "aria-pressed": activeState.underline, style: activeButtonStyle(activeState.underline, { textDecoration: "underline" }), children: "U" }), _jsx("button", { title: "Strikethrough", onClick: () => exec("strikeThrough"), "aria-pressed": activeState.strikeThrough, style: activeButtonStyle(activeState.strikeThrough, { textDecoration: "line-through" }), children: "S" }), _jsxs("select", { value: currentFontSize, onMouseDown: () => {
3508
+ }, children: [_jsx("option", { value: "p", children: "Paragraph" }), _jsx("option", { value: "h1", children: "Heading 1" }), _jsx("option", { value: "h2", children: "Heading 2" }), _jsx("option", { value: "h3", children: "Heading 3" })] }), _jsx("button", { title: "Bold", onClick: () => exec("bold"), "aria-pressed": activeState.bold, style: activeButtonStyle(activeState.bold), children: _jsx("span", { style: { fontWeight: 700 }, children: "B" }) }), _jsx("button", { title: "Italic", onClick: () => exec("italic"), "aria-pressed": activeState.italic, style: activeButtonStyle(activeState.italic, { fontStyle: "italic" }), children: "I" }), _jsx("button", { title: "Underline", onClick: () => exec("underline"), "aria-pressed": activeState.underline, style: activeButtonStyle(activeState.underline, { textDecoration: "underline" }), children: "U" }), _jsx("button", { title: "Strikethrough", onClick: () => exec("strikeThrough"), "aria-pressed": activeState.strikeThrough, style: activeButtonStyle(activeState.strikeThrough, { textDecoration: "line-through" }), children: "S" }), showFontSize && (_jsxs("select", { value: currentFontSize, onMouseDown: () => {
2964
3509
  // Save selection before dropdown interaction
2965
3510
  const sel = window.getSelection();
2966
3511
  if (sel && sel.rangeCount > 0) {
@@ -2977,7 +3522,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2977
3522
  borderRadius: 6,
2978
3523
  background: "var(--srte-input-bg)",
2979
3524
  color: "var(--srte-input-text)",
2980
- }, children: [_jsx("option", { value: "", disabled: true, children: "Size" }), _jsx("option", { value: "8", children: "8" }), _jsx("option", { value: "9", children: "9" }), _jsx("option", { value: "10", children: "10" }), _jsx("option", { value: "11", children: "11" }), _jsx("option", { value: "12", children: "12" }), _jsx("option", { value: "14", children: "14" }), _jsx("option", { value: "18", children: "18" }), _jsx("option", { value: "24", children: "24" }), _jsx("option", { value: "30", children: "30" }), _jsx("option", { value: "36", children: "36" }), _jsx("option", { value: "48", children: "48" }), _jsx("option", { value: "60", children: "60" }), _jsx("option", { value: "72", children: "72" }), _jsx("option", { value: "96", children: "96" })] }), preserveFontFamily && (_jsxs("select", { value: currentFont, onMouseDown: () => {
3525
+ }, children: [_jsx("option", { value: "", disabled: true, children: "Size" }), _jsx("option", { value: "8", children: "8" }), _jsx("option", { value: "9", children: "9" }), _jsx("option", { value: "10", children: "10" }), _jsx("option", { value: "11", children: "11" }), _jsx("option", { value: "12", children: "12" }), _jsx("option", { value: "14", children: "14" }), _jsx("option", { value: "18", children: "18" }), _jsx("option", { value: "24", children: "24" }), _jsx("option", { value: "30", children: "30" }), _jsx("option", { value: "36", children: "36" }), _jsx("option", { value: "48", children: "48" }), _jsx("option", { value: "60", children: "60" }), _jsx("option", { value: "72", children: "72" }), _jsx("option", { value: "96", children: "96" })] })), preserveFontFamily && (_jsxs("select", { value: currentFont, onMouseDown: () => {
2981
3526
  const sel = window.getSelection();
2982
3527
  if (sel && sel.rangeCount > 0) {
2983
3528
  const range = sel.getRangeAt(0);
@@ -3185,7 +3730,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
3185
3730
  borderRadius: 6,
3186
3731
  background: "var(--srte-input-bg)",
3187
3732
  color: "var(--srte-input-text)",
3188
- }, children: "\u2795 Table" })), _jsx("button", { title: "Undo", onClick: () => exec("undo"), style: {
3733
+ }, children: "\u2795 Table" })), _jsxs("div", { style: {
3734
+ display: "inline-flex",
3735
+ gap: 4,
3736
+ alignItems: "center",
3737
+ marginLeft: 6,
3738
+ }, children: [_jsx("span", { style: { fontSize: 12, opacity: 0.7 }, children: "Move:" }), _jsx("button", { title: "Move selected block up", onClick: () => moveCurrentElement("up"), style: activeButtonStyle(false, { height: 28, minWidth: 28, padding: "0 6px" }), children: "\u2191" }), _jsx("button", { title: "Move selected block down", onClick: () => moveCurrentElement("down"), style: activeButtonStyle(false, { height: 28, minWidth: 28, padding: "0 6px" }), children: "\u2193" }), _jsx("button", { title: "Move selected block left", onClick: () => moveCurrentElement("left"), style: activeButtonStyle(false, { height: 28, minWidth: 28, padding: "0 6px" }), children: "\u2190" }), _jsx("button", { title: "Move selected block right", onClick: () => moveCurrentElement("right"), style: activeButtonStyle(false, { height: 28, minWidth: 28, padding: "0 6px" }), children: "\u2192" })] }), _jsx("button", { title: "Undo", onClick: () => exec("undo"), style: {
3189
3739
  height: 32,
3190
3740
  padding: "0 10px",
3191
3741
  border: "1px solid var(--srte-input-border)",
@@ -3352,7 +3902,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
3352
3902
  borderRadius: 4,
3353
3903
  background: color,
3354
3904
  cursor: 'pointer',
3355
- }, title: color }, color))) }), _jsxs("div", { style: { marginBottom: 12 }, children: [_jsx("label", { style: { display: 'block', marginBottom: 6, fontSize: 12 }, children: "Custom color:" }), _jsx("input", { type: "color", onChange: (e) => {
3905
+ }, title: color }, color))) }), _jsxs("div", { style: { marginBottom: 12 }, children: [_jsx("label", { style: { display: 'block', marginBottom: 6, fontSize: 12 }, children: "Custom color:" }), _jsx("input", { type: "color", value: currentPickerColorHex(), onChange: (e) => {
3356
3906
  if (colorPickerType === 'text') {
3357
3907
  applyTextColor(e.target.value);
3358
3908
  }
@@ -3521,7 +4071,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
3521
4071
  borderRadius: 4,
3522
4072
  background: "var(--srte-input-bg)",
3523
4073
  color: "var(--srte-modal-text)",
3524
- }, title: sym, children: sym }, i)))] })] }) })), _jsxs("div", { ref: editorScrollRef, style: {
4074
+ }, title: sym, children: sym }, i)))] })] }) })), _jsxs("div", { ref: editorScrollRef, onScroll: () => updateDragHandleForTarget(dragHandle?.target || null), style: {
3525
4075
  width: "100%",
3526
4076
  maxWidth: "100%",
3527
4077
  flex: "1 1 auto",
@@ -3537,6 +4087,13 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
3537
4087
  }, children: [_jsx("div", { ref: editableRef, contentEditable: !readOnly, suppressContentEditableWarning: true, onInput: handleInput, onKeyUp: updateActiveState, onMouseUp: updateActiveState, onCompositionStart: () => (isComposingRef.current = true), onCompositionEnd: () => {
3538
4088
  isComposingRef.current = false;
3539
4089
  handleInput();
4090
+ }, onMouseMove: (e) => {
4091
+ if (draggedBlockRef.current)
4092
+ return;
4093
+ updateDragHandleForTarget(getMovableElementFromNode(e.target));
4094
+ }, onMouseLeave: () => {
4095
+ if (!draggedBlockRef.current)
4096
+ scheduleDragHandleHide();
3540
4097
  }, onPaste: (e) => {
3541
4098
  const items = e.clipboardData?.files;
3542
4099
  if (media && items && items.length) {
@@ -3554,29 +4111,30 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
3554
4111
  }
3555
4112
  }, onDragOver: (e) => {
3556
4113
  // Allow dragging images within editor and file drops
3557
- if (draggedImageRef.current ||
4114
+ if (draggedBlockRef.current ||
4115
+ draggedImageRef.current ||
3558
4116
  e.dataTransfer?.types?.includes("Files")) {
3559
4117
  e.preventDefault();
3560
4118
  }
3561
4119
  }, onDrop: (e) => {
4120
+ if (draggedBlockRef.current) {
4121
+ e.preventDefault();
4122
+ const block = draggedBlockRef.current;
4123
+ draggedBlockRef.current = null;
4124
+ pushEditorHistory();
4125
+ if (dropBlockAtPoint(block, e.clientX, e.clientY)) {
4126
+ focusElementEnd(block);
4127
+ updateDragHandleForTarget(block);
4128
+ handleInput();
4129
+ }
4130
+ return;
4131
+ }
3562
4132
  // Move existing dragged image inside editor
3563
4133
  if (draggedImageRef.current) {
3564
4134
  e.preventDefault();
3565
4135
  const x = e.clientX;
3566
4136
  const y = e.clientY;
3567
- let range = null;
3568
- // @ts-ignore
3569
- if (document.caretRangeFromPoint) {
3570
- // @ts-ignore
3571
- range = document.caretRangeFromPoint(x, y);
3572
- }
3573
- else if (document.caretPositionFromPoint) {
3574
- const pos = document.caretPositionFromPoint(x, y);
3575
- if (pos) {
3576
- range = document.createRange();
3577
- range.setStart(pos.offsetNode, pos.offset);
3578
- }
3579
- }
4137
+ let range = getRangeFromPoint(x, y);
3580
4138
  const img = draggedImageRef.current;
3581
4139
  draggedImageRef.current = null;
3582
4140
  if (range &&
@@ -3585,6 +4143,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
3585
4143
  // Avoid inserting inside the image itself
3586
4144
  if (range.startContainer === img || range.endContainer === img)
3587
4145
  return;
4146
+ pushEditorHistory();
3588
4147
  // If dropping inside a link, insert right after the link element
3589
4148
  let container = range.commonAncestorContainer;
3590
4149
  let linkAncestor = null;
@@ -3617,19 +4176,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
3617
4176
  // Try to move caret to drop point
3618
4177
  const x = e.clientX;
3619
4178
  const y = e.clientY;
3620
- let range = null;
3621
- // @ts-ignore
3622
- if (document.caretRangeFromPoint) {
3623
- // @ts-ignore
3624
- range = document.caretRangeFromPoint(x, y);
3625
- }
3626
- else if (document.caretPositionFromPoint) {
3627
- const pos = document.caretPositionFromPoint(x, y);
3628
- if (pos) {
3629
- range = document.createRange();
3630
- range.setStart(pos.offsetNode, pos.offset);
3631
- }
3632
- }
4179
+ let range = getRangeFromPoint(x, y);
3633
4180
  if (range) {
3634
4181
  const sel = window.getSelection();
3635
4182
  sel?.removeAllRanges();
@@ -3671,6 +4218,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
3671
4218
  }
3672
4219
  }, onDragEnd: () => {
3673
4220
  draggedImageRef.current = null;
4221
+ draggedBlockRef.current = null;
3674
4222
  }, style: {
3675
4223
  minHeight: typeof minHeight === "number" ? `${minHeight}px` : minHeight,
3676
4224
  maxWidth: "100%",
@@ -3824,7 +4372,55 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
3824
4372
  setTableMenu(null);
3825
4373
  setImageMenu(null);
3826
4374
  }
3827
- } }), selectedImage && imageOverlay && (_jsxs("div", { style: {
4375
+ } }), dragHandle && !readOnly && (_jsx("button", { type: "button", draggable: true, title: "Drag block", "aria-label": "Drag block", onMouseEnter: () => {
4376
+ if (dragHandleHideTimerRef.current != null) {
4377
+ window.clearTimeout(dragHandleHideTimerRef.current);
4378
+ dragHandleHideTimerRef.current = null;
4379
+ }
4380
+ }, onMouseLeave: () => {
4381
+ if (!draggedBlockRef.current)
4382
+ scheduleDragHandleHide();
4383
+ }, onMouseDown: (e) => {
4384
+ e.stopPropagation();
4385
+ }, onDragStart: (e) => {
4386
+ draggedBlockRef.current = dragHandle.target;
4387
+ e.dataTransfer.effectAllowed = "move";
4388
+ e.dataTransfer.setData("text/plain", "moving-block");
4389
+ try {
4390
+ const ghost = dragHandle.target.cloneNode(true);
4391
+ ghost.style.position = "fixed";
4392
+ ghost.style.left = "-10000px";
4393
+ ghost.style.top = "-10000px";
4394
+ ghost.style.width = `${Math.min(dragHandle.target.getBoundingClientRect().width, 480)}px`;
4395
+ ghost.style.opacity = "0.75";
4396
+ document.body.appendChild(ghost);
4397
+ e.dataTransfer.setDragImage(ghost, 12, 12);
4398
+ window.setTimeout(() => ghost.remove(), 0);
4399
+ }
4400
+ catch { }
4401
+ }, onDragEnd: () => {
4402
+ draggedBlockRef.current = null;
4403
+ updateDragHandleForTarget(dragHandle.target);
4404
+ }, style: {
4405
+ position: "absolute",
4406
+ left: dragHandle.left,
4407
+ top: dragHandle.top + Math.max(0, (dragHandle.height - 24) / 2),
4408
+ width: 24,
4409
+ height: 24,
4410
+ display: "inline-flex",
4411
+ alignItems: "center",
4412
+ justifyContent: "center",
4413
+ border: "1px solid var(--srte-border)",
4414
+ borderRadius: 6,
4415
+ background: "var(--srte-input-bg)",
4416
+ color: "var(--srte-input-text)",
4417
+ boxShadow: "var(--srte-menu-shadow)",
4418
+ cursor: "grab",
4419
+ zIndex: 8,
4420
+ fontSize: 14,
4421
+ lineHeight: 1,
4422
+ padding: 0,
4423
+ }, children: "\u22EE\u22EE" })), selectedImage && imageOverlay && (_jsxs("div", { style: {
3828
4424
  position: "absolute",
3829
4425
  left: imageOverlay.left,
3830
4426
  top: imageOverlay.top,
@@ -3955,7 +4551,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
3955
4551
  alignItems: "center",
3956
4552
  padding: "4px 6px",
3957
4553
  fontSize: 12,
3958
- }, children: [_jsx("span", { children: "Fill:" }), _jsx("input", { type: "color", defaultValue: "#ffffff", onChange: (e) => {
4554
+ }, children: [_jsx("span", { children: "Fill:" }), _jsx("input", { type: "color", value: tableMenuFillHex(tableMenu.cell), onChange: (e) => {
3959
4555
  runTableCellAction(tableMenu.cell, (cell) => applyBgToSelection(e.target.value, cell));
3960
4556
  }, style: {
3961
4557
  width: 28,
@@ -37,7 +37,7 @@ function ClassicEditorHost(props, ref) {
37
37
  }
38
38
  }
39
39
  catch { }
40
- }, placeholder: props.placeholder, minHeight: props.minHeight, maxHeight: props.maxHeight, readOnly: props.readOnly, table: props.table, media: props.media, formula: props.formula, mediaManager: props.mediaManager, theme: props.theme, className: props.className }) }));
40
+ }, placeholder: props.placeholder, minHeight: props.minHeight, maxHeight: props.maxHeight, readOnly: props.readOnly, table: props.table, media: props.media, formula: props.formula, showFontSize: props.showFontSize, mediaManager: props.mediaManager, theme: props.theme, className: props.className }) }));
41
41
  }
42
42
  const ClassicEditorHostWithRef = React.forwardRef(ClassicEditorHost);
43
43
  function initClassicEditor(opts) {
package/dist/theme.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export type SrteTheme = 'light' | 'dark';
2
- export declare const SRTE_DEFAULT_CSS = "\n.srte-editor {\n --srte-bg: #ffffff;\n --srte-text: #111111;\n --srte-text-muted: #4b5563;\n --srte-border: #dddddd;\n --srte-border-light: #eeeeee;\n --srte-toolbar-bg: #ffffff;\n --srte-input-bg: #ffffff;\n --srte-input-text: #111111;\n --srte-input-border: #e5e7eb;\n --srte-modal-backdrop: rgba(0, 0, 0, 0.35);\n --srte-modal-backdrop-filter: blur(2px);\n --srte-modal-bg: #ffffff;\n --srte-modal-text: #000000;\n --srte-menu-bg: #ffffff;\n --srte-menu-text: #111111;\n --srte-menu-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);\n --srte-accent: #1e90ff;\n --srte-accent-bg: rgba(30, 144, 255, 0.15);\n --srte-danger: #dc2626;\n --srte-primary: #2563eb;\n --srte-surface-subtle: #f3f4f6;\n --srte-on-primary: #ffffff;\n --srte-cancel-bg: #f3f4f6;\n}\n.srte-editor.srte-dark {\n --srte-bg: #1e1e1e;\n --srte-text: #e0e0e0;\n --srte-text-muted: #9ca3af;\n --srte-border: #3a3a3a;\n --srte-border-light: #2e2e2e;\n --srte-toolbar-bg: #252525;\n --srte-input-bg: #2a2a2a;\n --srte-input-text: #e0e0e0;\n --srte-input-border: #444444;\n --srte-modal-backdrop: rgba(0, 0, 0, 0.22);\n --srte-modal-backdrop-filter: blur(10px) saturate(0.9);\n --srte-modal-bg: #1e293b;\n --srte-modal-text: #e0e0e0;\n --srte-menu-bg: #1e293b;\n --srte-menu-text: #e0e0e0;\n --srte-menu-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);\n --srte-accent: #3b9eff;\n --srte-accent-bg: rgba(59, 158, 255, 0.2);\n --srte-danger: #ef4444;\n --srte-primary: #3b82f6;\n --srte-surface-subtle: #333333;\n --srte-on-primary: #ffffff;\n --srte-cancel-bg: #333333;\n}\n.srte-editor [contenteditable] blockquote {\n border-left: 4px solid var(--srte-accent);\n margin: 0.75em 0;\n padding: 0.5em 1em;\n background: var(--srte-surface-subtle);\n color: var(--srte-text);\n}\n.srte-editor [contenteditable] ul {\n list-style-type: disc;\n list-style-position: outside;\n margin: 0.75em 0;\n padding-left: 1.75em;\n}\n.srte-editor [contenteditable] ol {\n list-style-type: decimal;\n list-style-position: outside;\n margin: 0.75em 0;\n padding-left: 1.75em;\n}\n.srte-editor [contenteditable] li {\n display: list-item;\n margin: 0.25em 0;\n padding-left: 0.25em;\n}\n.srte-editor [contenteditable] li::marker {\n color: currentColor;\n}\n.srte-editor.srte-dark [contenteditable] [style*=\"color\"]:not(.srte-preserve-colors):not(.srte-preserve-colors *),\n.srte-editor.srte-dark [contenteditable] [style*=\"background\"]:not(.srte-preserve-colors):not(.srte-preserve-colors *) {\n color: var(--srte-text) !important;\n background: transparent !important;\n background-color: transparent !important;\n}\n.srte-editor [contenteditable] sub,\n.srte-editor [contenteditable] sup {\n line-height: 0;\n}\n";
2
+ export declare const SRTE_DEFAULT_CSS = "\n.srte-editor {\n --srte-bg: #ffffff;\n --srte-text: #111111;\n --srte-text-muted: #4b5563;\n --srte-border: #dddddd;\n --srte-border-light: #eeeeee;\n --srte-toolbar-bg: #ffffff;\n --srte-input-bg: #ffffff;\n --srte-input-text: #111111;\n --srte-input-border: #e5e7eb;\n --srte-modal-backdrop: rgba(0, 0, 0, 0.35);\n --srte-modal-backdrop-filter: blur(2px);\n --srte-modal-bg: #ffffff;\n --srte-modal-text: #000000;\n --srte-menu-bg: #ffffff;\n --srte-menu-text: #111111;\n --srte-menu-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);\n --srte-accent: #1e90ff;\n --srte-accent-bg: rgba(30, 144, 255, 0.15);\n --srte-danger: #dc2626;\n --srte-primary: #2563eb;\n --srte-surface-subtle: #f3f4f6;\n --srte-on-primary: #ffffff;\n --srte-cancel-bg: #f3f4f6;\n}\n.srte-editor.srte-dark {\n --srte-bg: #1e1e1e;\n --srte-text: #e0e0e0;\n --srte-text-muted: #9ca3af;\n --srte-border: #3a3a3a;\n --srte-border-light: #2e2e2e;\n --srte-toolbar-bg: #252525;\n --srte-input-bg: #2a2a2a;\n --srte-input-text: #e0e0e0;\n --srte-input-border: #444444;\n --srte-modal-backdrop: rgba(0, 0, 0, 0.22);\n --srte-modal-backdrop-filter: blur(10px) saturate(0.9);\n --srte-modal-bg: #1e293b;\n --srte-modal-text: #e0e0e0;\n --srte-menu-bg: #1e293b;\n --srte-menu-text: #e0e0e0;\n --srte-menu-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);\n --srte-accent: #3b9eff;\n --srte-accent-bg: rgba(59, 158, 255, 0.2);\n --srte-danger: #ef4444;\n --srte-primary: #3b82f6;\n --srte-surface-subtle: #333333;\n --srte-on-primary: #ffffff;\n --srte-cancel-bg: #333333;\n}\n.srte-editor [contenteditable] blockquote {\n border-left: 4px solid var(--srte-accent);\n margin: 0.75em 0;\n padding: 0.5em 1em;\n background: var(--srte-surface-subtle);\n color: var(--srte-text);\n}\n.srte-editor [contenteditable] p,\n.srte-editor [contenteditable] h1,\n.srte-editor [contenteditable] h2,\n.srte-editor [contenteditable] h3 {\n color: inherit;\n}\n.srte-editor [contenteditable] p {\n display: block;\n margin: 0 0 0.75em;\n font-size: 1em;\n font-weight: 400;\n line-height: 1.6;\n}\n.srte-editor [contenteditable] h1,\n.srte-editor [contenteditable] h2,\n.srte-editor [contenteditable] h3 {\n display: block;\n margin: 0.75em 0 0.4em;\n font-weight: 700;\n line-height: 1.25;\n}\n.srte-editor [contenteditable] h1 {\n font-size: 2em;\n}\n.srte-editor [contenteditable] h2 {\n font-size: 1.5em;\n}\n.srte-editor [contenteditable] h3 {\n font-size: 1.25em;\n}\n.srte-editor [contenteditable] > :first-child {\n margin-top: 0;\n}\n.srte-editor [contenteditable] ul {\n list-style-type: disc;\n list-style-position: outside;\n margin: 0.75em 0;\n padding-left: 1.75em;\n}\n.srte-editor [contenteditable] ol {\n list-style-type: decimal;\n list-style-position: outside;\n margin: 0.75em 0;\n padding-left: 1.75em;\n}\n.srte-editor [contenteditable] li {\n display: list-item;\n margin: 0.25em 0;\n padding-left: 0.25em;\n}\n.srte-editor [contenteditable] li::marker {\n color: currentColor;\n}\n.srte-editor.srte-dark [contenteditable] [style*=\"color\"]:not(td):not(th):not(.srte-preserve-colors):not(.srte-preserve-colors *),\n.srte-editor.srte-dark [contenteditable] [style*=\"background\"]:not(td):not(th):not(.srte-preserve-colors):not(.srte-preserve-colors *) {\n color: var(--srte-text) !important;\n background: transparent !important;\n background-color: transparent !important;\n}\n.srte-editor [contenteditable] sub,\n.srte-editor [contenteditable] sup {\n line-height: 0;\n}\n";
3
3
  export declare function ensureStyleSheet(): void;
package/dist/theme.js CHANGED
@@ -56,6 +56,39 @@ export const SRTE_DEFAULT_CSS = `
56
56
  background: var(--srte-surface-subtle);
57
57
  color: var(--srte-text);
58
58
  }
59
+ .srte-editor [contenteditable] p,
60
+ .srte-editor [contenteditable] h1,
61
+ .srte-editor [contenteditable] h2,
62
+ .srte-editor [contenteditable] h3 {
63
+ color: inherit;
64
+ }
65
+ .srte-editor [contenteditable] p {
66
+ display: block;
67
+ margin: 0 0 0.75em;
68
+ font-size: 1em;
69
+ font-weight: 400;
70
+ line-height: 1.6;
71
+ }
72
+ .srte-editor [contenteditable] h1,
73
+ .srte-editor [contenteditable] h2,
74
+ .srte-editor [contenteditable] h3 {
75
+ display: block;
76
+ margin: 0.75em 0 0.4em;
77
+ font-weight: 700;
78
+ line-height: 1.25;
79
+ }
80
+ .srte-editor [contenteditable] h1 {
81
+ font-size: 2em;
82
+ }
83
+ .srte-editor [contenteditable] h2 {
84
+ font-size: 1.5em;
85
+ }
86
+ .srte-editor [contenteditable] h3 {
87
+ font-size: 1.25em;
88
+ }
89
+ .srte-editor [contenteditable] > :first-child {
90
+ margin-top: 0;
91
+ }
59
92
  .srte-editor [contenteditable] ul {
60
93
  list-style-type: disc;
61
94
  list-style-position: outside;
@@ -76,8 +109,8 @@ export const SRTE_DEFAULT_CSS = `
76
109
  .srte-editor [contenteditable] li::marker {
77
110
  color: currentColor;
78
111
  }
79
- .srte-editor.srte-dark [contenteditable] [style*="color"]:not(.srte-preserve-colors):not(.srte-preserve-colors *),
80
- .srte-editor.srte-dark [contenteditable] [style*="background"]:not(.srte-preserve-colors):not(.srte-preserve-colors *) {
112
+ .srte-editor.srte-dark [contenteditable] [style*="color"]:not(td):not(th):not(.srte-preserve-colors):not(.srte-preserve-colors *),
113
+ .srte-editor.srte-dark [contenteditable] [style*="background"]:not(td):not(th):not(.srte-preserve-colors):not(.srte-preserve-colors *) {
81
114
  color: var(--srte-text) !important;
82
115
  background: transparent !important;
83
116
  background-color: transparent !important;
@@ -91,8 +124,13 @@ const SRTE_STYLE_ID = 'srte-theme-defaults';
91
124
  export function ensureStyleSheet() {
92
125
  if (typeof document === 'undefined')
93
126
  return;
94
- if (document.getElementById(SRTE_STYLE_ID))
127
+ const existing = document.getElementById(SRTE_STYLE_ID);
128
+ if (existing) {
129
+ if (existing.textContent !== SRTE_DEFAULT_CSS) {
130
+ existing.textContent = SRTE_DEFAULT_CSS;
131
+ }
95
132
  return;
133
+ }
96
134
  const style = document.createElement('style');
97
135
  style.id = SRTE_STYLE_ID;
98
136
  style.textContent = SRTE_DEFAULT_CSS;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smartrte-react",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "A powerful, feature-rich Rich Text Editor for React with support for tables, mathematical formulas (LaTeX/KaTeX), and media management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",