smartrte-react 0.2.6 → 0.2.7

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;
@@ -2943,7 +3468,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2943
3468
  } }), _jsx("input", { ref: mdInputRef, type: "file", accept: ".md,.markdown,text/markdown,text/plain", style: { display: "none" }, onChange: (e) => {
2944
3469
  importTextFile(e.currentTarget.files, "md");
2945
3470
  e.currentTarget.value = "";
2946
- } }), _jsxs("select", { defaultValue: "p", onMouseDown: preserveEditorSelection, onChange: (e) => {
3471
+ } }), _jsxs("select", { value: currentBlockType, onMouseDown: preserveEditorSelection, onChange: (e) => {
2947
3472
  const val = e.target.value;
2948
3473
  if (val === "p")
2949
3474
  applyFormatBlock("<p>");
@@ -2960,7 +3485,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2960
3485
  borderRadius: 6,
2961
3486
  background: "var(--srte-input-bg)",
2962
3487
  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: () => {
3488
+ }, 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
3489
  // Save selection before dropdown interaction
2965
3490
  const sel = window.getSelection();
2966
3491
  if (sel && sel.rangeCount > 0) {
@@ -2977,7 +3502,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2977
3502
  borderRadius: 6,
2978
3503
  background: "var(--srte-input-bg)",
2979
3504
  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: () => {
3505
+ }, 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
3506
  const sel = window.getSelection();
2982
3507
  if (sel && sel.rangeCount > 0) {
2983
3508
  const range = sel.getRangeAt(0);
@@ -3185,7 +3710,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
3185
3710
  borderRadius: 6,
3186
3711
  background: "var(--srte-input-bg)",
3187
3712
  color: "var(--srte-input-text)",
3188
- }, children: "\u2795 Table" })), _jsx("button", { title: "Undo", onClick: () => exec("undo"), style: {
3713
+ }, children: "\u2795 Table" })), _jsxs("div", { style: {
3714
+ display: "inline-flex",
3715
+ gap: 4,
3716
+ alignItems: "center",
3717
+ marginLeft: 6,
3718
+ }, 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
3719
  height: 32,
3190
3720
  padding: "0 10px",
3191
3721
  border: "1px solid var(--srte-input-border)",
@@ -3352,7 +3882,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
3352
3882
  borderRadius: 4,
3353
3883
  background: color,
3354
3884
  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) => {
3885
+ }, 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
3886
  if (colorPickerType === 'text') {
3357
3887
  applyTextColor(e.target.value);
3358
3888
  }
@@ -3521,7 +4051,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
3521
4051
  borderRadius: 4,
3522
4052
  background: "var(--srte-input-bg)",
3523
4053
  color: "var(--srte-modal-text)",
3524
- }, title: sym, children: sym }, i)))] })] }) })), _jsxs("div", { ref: editorScrollRef, style: {
4054
+ }, title: sym, children: sym }, i)))] })] }) })), _jsxs("div", { ref: editorScrollRef, onScroll: () => updateDragHandleForTarget(dragHandle?.target || null), style: {
3525
4055
  width: "100%",
3526
4056
  maxWidth: "100%",
3527
4057
  flex: "1 1 auto",
@@ -3537,6 +4067,13 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
3537
4067
  }, children: [_jsx("div", { ref: editableRef, contentEditable: !readOnly, suppressContentEditableWarning: true, onInput: handleInput, onKeyUp: updateActiveState, onMouseUp: updateActiveState, onCompositionStart: () => (isComposingRef.current = true), onCompositionEnd: () => {
3538
4068
  isComposingRef.current = false;
3539
4069
  handleInput();
4070
+ }, onMouseMove: (e) => {
4071
+ if (draggedBlockRef.current)
4072
+ return;
4073
+ updateDragHandleForTarget(getMovableElementFromNode(e.target));
4074
+ }, onMouseLeave: () => {
4075
+ if (!draggedBlockRef.current)
4076
+ scheduleDragHandleHide();
3540
4077
  }, onPaste: (e) => {
3541
4078
  const items = e.clipboardData?.files;
3542
4079
  if (media && items && items.length) {
@@ -3554,29 +4091,30 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
3554
4091
  }
3555
4092
  }, onDragOver: (e) => {
3556
4093
  // Allow dragging images within editor and file drops
3557
- if (draggedImageRef.current ||
4094
+ if (draggedBlockRef.current ||
4095
+ draggedImageRef.current ||
3558
4096
  e.dataTransfer?.types?.includes("Files")) {
3559
4097
  e.preventDefault();
3560
4098
  }
3561
4099
  }, onDrop: (e) => {
4100
+ if (draggedBlockRef.current) {
4101
+ e.preventDefault();
4102
+ const block = draggedBlockRef.current;
4103
+ draggedBlockRef.current = null;
4104
+ pushEditorHistory();
4105
+ if (dropBlockAtPoint(block, e.clientX, e.clientY)) {
4106
+ focusElementEnd(block);
4107
+ updateDragHandleForTarget(block);
4108
+ handleInput();
4109
+ }
4110
+ return;
4111
+ }
3562
4112
  // Move existing dragged image inside editor
3563
4113
  if (draggedImageRef.current) {
3564
4114
  e.preventDefault();
3565
4115
  const x = e.clientX;
3566
4116
  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
- }
4117
+ let range = getRangeFromPoint(x, y);
3580
4118
  const img = draggedImageRef.current;
3581
4119
  draggedImageRef.current = null;
3582
4120
  if (range &&
@@ -3585,6 +4123,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
3585
4123
  // Avoid inserting inside the image itself
3586
4124
  if (range.startContainer === img || range.endContainer === img)
3587
4125
  return;
4126
+ pushEditorHistory();
3588
4127
  // If dropping inside a link, insert right after the link element
3589
4128
  let container = range.commonAncestorContainer;
3590
4129
  let linkAncestor = null;
@@ -3617,19 +4156,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
3617
4156
  // Try to move caret to drop point
3618
4157
  const x = e.clientX;
3619
4158
  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
- }
4159
+ let range = getRangeFromPoint(x, y);
3633
4160
  if (range) {
3634
4161
  const sel = window.getSelection();
3635
4162
  sel?.removeAllRanges();
@@ -3671,6 +4198,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
3671
4198
  }
3672
4199
  }, onDragEnd: () => {
3673
4200
  draggedImageRef.current = null;
4201
+ draggedBlockRef.current = null;
3674
4202
  }, style: {
3675
4203
  minHeight: typeof minHeight === "number" ? `${minHeight}px` : minHeight,
3676
4204
  maxWidth: "100%",
@@ -3824,7 +4352,55 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
3824
4352
  setTableMenu(null);
3825
4353
  setImageMenu(null);
3826
4354
  }
3827
- } }), selectedImage && imageOverlay && (_jsxs("div", { style: {
4355
+ } }), dragHandle && !readOnly && (_jsx("button", { type: "button", draggable: true, title: "Drag block", "aria-label": "Drag block", onMouseEnter: () => {
4356
+ if (dragHandleHideTimerRef.current != null) {
4357
+ window.clearTimeout(dragHandleHideTimerRef.current);
4358
+ dragHandleHideTimerRef.current = null;
4359
+ }
4360
+ }, onMouseLeave: () => {
4361
+ if (!draggedBlockRef.current)
4362
+ scheduleDragHandleHide();
4363
+ }, onMouseDown: (e) => {
4364
+ e.stopPropagation();
4365
+ }, onDragStart: (e) => {
4366
+ draggedBlockRef.current = dragHandle.target;
4367
+ e.dataTransfer.effectAllowed = "move";
4368
+ e.dataTransfer.setData("text/plain", "moving-block");
4369
+ try {
4370
+ const ghost = dragHandle.target.cloneNode(true);
4371
+ ghost.style.position = "fixed";
4372
+ ghost.style.left = "-10000px";
4373
+ ghost.style.top = "-10000px";
4374
+ ghost.style.width = `${Math.min(dragHandle.target.getBoundingClientRect().width, 480)}px`;
4375
+ ghost.style.opacity = "0.75";
4376
+ document.body.appendChild(ghost);
4377
+ e.dataTransfer.setDragImage(ghost, 12, 12);
4378
+ window.setTimeout(() => ghost.remove(), 0);
4379
+ }
4380
+ catch { }
4381
+ }, onDragEnd: () => {
4382
+ draggedBlockRef.current = null;
4383
+ updateDragHandleForTarget(dragHandle.target);
4384
+ }, style: {
4385
+ position: "absolute",
4386
+ left: dragHandle.left,
4387
+ top: dragHandle.top + Math.max(0, (dragHandle.height - 24) / 2),
4388
+ width: 24,
4389
+ height: 24,
4390
+ display: "inline-flex",
4391
+ alignItems: "center",
4392
+ justifyContent: "center",
4393
+ border: "1px solid var(--srte-border)",
4394
+ borderRadius: 6,
4395
+ background: "var(--srte-input-bg)",
4396
+ color: "var(--srte-input-text)",
4397
+ boxShadow: "var(--srte-menu-shadow)",
4398
+ cursor: "grab",
4399
+ zIndex: 8,
4400
+ fontSize: 14,
4401
+ lineHeight: 1,
4402
+ padding: 0,
4403
+ }, children: "\u22EE\u22EE" })), selectedImage && imageOverlay && (_jsxs("div", { style: {
3828
4404
  position: "absolute",
3829
4405
  left: imageOverlay.left,
3830
4406
  top: imageOverlay.top,
@@ -3955,7 +4531,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
3955
4531
  alignItems: "center",
3956
4532
  padding: "4px 6px",
3957
4533
  fontSize: 12,
3958
- }, children: [_jsx("span", { children: "Fill:" }), _jsx("input", { type: "color", defaultValue: "#ffffff", onChange: (e) => {
4534
+ }, children: [_jsx("span", { children: "Fill:" }), _jsx("input", { type: "color", value: tableMenuFillHex(tableMenu.cell), onChange: (e) => {
3959
4535
  runTableCellAction(tableMenu.cell, (cell) => applyBgToSelection(e.target.value, cell));
3960
4536
  }, style: {
3961
4537
  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.7",
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",