smartrte-react 0.3.3 → 0.3.5

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.
Files changed (87) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +14 -83
  3. package/dist/adapters/canonicalInlineCommandBridge.d.ts +15 -0
  4. package/dist/adapters/canonicalInlineCommandBridge.js +269 -0
  5. package/dist/adapters/canonicalListCommandBridge.d.ts +26 -0
  6. package/dist/adapters/canonicalListCommandBridge.js +430 -0
  7. package/dist/adapters/documentFormats.d.ts +26 -0
  8. package/dist/adapters/documentFormats.js +49 -0
  9. package/dist/adapters/docxFormat.d.ts +5 -0
  10. package/dist/adapters/docxFormat.js +272 -0
  11. package/dist/adapters/domBlockCommandBridge.d.ts +19 -0
  12. package/dist/adapters/domBlockCommandBridge.js +79 -0
  13. package/dist/adapters/domChecklistCommandBridge.d.ts +6 -0
  14. package/dist/adapters/domChecklistCommandBridge.js +33 -0
  15. package/dist/adapters/domCommandBridge.d.ts +19 -0
  16. package/dist/adapters/domCommandBridge.js +33 -0
  17. package/dist/adapters/domInlineAtomCommandBridge.d.ts +14 -0
  18. package/dist/adapters/domInlineAtomCommandBridge.js +101 -0
  19. package/dist/adapters/domInlineImageCommandBridge.d.ts +14 -0
  20. package/dist/adapters/domInlineImageCommandBridge.js +72 -0
  21. package/dist/adapters/domMoveCommandBridge.d.ts +7 -0
  22. package/dist/adapters/domMoveCommandBridge.js +54 -0
  23. package/dist/adapters/domSelectionBridge.d.ts +3 -1
  24. package/dist/adapters/domSelectionBridge.js +96 -44
  25. package/dist/adapters/domSmartDocument.js +158 -16
  26. package/dist/adapters/domTableCommandBridge.d.ts +35 -0
  27. package/dist/adapters/domTableCommandBridge.js +201 -0
  28. package/dist/adapters/legacyListShadowComparator.d.ts +26 -0
  29. package/dist/adapters/legacyListShadowComparator.js +316 -0
  30. package/dist/adapters/pdfFormat.d.ts +23 -0
  31. package/dist/adapters/pdfFormat.js +204 -0
  32. package/dist/adapters/portableDocxAtoms.d.ts +4 -0
  33. package/dist/adapters/portableDocxAtoms.js +58 -0
  34. package/dist/adapters/styledDocxFormat.d.ts +8 -0
  35. package/dist/adapters/styledDocxFormat.js +190 -0
  36. package/dist/builtInFormatDefinitions.d.ts +5 -0
  37. package/dist/builtInFormatDefinitions.js +82 -0
  38. package/dist/canonicalAuthorityFlag.d.ts +32 -0
  39. package/dist/canonicalAuthorityFlag.js +49 -0
  40. package/dist/canonicalClipboardRuntime.d.ts +15 -0
  41. package/dist/canonicalClipboardRuntime.js +76 -0
  42. package/dist/canonicalEditorRuntime.d.ts +77 -0
  43. package/dist/canonicalEditorRuntime.js +274 -0
  44. package/dist/components/CanonicalAuthorityEditor.d.ts +25 -0
  45. package/dist/components/CanonicalAuthorityEditor.js +518 -0
  46. package/dist/components/ClassicEditor.d.ts +13 -3
  47. package/dist/components/ClassicEditor.js +1309 -1159
  48. package/dist/components/ClassicEditorAuthority.d.ts +38 -0
  49. package/dist/components/ClassicEditorAuthority.js +49 -0
  50. package/dist/components/MediaPicker.d.ts +10 -0
  51. package/dist/components/MediaPicker.js +16 -0
  52. package/dist/editorController.d.ts +78 -0
  53. package/dist/editorController.js +298 -0
  54. package/dist/formatFidelity.d.ts +14 -0
  55. package/dist/formatFidelity.js +107 -0
  56. package/dist/formatRuntime.d.ts +50 -0
  57. package/dist/formatRuntime.js +20 -0
  58. package/dist/index.d.ts +24 -0
  59. package/dist/index.js +10 -0
  60. package/dist/mediaProvider.d.ts +39 -0
  61. package/dist/mediaProvider.js +1 -0
  62. package/dist/pluginRuntime.d.ts +68 -0
  63. package/dist/pluginRuntime.js +78 -0
  64. package/dist/standalone/editor.js +722 -495
  65. package/dist/test-harness/atomShadowComparator.d.ts +14 -0
  66. package/dist/test-harness/atomShadowComparator.js +63 -0
  67. package/dist/test-harness/blockShadowComparator.d.ts +30 -0
  68. package/dist/test-harness/blockShadowComparator.js +140 -0
  69. package/dist/test-harness/clipboardShadowComparator.d.ts +12 -0
  70. package/dist/test-harness/clipboardShadowComparator.js +46 -0
  71. package/dist/test-harness/inlineShadowComparator.d.ts +32 -0
  72. package/dist/test-harness/inlineShadowComparator.js +152 -0
  73. package/dist/test-harness/legacyAtomEngine.d.ts +10 -0
  74. package/dist/test-harness/legacyAtomEngine.js +15 -0
  75. package/dist/test-harness/legacyBlockEngine.d.ts +31 -0
  76. package/dist/test-harness/legacyBlockEngine.js +31 -0
  77. package/dist/test-harness/legacyClipboardEngine.d.ts +14 -0
  78. package/dist/test-harness/legacyClipboardEngine.js +67 -0
  79. package/dist/test-harness/legacyInlineEngine.d.ts +19 -0
  80. package/dist/test-harness/legacyInlineEngine.js +49 -0
  81. package/dist/test-harness/legacyTableEngine.d.ts +12 -0
  82. package/dist/test-harness/legacyTableEngine.js +25 -0
  83. package/dist/test-harness/tableShadowComparator.d.ts +29 -0
  84. package/dist/test-harness/tableShadowComparator.js +101 -0
  85. package/dist/theme.d.ts +1 -1
  86. package/dist/theme.js +28 -6
  87. package/package.json +1 -1
@@ -2,16 +2,19 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
2
2
  import { useEffect, useRef, useState } from "react";
3
3
  import { MediaManager } from "./MediaManager.js";
4
4
  import { LinkEditorPopover } from "./LinkEditorPopover.js";
5
- import * as pdfjsLib from 'pdfjs-dist';
6
- import mammoth from 'mammoth';
7
- import JSZip from 'jszip';
8
- import { applyLink, applyTextColor as coreApplyTextColor, markdownToCompatibilityHtml, removeLink, sanitizeLinkHref, toggleBold, toggleItalic, toggleSubscript, toggleSuperscript, toggleUnderline } from 'smartrte-core';
5
+ import { applyLink, applyTextColor as coreApplyTextColor, getSmartListPreset, isSmartListPreset, listStyleForPresetDepth, removeLink, sanitizeLinkHref, SMART_LIST_PRESETS, toggleBold, toggleItalic, toggleSubscript, toggleSuperscript, toggleUnderline } from 'smartrte-core';
6
+ import { executeDomMoveCommand } from '../adapters/domMoveCommandBridge.js';
7
+ import { adjacentInlineAtom } from '../adapters/domInlineAtomCommandBridge.js';
9
8
  import { restoreSelectionToDom, selectionFromDom } from '../adapters/domSelectionBridge.js';
10
9
  import { serializeSmartDocument, smartDocumentFromEditorRoot } from '../adapters/domSmartDocument.js';
11
10
  import { isShadowModeEnabled, runShadowCommand } from '../adapters/shadowMode.js';
12
11
  import { getCoreInlineMarkResult, isCoreInlineMarkEnabled } from '../adapters/inlineMarkCoreExecution.js';
13
12
  import { closestFromTarget, isNode } from '../adapters/domTargets.js';
14
13
  import { ensureStyleSheet } from '../theme.js';
14
+ import { createReactEditorPluginRuntime, matchesPluginShortcut } from '../pluginRuntime.js';
15
+ import { createEditorFormatRuntime } from '../formatRuntime.js';
16
+ import { createBuiltInFormatDefinitions } from '../builtInFormatDefinitions.js';
17
+ import { createDomEditorController } from '../editorController.js';
15
18
  const iconPaths = {
16
19
  undo: _jsxs(_Fragment, { children: [_jsx("path", { d: "M9 7 5 11l4 4" }), _jsx("path", { d: "M5 11h7a5 5 0 0 1 5 5" })] }),
17
20
  redo: _jsxs(_Fragment, { children: [_jsx("path", { d: "m15 7 4 4-4 4" }), _jsx("path", { d: "M19 11h-7a5 5 0 0 0-5 5" })] }),
@@ -78,11 +81,7 @@ function ToolbarMenu({ label, icon, active, priority, children }) {
78
81
  function MenuItem({ icon, label, active, disabled, onClick, title }) {
79
82
  return _jsxs("button", { type: "button", role: "menuitem", className: "srte-menu-item", "aria-label": title || label, "aria-pressed": Boolean(active), "aria-checked": active || undefined, disabled: disabled, title: title || label, onClick: (event) => { onClick(); event.currentTarget.closest("details")?.removeAttribute("open"); }, children: [_jsx(ToolbarIcon, { name: icon }), _jsx("span", { children: label }), active && _jsx("span", { className: "srte-menu-check", "aria-hidden": "true", children: "\u2713" })] });
80
83
  }
81
- // Initialize PDF.js worker
82
- if (typeof window !== 'undefined') {
83
- pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
84
- }
85
- export function ClassicEditor({ value, onChange, placeholder = "Type here…", minHeight = 200, maxHeight = 500, readOnly = false, table = true, media = true, formula = true, mediaManager, fonts = [
84
+ export function ClassicEditor({ value, onChange, placeholder = "Type here…", minHeight = 200, maxHeight = 500, readOnly = false, table: legacyTable = true, media: legacyMedia = true, formula: legacyFormula = true, features, plugins, formats, formatDefinitions, mediaManager, fonts = [
86
85
  { name: "Arial", value: "Arial, Helvetica, sans-serif" },
87
86
  { name: "Georgia", value: "Georgia, serif" },
88
87
  { name: "Impact", value: "Impact, Charcoal, sans-serif" },
@@ -92,18 +91,50 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
92
91
  { name: "Courier New", value: "'Courier New', Courier, monospace" },
93
92
  ], defaultFont, preserveFontFamily = false, preserveColors = false, preserveDocxStyles = true, theme = "light", showFontSize = true, className, }) {
94
93
  ensureStyleSheet();
94
+ const pluginRuntime = createReactEditorPluginRuntime({
95
+ ...(plugins ? { plugins } : {
96
+ features: {
97
+ ...features,
98
+ table: features?.table ?? legacyTable,
99
+ media: features?.media ?? legacyMedia,
100
+ formula: features?.formula ?? legacyFormula,
101
+ },
102
+ }),
103
+ });
104
+ const formatRuntime = createEditorFormatRuntime(formats, [
105
+ ...(formatDefinitions ?? createBuiltInFormatDefinitions({ preserveDocxStyles })),
106
+ ...pluginRuntime.formats,
107
+ ]);
108
+ const proxyFormatLabel = (format) => format.id === "docx" ? "Word" : format.label;
109
+ const proxyExportFormats = [
110
+ ...["html", "markdown", "docx", "pdf"].flatMap((id) => formatRuntime.exports.filter((format) => format.id === id)),
111
+ ...formatRuntime.exports.filter((format) => !["html", "markdown", "docx", "pdf"].includes(format.id)),
112
+ ];
113
+ const table = pluginRuntime.hasFeature("table");
114
+ const media = pluginRuntime.hasFeature("media");
115
+ const formula = pluginRuntime.hasFeature("formula");
116
+ const listFeature = pluginRuntime.hasFeature("list");
117
+ const checklistFeature = pluginRuntime.hasFeature("checklist");
118
+ const blockquoteFeature = pluginRuntime.hasFeature("blockquote");
119
+ const codeBlockFeature = pluginRuntime.hasFeature("codeBlock");
120
+ const alignmentFeature = pluginRuntime.hasFeature("alignment");
121
+ const blockTypeFeature = pluginRuntime.hasFeature("blockType");
122
+ const basicFormattingFeature = pluginRuntime.hasFeature("basicFormatting");
123
+ const moveFeature = pluginRuntime.hasFeature("move");
95
124
  const editableRef = useRef(null);
125
+ const editorControllerRef = useRef(null);
126
+ if (!editorControllerRef.current)
127
+ editorControllerRef.current = createDomEditorController();
128
+ editorControllerRef.current.configure({
129
+ plugins: pluginRuntime.plugins,
130
+ readOnly,
131
+ });
96
132
  const editorScrollRef = useRef(null);
97
133
  const lastEmittedRef = useRef("");
98
134
  const isComposingRef = useRef(false);
99
135
  const fileInputRef = useRef(null);
100
- const pdfInputRef = useRef(null);
101
- const docxInputRef = useRef(null);
102
- const htmlInputRef = useRef(null);
103
- const mdInputRef = useRef(null);
104
136
  const [loadingPdf, setLoadingPdf] = useState(false);
105
137
  const [loadingDocx, setLoadingDocx] = useState(false);
106
- // State for import confirmation
107
138
  const [pendingImport, setPendingImport] = useState(null);
108
139
  const replaceTargetRef = useRef(null);
109
140
  const [selectedImage, setSelectedImage] = useState(null);
@@ -121,8 +152,10 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
121
152
  const [formulaInput, setFormulaInput] = useState("E=mc^2");
122
153
  const [tableMenu, setTableMenu] = useState(null);
123
154
  const selectionRef = useRef(null);
155
+ const tableFillTargetsRef = useRef(null);
124
156
  const selectingRef = useRef(null);
125
157
  const [imageMenu, setImageMenu] = useState(null);
158
+ const [pluginContextMenu, setPluginContextMenu] = useState(null);
126
159
  const [linkMenu, setLinkMenu] = useState(null);
127
160
  const [showMediaManager, setShowMediaManager] = useState(false);
128
161
  const [showColorPicker, setShowColorPicker] = useState(false);
@@ -131,15 +164,13 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
131
164
  const savedRangeRef = useRef(null);
132
165
  const pendingFontSizeRef = useRef(null);
133
166
  const inlineScriptCaretOverrideRef = useRef(null);
134
- const historyRef = useRef({
135
- undo: [],
136
- redo: [],
137
- });
138
167
  const inputHistoryGroupRef = useRef(null);
139
168
  const [currentFontSize, setCurrentFontSize] = useState("");
140
169
  const [currentFont, setCurrentFont] = useState("");
141
170
  const [currentBlockType, setCurrentBlockType] = useState("p");
142
171
  const [currentAlignment, setCurrentAlignment] = useState("left");
172
+ const [currentTextColor, setCurrentTextColor] = useState("#000000");
173
+ const [currentBackgroundColor, setCurrentBackgroundColor] = useState("#ffffff");
143
174
  const [activeState, setActiveState] = useState({
144
175
  bold: false,
145
176
  italic: false,
@@ -164,6 +195,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
164
195
  fixNegativeMargins(el);
165
196
  normalizeInvalidQuoteNesting(el);
166
197
  normalizeInvalidCodeBlockNesting(el);
198
+ normalizeBlockContainerStyling(el);
167
199
  normalizeInvalidTableNesting(el);
168
200
  ensureTableWrappers(el);
169
201
  ensureCaretBoundaryParagraphs(el);
@@ -171,6 +203,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
171
203
  }
172
204
  normalizeInvalidQuoteNesting(el);
173
205
  normalizeInvalidCodeBlockNesting(el);
206
+ normalizeBlockContainerStyling(el);
174
207
  normalizeInvalidTableNesting(el);
175
208
  ensureCaretBoundaryParagraphs(el);
176
209
  // Suppress native context menu inside table cells at capture phase
@@ -274,8 +307,19 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
274
307
  if (!inEditor)
275
308
  return;
276
309
  let node = range.commonAncestorContainer;
277
- if (node.nodeType === Node.TEXT_NODE)
310
+ if (node.nodeType === Node.TEXT_NODE) {
278
311
  node = node.parentNode;
312
+ }
313
+ else if (range.collapsed && node instanceof HTMLElement) {
314
+ // A caret collapsed "between children" of a container (e.g. left
315
+ // there by focusElementEnd(list) after unwrapping a blockquote)
316
+ // must resolve down into that container's actual content, or every
317
+ // active-state indicator below silently reports "not active" even
318
+ // though the caret is sitting right next to a blockquote/pre.
319
+ node = resolveCollapsedElementPosition(node, range.startOffset);
320
+ if (node.nodeType === Node.TEXT_NODE)
321
+ node = node.parentNode;
322
+ }
279
323
  const element = node instanceof HTMLElement ? node : null;
280
324
  const block = element?.closest("p,h1,h2,h3,h4,h5,h6,li,blockquote,pre,div");
281
325
  const tag = block?.tagName.toLowerCase();
@@ -312,6 +356,9 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
312
356
  setCurrentBlockType(/^h[1-6]$/.test(tag || "") ? tag : "p");
313
357
  }
314
358
  setCurrentFontSize(resolveFontSizeForRange(range));
359
+ const colorTarget = getRangeStartElement(range) || editor;
360
+ setCurrentTextColor(cssColorToHex(window.getComputedStyle(colorTarget).color) || "#000000");
361
+ setCurrentBackgroundColor(getEffectiveBackgroundHex(colorTarget) || "#ffffff");
315
362
  const alignmentTargets = getAlignmentTargets(range);
316
363
  const alignments = new Set(alignmentTargets.map(readTextAlignment));
317
364
  setCurrentAlignment(alignments.size > 1 ? "mixed" : Array.from(alignments)[0] || "left");
@@ -576,13 +623,39 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
576
623
  return () => editor.removeEventListener("beforeinput", applyPendingInlineScript);
577
624
  }, []);
578
625
  const applyFormatBlock = (blockName) => {
626
+ if (!blockTypeFeature)
627
+ return;
579
628
  try {
580
629
  if (!restoreSavedSelection()) {
581
630
  safeSelectRange(getSelectionRangeInEditor());
582
631
  }
583
632
  pushEditorHistory();
633
+ const range = getSelectionRangeInEditor();
634
+ const tag = normalizeBlockTag(blockName);
635
+ const blocks = range
636
+ ? (range.collapsed
637
+ ? [getCurrentBlock()].filter((block) => Boolean(block))
638
+ : getSelectedBlocks(range))
639
+ : [];
640
+ const coreBlocks = blocks.filter((block) => block.matches("p,h1,h2,h3,h4,h5,h6") && !block.closest("li,td,th"));
641
+ if (coreBlocks.length === blocks.length &&
642
+ coreBlocks.length > 0 &&
643
+ (tag === "p" || /^h[1-6]$/.test(tag || ""))) {
644
+ const replacements = editorControllerRef.current.bindRoot(editableRef.current).executeBlockCommand(coreBlocks, {
645
+ id: "block-type.set",
646
+ input: tag === "p"
647
+ ? { type: "paragraph" }
648
+ : { type: "heading", level: Number(tag.slice(1)) },
649
+ });
650
+ if (replacements) {
651
+ focusElementEnd(replacements[replacements.length - 1]);
652
+ setCurrentBlockType(tag);
653
+ handleInput();
654
+ requestAnimationFrame(updateActiveState);
655
+ return;
656
+ }
657
+ }
584
658
  if (applyFormatBlockFallback(blockName)) {
585
- const tag = normalizeBlockTag(blockName);
586
659
  if (tag === "p" || /^h[1-6]$/.test(tag || "")) {
587
660
  setCurrentBlockType(tag);
588
661
  }
@@ -631,37 +704,19 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
631
704
  cell.style.outline = outline;
632
705
  cell.style.outlineOffset = outlineOffset;
633
706
  });
634
- const undo = historyRef.current.undo;
635
- if (undo[undo.length - 1] !== html) {
636
- undo.push(html);
637
- if (undo.length > 100)
638
- undo.shift();
639
- }
640
- historyRef.current.redo = [];
707
+ editorControllerRef.current.bindRoot(editor).recordHistorySnapshot(html);
641
708
  };
642
709
  const restoreEditorHistory = (dir) => {
643
710
  const editor = editableRef.current;
644
711
  if (!editor)
645
712
  return false;
646
- const history = historyRef.current;
647
- const from = dir === "undo" ? history.undo : history.redo;
648
- const to = dir === "undo" ? history.redo : history.undo;
649
- const currentHtml = editor.innerHTML;
650
- let html;
651
- while (from.length > 0) {
652
- const candidate = from.pop();
653
- if (candidate !== currentHtml) {
654
- html = candidate;
655
- break;
656
- }
657
- }
658
- if (html == null)
713
+ const restored = editorControllerRef.current.bindRoot(editor).restoreHistory(dir);
714
+ if (!restored)
659
715
  return false;
660
- if (to[to.length - 1] !== currentHtml)
661
- to.push(currentHtml);
662
- editor.innerHTML = html;
716
+ const html = restored.html;
663
717
  fixNegativeMargins(editor);
664
718
  normalizeInvalidCodeBlockNesting(editor);
719
+ normalizeBlockContainerStyling(editor);
665
720
  ensureTableWrappers(editor);
666
721
  ensureCaretBoundaryParagraphs(editor);
667
722
  addTableResizeHandles();
@@ -844,14 +899,35 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
844
899
  range.collapse(false);
845
900
  return range;
846
901
  };
902
+ // A collapsed range positioned directly on an element (e.g. after
903
+ // focusElementEnd collapses "inside" a blockquote rather than into one of
904
+ // its text nodes) sits between that element's children, not inside any of
905
+ // them. Resolve down to the actual child content at that position so a
906
+ // container like blockquote/li/ul/ol/td/th is never mistaken for the
907
+ // current block itself.
908
+ const resolveCollapsedElementPosition = (element, offset) => {
909
+ let node = element.childNodes[offset] ?? element.childNodes[offset - 1] ?? element;
910
+ while (node instanceof HTMLElement &&
911
+ node.matches("blockquote,li,ul,ol,td,th") &&
912
+ node.childNodes.length) {
913
+ node = node.lastChild;
914
+ }
915
+ return node;
916
+ };
847
917
  const getCurrentBlock = () => {
848
918
  const editor = editableRef.current;
849
919
  const range = getSelectionRangeInEditor();
850
920
  if (!editor || !range)
851
921
  return null;
852
922
  let node = range.startContainer;
853
- if (node.nodeType === Node.TEXT_NODE)
923
+ if (node.nodeType === Node.TEXT_NODE) {
854
924
  node = node.parentNode;
925
+ }
926
+ else if (node instanceof HTMLElement) {
927
+ node = resolveCollapsedElementPosition(node, range.startOffset);
928
+ if (node.nodeType === Node.TEXT_NODE)
929
+ node = node.parentNode;
930
+ }
855
931
  const element = node instanceof HTMLElement ? node : null;
856
932
  const cell = element?.closest("td,th");
857
933
  if (cell && editor.contains(cell)) {
@@ -869,6 +945,22 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
869
945
  return block;
870
946
  };
871
947
  const blockSelector = "p,h1,h2,h3,h4,h5,h6,li,blockquote,pre,div";
948
+ // Finds the nearest blockSelector ancestor without leaping over an
949
+ // intervening ul/ol/blockquote: those are containers, not interchangeable
950
+ // wrappers, so a candidate nested inside one is never mistaken for being
951
+ // owned by a distant container beyond it (e.g. a blockquote wrapping the
952
+ // whole document, or a list several levels up).
953
+ const findOwningBlock = (start) => {
954
+ let node = start;
955
+ while (node) {
956
+ if (node.matches("ul,ol,blockquote"))
957
+ return null;
958
+ if (node.matches(blockSelector))
959
+ return node;
960
+ node = node.parentElement;
961
+ }
962
+ return null;
963
+ };
872
964
  const getSelectedBlocks = (range) => {
873
965
  const editor = editableRef.current;
874
966
  if (!editor)
@@ -882,32 +974,50 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
882
974
  const startCell = startElement?.closest?.("td,th");
883
975
  const endCell = endElement?.closest?.("td,th");
884
976
  const scope = startCell && startCell === endCell && editor.contains(startCell) ? startCell : editor;
885
- const blocks = Array.from(scope.querySelectorAll(blockSelector)).filter((block) => {
977
+ const intersects = (node) => {
978
+ try {
979
+ return range.intersectsNode(node);
980
+ }
981
+ catch {
982
+ return false;
983
+ }
984
+ };
985
+ const candidates = Array.from(scope.querySelectorAll(blockSelector)).filter((block) => {
886
986
  if (block === editor || block.getAttribute("data-table-wrapper") === "true")
887
987
  return false;
888
- const parentBlock = block.parentElement?.closest(blockSelector);
889
- if (parentBlock && scope.contains(parentBlock) && parentBlock !== scope)
988
+ // Caret-boundary spacer paragraphs exist only to give the caret a click
989
+ // target next to a blockquote/pre/table; they aren't real content and
990
+ // must never be swept into a broader selection's block set.
991
+ if (block.getAttribute("data-srte-caret-boundary") === "true")
890
992
  return false;
891
- try {
892
- if (!range.intersectsNode(block))
993
+ if (!intersects(block))
994
+ return false;
995
+ const parent = block.parentNode;
996
+ if (parent === range.endContainer) {
997
+ const index = Array.prototype.indexOf.call(parent.childNodes, block);
998
+ if (range.endOffset <= index)
893
999
  return false;
894
- const parent = block.parentNode;
895
- if (parent === range.endContainer) {
896
- const index = Array.prototype.indexOf.call(parent.childNodes, block);
897
- if (range.endOffset <= index)
898
- return false;
899
- }
900
- if (parent === range.startContainer) {
901
- const index = Array.prototype.indexOf.call(parent.childNodes, block);
902
- if (range.startOffset > index)
903
- return false;
904
- }
905
- return true;
906
1000
  }
907
- catch {
908
- return false;
1001
+ if (parent === range.startContainer) {
1002
+ const index = Array.prototype.indexOf.call(parent.childNodes, block);
1003
+ if (range.startOffset > index)
1004
+ return false;
909
1005
  }
1006
+ return true;
1007
+ });
1008
+ // Prefer a candidate's owning block (e.g. an li wrapping a single p) only
1009
+ // when that owner has no other content outside the selection — otherwise
1010
+ // the owner is too broad (a container spanning many unrelated lines, or
1011
+ // the whole document) and the more specific candidate stands on its own.
1012
+ const stage1 = candidates.filter((block) => {
1013
+ const parentBlock = findOwningBlock(block.parentElement);
1014
+ if (!parentBlock || !scope.contains(parentBlock) || parentBlock === scope)
1015
+ return true;
1016
+ return Array.from(parentBlock.querySelectorAll(blockSelector)).some((sibling) => sibling !== block && findOwningBlock(sibling.parentElement) === parentBlock && !intersects(sibling));
910
1017
  });
1018
+ // Drop any survivor that only qualifies because a more specific
1019
+ // descendant survivor is nested inside it.
1020
+ const blocks = stage1.filter((block) => !stage1.some((other) => other !== block && block.contains(other)));
911
1021
  if (blocks.length > 0)
912
1022
  return sortInDocumentOrder(blocks);
913
1023
  const current = getCurrentBlock();
@@ -918,9 +1028,15 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
918
1028
  if (!editor)
919
1029
  return [];
920
1030
  if (range.collapsed) {
921
- const element = range.startContainer instanceof HTMLElement
922
- ? range.startContainer
923
- : range.startContainer.parentElement;
1031
+ // A caret left positioned "between children" of a container (e.g. via
1032
+ // focusElementEnd(ul) after unwrapping a blockquote) needs the same
1033
+ // drill-down as getCurrentBlock, or a list item is missed entirely
1034
+ // whenever the collapsed point sits on the list/blockquote itself
1035
+ // rather than inside one of its descendant text nodes.
1036
+ let node = range.startContainer;
1037
+ if (node instanceof HTMLElement)
1038
+ node = resolveCollapsedElementPosition(node, range.startOffset);
1039
+ const element = node instanceof HTMLElement ? node : node.parentElement;
924
1040
  const item = element?.closest("li");
925
1041
  return item && editor.contains(item) ? [item] : [];
926
1042
  }
@@ -936,6 +1052,22 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
936
1052
  });
937
1053
  return sortInDocumentOrder(Array.from(editor.querySelectorAll("li")).filter(ownsSelectedContent));
938
1054
  };
1055
+ const resolveCommonSelectedList = (items) => {
1056
+ if (items.length === 0)
1057
+ return null;
1058
+ const listChain = (item) => {
1059
+ const lists = [];
1060
+ let list = item.parentElement?.matches("ul,ol") ? item.parentElement : null;
1061
+ while (list) {
1062
+ lists.push(list);
1063
+ const ownerItem = list.parentElement?.closest("li");
1064
+ list = ownerItem?.parentElement?.matches("ul,ol") ? ownerItem.parentElement : null;
1065
+ }
1066
+ return lists;
1067
+ };
1068
+ const chains = items.map(listChain);
1069
+ return chains[0].find((candidate) => chains.every((chain) => chain.includes(candidate))) || null;
1070
+ };
939
1071
  const getAlignmentTarget = (block) => {
940
1072
  const item = block.tagName === "LI" ? block : block.closest("li");
941
1073
  return item instanceof HTMLElement ? item : block;
@@ -975,6 +1107,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
975
1107
  : "left";
976
1108
  };
977
1109
  const applyTextAlignment = (alignment) => {
1110
+ if (!alignmentFeature)
1111
+ return;
978
1112
  try {
979
1113
  if (!restoreSavedSelection())
980
1114
  safeSelectRange(getSelectionRangeInEditor());
@@ -985,6 +1119,26 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
985
1119
  if (targets.length === 0)
986
1120
  return;
987
1121
  pushEditorHistory();
1122
+ const coreTargets = targets.filter((target) => target.matches("p,h1,h2,h3,h4,h5,h6,blockquote,pre") &&
1123
+ !target.closest("li"));
1124
+ if (coreTargets.length === targets.length) {
1125
+ const groups = new Map();
1126
+ coreTargets.forEach((target) => {
1127
+ const parent = target.parentElement;
1128
+ if (parent)
1129
+ groups.set(parent, [...(groups.get(parent) || []), target]);
1130
+ });
1131
+ const replacements = Array.from(groups.values()).flatMap((group) => editorControllerRef.current.bindRoot(editableRef.current).executeBlockCommand(group, {
1132
+ id: "alignment.set",
1133
+ input: { alignment },
1134
+ }) || []);
1135
+ if (replacements.length === coreTargets.length) {
1136
+ focusElementEnd(replacements[replacements.length - 1]);
1137
+ handleInput();
1138
+ requestAnimationFrame(updateActiveState);
1139
+ return;
1140
+ }
1141
+ }
988
1142
  targets.forEach((target) => {
989
1143
  target.style.textAlign = alignment === "left" ? "" : alignment;
990
1144
  if (!target.getAttribute("style"))
@@ -1207,6 +1361,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1207
1361
  const isCompatible = (candidate) => candidate instanceof HTMLElement &&
1208
1362
  candidate.tagName === list.tagName &&
1209
1363
  candidate.style.listStyleType === list.style.listStyleType &&
1364
+ candidate.dataset.srteListPreset === list.dataset.srteListPreset &&
1365
+ candidate.dataset.srteListDepth === list.dataset.srteListDepth &&
1210
1366
  candidate.dataset.srteChecklist === list.dataset.srteChecklist &&
1211
1367
  candidate.dataset.srteChecklistStrike === list.dataset.srteChecklistStrike;
1212
1368
  let merged = list;
@@ -1332,6 +1488,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1332
1488
  list.parentElement?.replaceChild(replacement, list);
1333
1489
  const li = replacement.querySelector("li");
1334
1490
  focusElementEnd(li || replacement);
1491
+ return replacement;
1335
1492
  };
1336
1493
  const getOrCreateNestedList = (item, source) => {
1337
1494
  const compatible = Array.from(item.children).find((child) => child instanceof HTMLElement &&
@@ -1464,9 +1621,28 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1464
1621
  return list?.parentElement?.tagName === "LI" && ["UL", "OL"].includes(list.parentElement.parentElement?.tagName || "");
1465
1622
  });
1466
1623
  const applyListDepthChange = (items, direction) => {
1624
+ if (!listFeature)
1625
+ return false;
1467
1626
  const enabled = direction === "indent" ? canIndentListItems(items) : canOutdentListItems(items);
1468
1627
  if (!enabled)
1469
1628
  return false;
1629
+ const editor = editableRef.current;
1630
+ if (editor && items.every((item) => !item.closest("td,th,[data-srte-checklist='true']"))) {
1631
+ const beforeCoreHtml = editor.innerHTML;
1632
+ const result = editorControllerRef.current
1633
+ .bindRoot(editor)
1634
+ .execute(direction === "indent" ? "list.indent" : "list.outdent");
1635
+ if (result) {
1636
+ inputHistoryGroupRef.current = null;
1637
+ editorControllerRef.current.bindRoot(editor).recordHistorySnapshot(beforeCoreHtml);
1638
+ const selection = window.getSelection();
1639
+ if (selection?.rangeCount)
1640
+ savedRangeRef.current = selection.getRangeAt(0).cloneRange();
1641
+ handleInput();
1642
+ requestAnimationFrame(updateActiveState);
1643
+ return true;
1644
+ }
1645
+ }
1470
1646
  pushEditorHistory();
1471
1647
  const changed = direction === "indent"
1472
1648
  ? nestSelectedListItems(items)
@@ -1521,6 +1697,86 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1521
1697
  });
1522
1698
  return lastSelected;
1523
1699
  };
1700
+ const listStylesForRoot = (listTag, rootStyle) => {
1701
+ if (listTag === "ul") {
1702
+ if (rootStyle === "circle")
1703
+ return ["circle", "square", "disc"];
1704
+ if (rootStyle === "square")
1705
+ return ["square", "circle", "disc"];
1706
+ return ["disc", "circle", "square"];
1707
+ }
1708
+ if (rootStyle === "upper-alpha")
1709
+ return ["upper-alpha", "lower-alpha", "lower-roman"];
1710
+ if (rootStyle === "upper-roman")
1711
+ return ["upper-roman", "upper-alpha", "decimal"];
1712
+ // Legacy single-style choices remain stable across the first nested level.
1713
+ // Depth-aware families are represented by named presets above.
1714
+ if (rootStyle === "lower-alpha")
1715
+ return ["lower-alpha", "lower-alpha", "lower-roman"];
1716
+ if (rootStyle === "lower-roman")
1717
+ return ["lower-roman", "lower-roman", "lower-alpha"];
1718
+ if (rootStyle === "decimal-leading-zero")
1719
+ return ["decimal-leading-zero", "lower-alpha", "lower-roman"];
1720
+ return ["decimal", "lower-alpha", "lower-roman"];
1721
+ };
1722
+ const clearListPreset = (list) => {
1723
+ delete list.dataset.srteListPreset;
1724
+ delete list.dataset.srteListDepth;
1725
+ delete list.dataset.srteListMarker;
1726
+ };
1727
+ const applyListPreset = (list, preset, depth) => {
1728
+ const definition = getSmartListPreset(preset);
1729
+ list.dataset.srteListPreset = preset;
1730
+ list.dataset.srteListDepth = String(depth);
1731
+ list.style.listStyleType = listStyleForPresetDepth(preset, depth);
1732
+ const marker = definition.markers?.[Math.min(depth, definition.markers.length - 1)];
1733
+ if (marker)
1734
+ list.dataset.srteListMarker = marker;
1735
+ else
1736
+ delete list.dataset.srteListMarker;
1737
+ };
1738
+ const applyListPresetHierarchy = (list, preset, depth = 0) => {
1739
+ applyListPreset(list, preset, depth);
1740
+ Array.from(list.children).forEach((item) => {
1741
+ if (!(item instanceof HTMLElement) || item.tagName !== "LI")
1742
+ return;
1743
+ Array.from(item.children).forEach((child) => {
1744
+ if (child instanceof HTMLElement && child.matches("ul,ol")) {
1745
+ applyListPresetHierarchy(child, preset, depth + 1);
1746
+ }
1747
+ });
1748
+ });
1749
+ };
1750
+ const defaultListStyleAtDepth = (listTag, depth, rootStyle) => {
1751
+ const styles = listStylesForRoot(listTag, rootStyle);
1752
+ return styles[Math.min(depth, styles.length - 1)];
1753
+ };
1754
+ const restyleDescendantLists = (item, listTag, depth = 1, rootStyle, preset) => {
1755
+ Array.from(item.children).forEach((child) => {
1756
+ if (!(child instanceof HTMLElement) || !child.matches("ul,ol"))
1757
+ return;
1758
+ const replacement = child.tagName.toLowerCase() === listTag
1759
+ ? child
1760
+ : cloneListShell(child, listTag);
1761
+ if (replacement !== child) {
1762
+ while (child.firstChild)
1763
+ replacement.appendChild(child.firstChild);
1764
+ child.replaceWith(replacement);
1765
+ }
1766
+ clearChecklist(replacement);
1767
+ if (preset)
1768
+ applyListPreset(replacement, preset, depth);
1769
+ else {
1770
+ clearListPreset(replacement);
1771
+ replacement.style.listStyleType = defaultListStyleAtDepth(listTag, depth, rootStyle);
1772
+ }
1773
+ Array.from(replacement.children).forEach((nestedItem) => {
1774
+ if (nestedItem instanceof HTMLElement && nestedItem.tagName === "LI") {
1775
+ restyleDescendantLists(nestedItem, listTag, depth + 1, rootStyle, preset);
1776
+ }
1777
+ });
1778
+ });
1779
+ };
1524
1780
  const getDirectSelectionCell = (range) => {
1525
1781
  const startCell = getClosestCell(range.startContainer);
1526
1782
  const endCell = getClosestCell(range.endContainer);
@@ -1582,8 +1838,15 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1582
1838
  return convertDirectTableCellSelectionToList(range, listTag);
1583
1839
  };
1584
1840
  const applyListStyle = (value) => {
1585
- const listTag = value.startsWith("ordered:") ? "ol" : "ul";
1586
- const styleType = value.replace(/^(ordered|bullet):/, "");
1841
+ if (!listFeature)
1842
+ return;
1843
+ const listTag = value.startsWith("ordered:") || value.startsWith("ordered-preset:") ? "ol" : "ul";
1844
+ const requestedPreset = value.replace(/^(ordered|bullet)-preset:/, "");
1845
+ const preset = isSmartListPreset(requestedPreset) ? requestedPreset : undefined;
1846
+ const styleType = preset
1847
+ ? listStyleForPresetDepth(preset, 0)
1848
+ : value.replace(/^(ordered|bullet):/, "");
1849
+ normalizeListNesting(editableRef.current);
1587
1850
  if (!restoreSavedSelection())
1588
1851
  safeSelectRange(getSelectionRangeInEditor());
1589
1852
  const range = getSelectionRangeInEditor();
@@ -1594,7 +1857,10 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1594
1857
  pushEditorHistory();
1595
1858
  const list = convertDirectTableCellContentsToList(currentBlock, listTag);
1596
1859
  if (list) {
1597
- list.style.listStyleType = styleType;
1860
+ if (preset)
1861
+ applyListPreset(list, preset, 0);
1862
+ else
1863
+ list.style.listStyleType = styleType;
1598
1864
  handleInput();
1599
1865
  requestAnimationFrame(updateActiveState);
1600
1866
  }
@@ -1604,7 +1870,10 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1604
1870
  pushEditorHistory();
1605
1871
  const list = convertDirectTableCellSelectionToList(range, listTag);
1606
1872
  if (list) {
1607
- list.style.listStyleType = styleType;
1873
+ if (preset)
1874
+ applyListPreset(list, preset, 0);
1875
+ else
1876
+ list.style.listStyleType = styleType;
1608
1877
  handleInput();
1609
1878
  requestAnimationFrame(updateActiveState);
1610
1879
  }
@@ -1614,8 +1883,52 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1614
1883
  const selectedItems = resolveSelectedListItems(range);
1615
1884
  const selectedPlainBlocks = selectedBlocks.filter((block) => !block.closest("ul,ol"));
1616
1885
  if (selectedItems.length > 0) {
1886
+ const outermostItems = selectedItems.filter((item) => !selectedItems.some((candidate) => candidate !== item && candidate.contains(item)));
1887
+ const commonSelectedList = resolveCommonSelectedList(selectedItems);
1888
+ const promotedLists = new Set(selectedPlainBlocks.length > 0
1889
+ ? outermostItems.map((item) => {
1890
+ let list = item.parentElement?.matches("ul,ol") ? item.parentElement : null;
1891
+ while (list?.parentElement?.closest("li")?.parentElement?.matches("ul,ol")) {
1892
+ list = list.parentElement.closest("li").parentElement;
1893
+ }
1894
+ return list;
1895
+ }).filter((list) => Boolean(list))
1896
+ : commonSelectedList ? [commonSelectedList] : []);
1617
1897
  pushEditorHistory();
1618
- const lastItem = restyleSelectedListItems(selectedItems, listTag, styleType);
1898
+ let lastItem = null;
1899
+ if (promotedLists.size > 0) {
1900
+ promotedLists.forEach((source) => {
1901
+ const items = Array.from(source.children).filter((child) => child instanceof HTMLElement && child.tagName === "LI");
1902
+ items.forEach((item) => restyleDescendantLists(item, listTag, 1, styleType, preset));
1903
+ const target = source.tagName.toLowerCase() === listTag
1904
+ ? source
1905
+ : cloneListShell(source, listTag);
1906
+ if (target !== source) {
1907
+ while (source.firstChild)
1908
+ target.appendChild(source.firstChild);
1909
+ source.replaceWith(target);
1910
+ }
1911
+ clearChecklist(target);
1912
+ if (preset)
1913
+ applyListPreset(target, preset, 0);
1914
+ else {
1915
+ clearListPreset(target);
1916
+ target.style.listStyleType = styleType;
1917
+ }
1918
+ lastItem = target.lastElementChild;
1919
+ mergeAdjacentCompatibleLists(target);
1920
+ });
1921
+ }
1922
+ const scopedItems = outermostItems.filter((item) => !Array.from(promotedLists).some((list) => list.contains(item)));
1923
+ scopedItems.forEach((item) => restyleDescendantLists(item, listTag, 1, styleType, preset));
1924
+ lastItem = restyleSelectedListItems(scopedItems, listTag, styleType) || lastItem;
1925
+ if (preset) {
1926
+ scopedItems.forEach((item) => {
1927
+ const list = item.parentElement;
1928
+ if (list?.matches("ul,ol"))
1929
+ applyListPreset(list, preset, 0);
1930
+ });
1931
+ }
1619
1932
  const convertedPlainBlocks = convertSelectedBlocksToList(selectedPlainBlocks, listTag, styleType);
1620
1933
  if (!convertedPlainBlocks && lastItem)
1621
1934
  focusElementEnd(lastItem);
@@ -1649,7 +1962,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1649
1962
  if (convertSelectedBlocksToList(convertibleBlocks, listTag)) {
1650
1963
  const createdList = getCurrentBlock()?.closest("ul,ol");
1651
1964
  if (createdList) {
1652
- createdList.style.listStyleType = styleType;
1965
+ if (preset)
1966
+ applyListPresetHierarchy(createdList, preset);
1967
+ else {
1968
+ clearListPreset(createdList);
1969
+ createdList.style.listStyleType = styleType;
1970
+ }
1653
1971
  const mergedList = mergeAdjacentCompatibleLists(createdList);
1654
1972
  const lastItem = mergedList.lastElementChild;
1655
1973
  if (lastItem)
@@ -1664,7 +1982,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1664
1982
  toggleList(listTag);
1665
1983
  const createdList = getCurrentBlock()?.closest("ul,ol");
1666
1984
  if (createdList) {
1667
- createdList.style.listStyleType = styleType;
1985
+ if (preset)
1986
+ applyListPresetHierarchy(createdList, preset);
1987
+ else {
1988
+ clearListPreset(createdList);
1989
+ createdList.style.listStyleType = styleType;
1990
+ }
1668
1991
  handleInput();
1669
1992
  }
1670
1993
  return;
@@ -1690,7 +2013,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1690
2013
  list.parentElement?.replaceChild(target, list);
1691
2014
  }
1692
2015
  clearChecklist(target);
1693
- target.style.listStyleType = styleType;
2016
+ if (preset)
2017
+ applyListPresetHierarchy(target, preset);
2018
+ else {
2019
+ clearListPreset(target);
2020
+ target.style.listStyleType = styleType;
2021
+ }
1694
2022
  lastList = target;
1695
2023
  styledLists.push(target);
1696
2024
  });
@@ -1706,6 +2034,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1706
2034
  requestAnimationFrame(updateActiveState);
1707
2035
  };
1708
2036
  const applyChecklist = (strikeCompleted = false, toggleOff = false) => {
2037
+ if (!checklistFeature)
2038
+ return;
1709
2039
  if (!restoreSavedSelection())
1710
2040
  safeSelectRange(getSelectionRangeInEditor());
1711
2041
  const range = getSelectionRangeInEditor();
@@ -1713,6 +2043,32 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1713
2043
  return;
1714
2044
  const selectedItems = resolveSelectedListItems(range);
1715
2045
  const selectedChecklists = selectedItems.filter((item) => item.parentElement?.dataset.srteChecklist === "true");
2046
+ const selectedLists = new Set(selectedItems.map((item) => item.parentElement).filter(Boolean));
2047
+ const coversCompleteList = selectedLists.size === 1 &&
2048
+ selectedItems.length === (Array.from(selectedLists)[0]?.children.length || 0);
2049
+ const removingActiveChecklist = toggleOff &&
2050
+ selectedItems.length > 0 &&
2051
+ selectedChecklists.length === selectedItems.length;
2052
+ if (!removingActiveChecklist && (selectedItems.length === 0 || coversCompleteList)) {
2053
+ const editor = editableRef.current;
2054
+ const beforeCoreHtml = editor?.innerHTML || "";
2055
+ const coreResult = editor
2056
+ ? editorControllerRef.current
2057
+ .bindRoot(editor)
2058
+ .execute("checklist.toggle", { strikeCompleted })
2059
+ : null;
2060
+ if (coreResult) {
2061
+ inputHistoryGroupRef.current = null;
2062
+ editorControllerRef.current.bindRoot(editor).recordHistorySnapshot(beforeCoreHtml);
2063
+ syncChecklistControls(editor);
2064
+ const selection = window.getSelection();
2065
+ if (selection?.rangeCount)
2066
+ savedRangeRef.current = selection.getRangeAt(0).cloneRange();
2067
+ handleInput();
2068
+ requestAnimationFrame(updateActiveState);
2069
+ return;
2070
+ }
2071
+ }
1716
2072
  if (toggleOff && selectedItems.length > 0 && selectedChecklists.length === selectedItems.length) {
1717
2073
  pushEditorHistory();
1718
2074
  selectedItems.forEach((item) => {
@@ -1930,9 +2286,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1930
2286
  return changed;
1931
2287
  };
1932
2288
  const toggleList = (listTag) => {
2289
+ if (!listFeature)
2290
+ return;
1933
2291
  const editor = editableRef.current;
1934
2292
  if (!editor)
1935
2293
  return;
2294
+ normalizeListNesting(editor);
1936
2295
  if (!restoreSavedSelection())
1937
2296
  safeSelectRange(getSelectionRangeInEditor());
1938
2297
  const setListActiveState = (active) => {
@@ -1956,10 +2315,41 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1956
2315
  const blocks = getSelectedBlocks(range);
1957
2316
  const selectedListItems = resolveSelectedListItems(range);
1958
2317
  if (selectedListItems.length > 0) {
2318
+ const selectedPlainBlocks = blocks.filter((block) => !block.closest("ul,ol"));
2319
+ const commonSelectedList = resolveCommonSelectedList(selectedListItems);
2320
+ let outermostItems = selectedListItems.filter((item) => !selectedListItems.some((candidate) => candidate !== item && candidate.contains(item)));
1959
2321
  pushEditorHistory();
1960
- if (transformSelectedListItems(selectedListItems, listTag)) {
1961
- const active = Boolean(getCurrentBlock()?.closest(listTag));
1962
- setListActiveState(active);
2322
+ const togglingOff = selectedPlainBlocks.length === 0 &&
2323
+ outermostItems.every((item) => item.parentElement?.tagName.toLowerCase() === listTag);
2324
+ if (togglingOff) {
2325
+ if (transformSelectedListItems(outermostItems, listTag)) {
2326
+ setListActiveState(false);
2327
+ handleInput();
2328
+ requestAnimationFrame(updateActiveState);
2329
+ return;
2330
+ }
2331
+ }
2332
+ else {
2333
+ const containingLists = new Set(selectedPlainBlocks.length === 0 && commonSelectedList
2334
+ ? [commonSelectedList]
2335
+ : outermostItems.map((item) => {
2336
+ let list = item.parentElement?.matches("ul,ol") ? item.parentElement : null;
2337
+ if (selectedPlainBlocks.length > 0) {
2338
+ while (list?.parentElement?.closest("li")?.parentElement?.matches("ul,ol")) {
2339
+ list = list.parentElement.closest("li").parentElement;
2340
+ }
2341
+ }
2342
+ return list;
2343
+ }));
2344
+ outermostItems = Array.from(containingLists).flatMap((list) => list
2345
+ ? Array.from(list.children).filter((child) => child instanceof HTMLElement && child.tagName === "LI")
2346
+ : []);
2347
+ outermostItems.forEach((item) => restyleDescendantLists(item, listTag));
2348
+ const lastItem = restyleSelectedListItems(outermostItems, listTag, defaultListStyleAtDepth(listTag, 0));
2349
+ const convertedPlainBlocks = convertSelectedBlocksToList(selectedPlainBlocks, listTag, defaultListStyleAtDepth(listTag, 0));
2350
+ if (!convertedPlainBlocks && lastItem)
2351
+ focusElementEnd(lastItem);
2352
+ setListActiveState(true);
1963
2353
  handleInput();
1964
2354
  requestAnimationFrame(updateActiveState);
1965
2355
  return;
@@ -2055,6 +2445,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2055
2445
  catch { }
2056
2446
  };
2057
2447
  const toggleBlockquote = () => {
2448
+ if (!blockquoteFeature)
2449
+ return;
2058
2450
  try {
2059
2451
  if (!restoreSavedSelection())
2060
2452
  safeSelectRange(getSelectionRangeInEditor());
@@ -2062,6 +2454,40 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2062
2454
  const editor = editableRef.current;
2063
2455
  if (!editor || !range)
2064
2456
  return;
2457
+ const rangeNode = range.commonAncestorContainer instanceof HTMLElement
2458
+ ? range.commonAncestorContainer
2459
+ : range.commonAncestorContainer.parentElement;
2460
+ const activeQuote = rangeNode?.closest("blockquote");
2461
+ if (activeQuote && editor.contains(activeQuote)) {
2462
+ const beforeCoreHtml = editor.innerHTML;
2463
+ pushEditorHistory();
2464
+ const replacements = editorControllerRef.current.bindRoot(editor).executeBlockCommand([activeQuote], { id: "blockquote.toggle" });
2465
+ if (replacements) {
2466
+ focusElementEnd(replacements[replacements.length - 1]);
2467
+ handleInput();
2468
+ requestAnimationFrame(updateActiveState);
2469
+ return;
2470
+ }
2471
+ editorControllerRef.current.discardLastHistorySnapshot(beforeCoreHtml);
2472
+ }
2473
+ else {
2474
+ const candidates = (range.collapsed
2475
+ ? [getCurrentBlock()].filter((block) => Boolean(block))
2476
+ : getSelectedBlocks(range)).filter((block) => editor.contains(block));
2477
+ const coreCandidates = candidates.filter((block) => block.matches("p,h1,h2,h3,h4,h5,h6,pre") && !block.closest("li,td,th"));
2478
+ if (coreCandidates.length === candidates.length && coreCandidates.length > 0) {
2479
+ const beforeCoreHtml = editor.innerHTML;
2480
+ pushEditorHistory();
2481
+ const replacements = editorControllerRef.current.bindRoot(editor).executeBlockCommand(coreCandidates, { id: "blockquote.toggle" });
2482
+ if (replacements) {
2483
+ focusElementEnd(replacements[replacements.length - 1]);
2484
+ handleInput();
2485
+ requestAnimationFrame(updateActiveState);
2486
+ return;
2487
+ }
2488
+ editorControllerRef.current.discardLastHistorySnapshot(beforeCoreHtml);
2489
+ }
2490
+ }
2065
2491
  const unwrapQuote = (quote) => {
2066
2492
  const parent = quote.parentElement;
2067
2493
  if (!parent)
@@ -2203,6 +2629,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2203
2629
  catch { }
2204
2630
  };
2205
2631
  const toggleCodeBlock = () => {
2632
+ if (!codeBlockFeature)
2633
+ return;
2206
2634
  try {
2207
2635
  if (!restoreSavedSelection())
2208
2636
  safeSelectRange(getSelectionRangeInEditor());
@@ -2217,10 +2645,27 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2217
2645
  return;
2218
2646
  const listItems = resolveSelectedListItems(range);
2219
2647
  const plainBlocks = blocks.filter((block) => !block.closest("ul,ol") && !block.closest("table"));
2220
- const getItemCode = (item) => item.querySelector(":scope > pre[data-srte-list-code]");
2648
+ // Detect by tag alone, not the data-srte-list-code marker: operations
2649
+ // that round-trip a list item's content through the canonical core
2650
+ // model (e.g. unwrapping an enclosing blockquote) don't know about
2651
+ // this editor-internal attribute and drop it, which previously made
2652
+ // an already-coded item look uncoded and get double-wrapped.
2653
+ const getItemCode = (item) => item.querySelector(":scope > pre");
2221
2654
  const allTargetsActive = listItems.every((item) => Boolean(getItemCode(item))) &&
2222
2655
  plainBlocks.every((block) => block.tagName === "PRE") &&
2223
2656
  listItems.length + plainBlocks.length > 0;
2657
+ if (listItems.length === 0 && plainBlocks.length === blocks.length && plainBlocks.length > 0) {
2658
+ const beforeCoreHtml = editor.innerHTML;
2659
+ pushEditorHistory();
2660
+ const replacements = editorControllerRef.current.bindRoot(editor).executeBlockCommand(plainBlocks, { id: "code-block.toggle" });
2661
+ if (replacements) {
2662
+ focusElementEnd(replacements[replacements.length - 1]);
2663
+ handleInput();
2664
+ requestAnimationFrame(updateActiveState);
2665
+ return;
2666
+ }
2667
+ editorControllerRef.current.discardLastHistorySnapshot(beforeCoreHtml);
2668
+ }
2224
2669
  pushEditorHistory();
2225
2670
  let lastTarget = null;
2226
2671
  listItems.forEach((item) => {
@@ -2228,8 +2673,15 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2228
2673
  if (allTargetsActive && existing) {
2229
2674
  const code = existing.querySelector(":scope > code");
2230
2675
  const source = code || existing;
2676
+ // <code> only ever holds inline content (see creation below), so
2677
+ // restore it inside a single paragraph rather than splicing bare
2678
+ // inline nodes directly into the list item.
2679
+ const paragraph = document.createElement("p");
2231
2680
  while (source.firstChild)
2232
- item.insertBefore(source.firstChild, existing);
2681
+ paragraph.appendChild(source.firstChild);
2682
+ if (!paragraph.childNodes.length)
2683
+ paragraph.innerHTML = "<br>";
2684
+ item.insertBefore(paragraph, existing);
2233
2685
  existing.remove();
2234
2686
  lastTarget = item;
2235
2687
  return;
@@ -2243,12 +2695,21 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2243
2695
  pre.style.textAlign = "left";
2244
2696
  const code = document.createElement("code");
2245
2697
  const nestedList = Array.from(item.children).find((child) => child.matches("ul,ol")) || null;
2246
- Array.from(item.childNodes).forEach((node) => {
2247
- if (node === nestedList)
2248
- return;
2249
- if (node instanceof HTMLElement && node.dataset.srteCheck === "true")
2250
- return;
2251
- code.appendChild(node);
2698
+ const contentNodes = Array.from(item.childNodes).filter((node) => node !== nestedList && !(node instanceof HTMLElement && node.dataset.srteCheck === "true"));
2699
+ // <code> can only hold inline content: a p/heading child (the usual
2700
+ // shape for list-item content) is unwrapped into its inline nodes
2701
+ // rather than nested whole, which would be invalid markup.
2702
+ contentNodes.forEach((node, index) => {
2703
+ if (node instanceof HTMLElement && node.matches("p,h1,h2,h3,h4,h5,h6")) {
2704
+ while (node.firstChild)
2705
+ code.appendChild(node.firstChild);
2706
+ node.remove();
2707
+ }
2708
+ else {
2709
+ code.appendChild(node);
2710
+ }
2711
+ if (index < contentNodes.length - 1)
2712
+ code.appendChild(document.createElement("br"));
2252
2713
  });
2253
2714
  if (!code.childNodes.length)
2254
2715
  code.innerHTML = "<br>";
@@ -2457,9 +2918,6 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2457
2918
  console.error('Error applying font family:', error);
2458
2919
  }
2459
2920
  };
2460
- const applyTextColor = (color) => {
2461
- exec("foreColor", color);
2462
- };
2463
2921
  const parseCssColor = (value) => {
2464
2922
  const normalized = value.trim().toLowerCase();
2465
2923
  if (!normalized || normalized === "transparent")
@@ -2508,13 +2966,20 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2508
2966
  node = node.parentElement;
2509
2967
  return node instanceof HTMLElement && editor.contains(node) ? node : null;
2510
2968
  };
2511
- const getInlineBackgroundHex = (element) => {
2969
+ const getEffectiveBackgroundHex = (element) => {
2512
2970
  const editor = editableRef.current;
2513
2971
  let current = element;
2514
- while (current && current !== editor) {
2515
- const inlineHex = cssColorToHex(current.style.backgroundColor || current.style.background);
2516
- if (inlineHex)
2517
- return inlineHex;
2972
+ while (current) {
2973
+ const inline = cssColorToHex(current.style.backgroundColor || current.style.background);
2974
+ if (inline)
2975
+ return inline;
2976
+ const computed = typeof window !== "undefined"
2977
+ ? cssColorToHex(window.getComputedStyle(current).backgroundColor)
2978
+ : "";
2979
+ if (computed)
2980
+ return computed;
2981
+ if (current === editor)
2982
+ break;
2518
2983
  current = current.parentElement;
2519
2984
  }
2520
2985
  return "";
@@ -2525,9 +2990,9 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2525
2990
  const editor = editableRef.current;
2526
2991
  if (!editor)
2527
2992
  return colorPickerType === "text" ? "#000000" : "#ffffff";
2528
- const element = getRangeStartElement(getSelectionRangeInEditor());
2993
+ const element = getRangeStartElement(getSelectionRangeInEditor() || savedRangeRef.current);
2529
2994
  if (colorPickerType === "background") {
2530
- return getInlineBackgroundHex(element) || "#ffffff";
2995
+ return getEffectiveBackgroundHex(element) || "#ffffff";
2531
2996
  }
2532
2997
  const target = element || editor;
2533
2998
  return cssColorToHex(window.getComputedStyle(target).color) || "#000000";
@@ -2556,7 +3021,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2556
3021
  }))
2557
3022
  .sort((a, b) => b.ratio - a.ratio)[0]?.color || "";
2558
3023
  };
2559
- const applyBackgroundColor = (color) => {
3024
+ const applyInlineColor = (type, color) => {
2560
3025
  try {
2561
3026
  const editor = editableRef.current;
2562
3027
  if (!editor)
@@ -2564,13 +3029,14 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2564
3029
  if (!restoreSavedSelection())
2565
3030
  safeSelectRange(getSelectionRangeInEditor());
2566
3031
  const range = getSelectionRangeInEditor();
2567
- const readableColor = readableTextColorForBackground(color);
2568
3032
  if (!range)
2569
3033
  return;
3034
+ pushEditorHistory();
2570
3035
  const applyToElement = (element) => {
2571
- element.style.backgroundColor = color;
2572
- if (readableColor)
2573
- element.style.color = readableColor;
3036
+ if (type === "text")
3037
+ element.style.color = color;
3038
+ else
3039
+ element.style.backgroundColor = color;
2574
3040
  };
2575
3041
  if (range.collapsed) {
2576
3042
  const span = document.createElement("span");
@@ -2585,34 +3051,46 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2585
3051
  handleInput();
2586
3052
  return;
2587
3053
  }
2588
- const selectedCells = Array.from(editor.querySelectorAll("td,th"))
2589
- .filter((cell) => {
2590
- try {
2591
- return range.intersectsNode(cell);
2592
- }
2593
- catch {
2594
- return false;
3054
+ const walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT);
3055
+ const selected = [];
3056
+ let current = walker.nextNode();
3057
+ while (current) {
3058
+ const node = current;
3059
+ if (node.data && range.intersectsNode(node)) {
3060
+ const start = node === range.startContainer ? range.startOffset : 0;
3061
+ const end = node === range.endContainer ? range.endOffset : node.data.length;
3062
+ if (end > start)
3063
+ selected.push({ node, start, end });
2595
3064
  }
3065
+ current = walker.nextNode();
3066
+ }
3067
+ const styledRuns = [];
3068
+ [...selected].reverse().forEach(({ node, start, end }) => {
3069
+ const runRange = document.createRange();
3070
+ runRange.setStart(node, start);
3071
+ runRange.setEnd(node, end);
3072
+ const span = document.createElement("span");
3073
+ applyToElement(span);
3074
+ span.appendChild(runRange.extractContents());
3075
+ runRange.insertNode(span);
3076
+ styledRuns.unshift(span);
2596
3077
  });
2597
- if (selectedCells.length > 0) {
2598
- selectedCells.forEach(applyToElement);
2599
- handleInput();
2600
- return;
3078
+ if (styledRuns.length) {
3079
+ const nextRange = document.createRange();
3080
+ nextRange.setStart(styledRuns[0], 0);
3081
+ nextRange.setEnd(styledRuns[styledRuns.length - 1], styledRuns[styledRuns.length - 1].childNodes.length);
3082
+ safeSelectRange(nextRange);
3083
+ savedRangeRef.current = nextRange.cloneRange();
2601
3084
  }
2602
- const span = document.createElement("span");
2603
- applyToElement(span);
2604
- const fragment = range.extractContents();
2605
- span.appendChild(fragment);
2606
- range.insertNode(span);
2607
- range.selectNodeContents(span);
2608
- safeSelectRange(range);
2609
- savedRangeRef.current = range.cloneRange();
2610
3085
  handleInput();
3086
+ requestAnimationFrame(updateActiveState);
2611
3087
  }
2612
3088
  catch {
2613
- exec("hiliteColor", color);
3089
+ exec(type === "text" ? "foreColor" : "hiliteColor", color);
2614
3090
  }
2615
3091
  };
3092
+ const applyTextColor = (color) => applyInlineColor("text", color);
3093
+ const applyBackgroundColor = (color) => applyInlineColor("background", color);
2616
3094
  const insertImage = () => {
2617
3095
  if (!media)
2618
3096
  return;
@@ -2790,6 +3268,33 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2790
3268
  range.collapse(false);
2791
3269
  safeSelectRange(range);
2792
3270
  }
3271
+ const coreImage = editorControllerRef.current.bindRoot(host).insertInlineImage({
3272
+ src,
3273
+ alt: typeof srcOrItem === "string" ? "image" : (srcOrItem.alt || srcOrItem.title || "image"),
3274
+ ...(typeof srcOrItem !== "string" && srcOrItem.title ? { title: srcOrItem.title } : {}),
3275
+ });
3276
+ if (coreImage) {
3277
+ coreImage.draggable = true;
3278
+ coreImage.style.maxWidth = "100%";
3279
+ coreImage.style.height = "auto";
3280
+ coreImage.style.display = "inline-block";
3281
+ if (typeof srcOrItem !== "string") {
3282
+ if (srcOrItem.license?.author)
3283
+ coreImage.dataset.licenseAuthor = srcOrItem.license.author;
3284
+ if (srcOrItem.license?.licenseType)
3285
+ coreImage.dataset.licenseType = srcOrItem.license.licenseType;
3286
+ if (srcOrItem.license?.licenseText)
3287
+ coreImage.dataset.licenseText = srcOrItem.license.licenseText;
3288
+ if (srcOrItem.license?.sourceUrl)
3289
+ coreImage.dataset.licenseUrl = srcOrItem.license.sourceUrl;
3290
+ if (srcOrItem.license?.workName)
3291
+ coreImage.dataset.workName = srcOrItem.license.workName;
3292
+ }
3293
+ setSelectedImage(coreImage);
3294
+ scheduleImageOverlay();
3295
+ handleInput();
3296
+ return;
3297
+ }
2793
3298
  const img = document.createElement("img");
2794
3299
  img.src = src;
2795
3300
  img.draggable = true;
@@ -2859,27 +3364,47 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2859
3364
  safeSelectRange(range);
2860
3365
  }
2861
3366
  pushEditorHistory();
2862
- const span = document.createElement("span");
2863
- span.setAttribute("data-formula", tex);
3367
+ const span = editorControllerRef.current.bindRoot(host).insertFormula({
3368
+ value: tex,
3369
+ displayText: `$${tex}$`,
3370
+ });
3371
+ if (span) {
3372
+ try {
3373
+ const katex = window.katex;
3374
+ if (katex && typeof katex.render === "function") {
3375
+ katex.render(tex, span, { throwOnError: false });
3376
+ }
3377
+ }
3378
+ catch { }
3379
+ const next = document.createRange();
3380
+ next.setStartAfter(span);
3381
+ next.collapse(true);
3382
+ safeSelectRange(next);
3383
+ savedRangeRef.current = next.cloneRange();
3384
+ handleInput();
3385
+ return;
3386
+ }
3387
+ const fallbackSpan = document.createElement("span");
3388
+ fallbackSpan.setAttribute("data-formula", tex);
2864
3389
  try {
2865
3390
  // @ts-ignore
2866
3391
  const katex = window.katex;
2867
3392
  if (katex && typeof katex.render === "function") {
2868
- katex.render(tex, span, { throwOnError: false });
3393
+ katex.render(tex, fallbackSpan, { throwOnError: false });
2869
3394
  }
2870
3395
  else {
2871
- span.textContent = `$${tex}$`;
3396
+ fallbackSpan.textContent = `$${tex}$`;
2872
3397
  }
2873
3398
  }
2874
3399
  catch {
2875
- span.textContent = `$${tex}$`;
3400
+ fallbackSpan.textContent = `$${tex}$`;
2876
3401
  }
2877
3402
  if (range)
2878
- range.insertNode(span);
3403
+ range.insertNode(fallbackSpan);
2879
3404
  else
2880
- host.appendChild(span);
3405
+ host.appendChild(fallbackSpan);
2881
3406
  const r = document.createRange();
2882
- r.setStartAfter(span);
3407
+ r.setStartAfter(fallbackSpan);
2883
3408
  r.collapse(true);
2884
3409
  safeSelectRange(r);
2885
3410
  handleInput();
@@ -2954,870 +3479,90 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2954
3479
  });
2955
3480
  }
2956
3481
  };
2957
- const handlePdfFiles = (files) => {
2958
- if (!files || files.length === 0)
3482
+ const getExportableDocument = () => {
3483
+ return editorControllerRef.current
3484
+ .bindRoot(editableRef.current)
3485
+ .getDocument();
3486
+ };
3487
+ const downloadFormatResult = (result) => {
3488
+ if (result.kind === "handled")
2959
3489
  return;
2960
- const file = files[0];
2961
- if (file.type !== 'application/pdf')
3490
+ const blob = result.kind === "blob"
3491
+ ? result.content
3492
+ : new Blob([result.content], { type: result.mediaType });
3493
+ const url = URL.createObjectURL(blob);
3494
+ const link = document.createElement("a");
3495
+ link.href = url;
3496
+ link.download = result.filename;
3497
+ document.body.appendChild(link);
3498
+ link.click();
3499
+ link.remove();
3500
+ URL.revokeObjectURL(url);
3501
+ };
3502
+ const processFormatImport = async (definition, file, mode) => {
3503
+ if (!definition.importFile)
2962
3504
  return;
2963
- // Check if editor has content
2964
- const el = editableRef.current;
2965
- const hasContent = el && el.textContent && el.textContent.trim().length > 0;
2966
- if (hasContent) {
2967
- setPendingImport({ file, type: 'pdf' });
2968
- }
2969
- else {
2970
- processImport(file, 'pdf', 'replace');
3505
+ if (definition.id === "pdf")
3506
+ setLoadingPdf(true);
3507
+ if (definition.id === "docx")
3508
+ setLoadingDocx(true);
3509
+ try {
3510
+ const imported = await definition.importFile(file, { ownerDocument: document });
3511
+ insertImportedHtml(imported.layoutHtml || serializeSmartDocument(imported.document), mode, {
3512
+ preserveColors: imported.preserveColors ?? true,
3513
+ preserveDocumentLayout: imported.preserveDocumentLayout ?? true,
3514
+ });
2971
3515
  }
2972
- };
2973
- const processImport = async (file, type, mode) => {
2974
- if (type === 'pdf') {
2975
- await processPdf(file, mode);
3516
+ catch (error) {
3517
+ console.error(`Error importing ${definition.label}:`, error);
2976
3518
  }
2977
- else {
2978
- await processDocx(file, mode);
3519
+ finally {
3520
+ if (definition.id === "pdf")
3521
+ setLoadingPdf(false);
3522
+ if (definition.id === "docx")
3523
+ setLoadingDocx(false);
3524
+ setPendingImport(null);
2979
3525
  }
2980
- setPendingImport(null);
2981
- // Reset inputs
2982
- if (pdfInputRef.current)
2983
- pdfInputRef.current.value = "";
2984
- if (docxInputRef.current)
2985
- docxInputRef.current.value = "";
2986
3526
  };
2987
- const processPdf = async (file, mode) => {
3527
+ const openFormatImport = (formatId) => {
3528
+ const definition = formatRuntime.get(formatId);
3529
+ if (!definition?.importFile || !formatRuntime.canImport(formatId))
3530
+ return;
3531
+ const input = document.createElement("input");
3532
+ input.type = "file";
3533
+ input.accept = definition.accept || `.${definition.extension}`;
3534
+ input.onchange = () => {
3535
+ const file = input.files?.[0];
3536
+ if (!file)
3537
+ return;
3538
+ const hasContent = Boolean(editableRef.current?.textContent?.trim());
3539
+ if (hasContent && definition.confirmImportWhenNotEmpty) {
3540
+ setPendingImport({ file, definition });
3541
+ return;
3542
+ }
3543
+ void processFormatImport(definition, file, hasContent ? "append" : "replace");
3544
+ };
3545
+ input.click();
3546
+ };
3547
+ const runFormatExport = async (formatId) => {
3548
+ const definition = formatRuntime.get(formatId);
3549
+ if (!definition || !formatRuntime.canExport(formatId))
3550
+ return;
3551
+ if (!definition.exportDocument)
3552
+ return;
3553
+ const result = await definition.exportDocument(getExportableDocument(), {
3554
+ ownerDocument: document,
3555
+ hostWindow: window,
3556
+ });
3557
+ downloadFormatResult(result);
3558
+ };
3559
+ const fixNegativeMargins = (root) => {
2988
3560
  try {
2989
- setLoadingPdf(true);
2990
- const arrayBuffer = await file.arrayBuffer();
2991
- const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
2992
- let fullHtml = '';
2993
- for (let i = 1; i <= pdf.numPages; i++) {
2994
- const page = await pdf.getPage(i);
2995
- const viewport = page.getViewport({ scale: 1 });
2996
- const textContent = await page.getTextContent();
2997
- const styles = textContent.styles;
2998
- // 1. Group items into lines
2999
- const items = textContent.items;
3000
- // Calculate base statistics
3001
- const heights = items.map(item => Math.abs(item.transform[3])).filter(h => h > 0);
3002
- heights.sort((a, b) => a - b);
3003
- const medianHeight = heights[Math.floor(heights.length / 2)] || 12;
3004
- // Group by Y (with tolerance)
3005
- const linesMap = new Map();
3006
- for (const item of items) {
3007
- if (!item.str.trim())
3008
- continue;
3009
- // Normalize Y to integer buckets to group roughly
3010
- // PDF Y is bottom-0, so higher Y is higher on page.
3011
- const y = item.transform[5];
3012
- // Find closest existing line
3013
- let foundKey = -1;
3014
- for (const key of linesMap.keys()) {
3015
- if (Math.abs(key - y) < medianHeight * 0.5) {
3016
- foundKey = key;
3017
- break;
3018
- }
3019
- }
3020
- if (foundKey !== -1) {
3021
- linesMap.get(foundKey).items.push(item);
3022
- }
3023
- else {
3024
- linesMap.set(y, { y, items: [item] });
3025
- }
3026
- }
3027
- // Convert map to sorted array (top to bottom)
3028
- const lines = Array.from(linesMap.values()).sort((a, b) => b.y - a.y);
3029
- // Sort items within lines (left to right)
3030
- lines.forEach(line => {
3031
- line.items.sort((a, b) => a.transform[4] - b.transform[4]);
3032
- });
3033
- // 2. Identify and Build Structures
3034
- let html = '';
3035
- let listStack = []; // 'ul' or 'ol'
3036
- let inTable = false;
3037
- let tableColumns = []; // X-coordinates of column starts
3038
- let tableHtml = '';
3039
- const closeList = () => {
3040
- if (listStack.length > 0) {
3041
- html += `</${listStack.pop()}>`;
3042
- }
3043
- };
3044
- const closeTable = () => {
3045
- if (inTable) {
3046
- html += '<div data-table-wrapper="true" style="overflow-x:auto;width:100%;"><table style="border-collapse:collapse;width:100%;" border="1"><tbody>' + tableHtml + '</tbody></table></div>';
3047
- tableHtml = '';
3048
- inTable = false;
3049
- tableColumns = [];
3050
- }
3051
- };
3052
- for (let lIndex = 0; lIndex < lines.length; lIndex++) {
3053
- const line = lines[lIndex];
3054
- // Calculate gaps and text
3055
- let lineText = '';
3056
- let lineHtmlContent = '';
3057
- let lastX = -1;
3058
- let gaps = [];
3059
- let itemXs = []; // Start X of logical items (words or phrases)
3060
- // Reconstruct text with spacing detection
3061
- for (let j = 0; j < line.items.length; j++) {
3062
- const item = line.items[j];
3063
- const x = item.transform[4];
3064
- const width = item.width;
3065
- const fontName = item.fontName;
3066
- const fontObj = styles[fontName];
3067
- const fontFamily = fontObj?.fontFamily?.toLowerCase() || '';
3068
- const isBold = fontFamily.includes('bold') || false;
3069
- const isItalic = fontFamily.includes('italic') || fontFamily.includes('oblique');
3070
- const fontSize = Math.max(8, Math.round(Math.abs(item.transform[3])));
3071
- if (lastX > 0) {
3072
- const gap = x - lastX;
3073
- if (gap > 2) { // Minimal space threshold
3074
- lineText += ' ';
3075
- lineHtmlContent += ' ';
3076
- if (gap > 20) { // Large gap threshold for table detection
3077
- gaps.push(gap);
3078
- }
3079
- }
3080
- }
3081
- else {
3082
- // First item
3083
- }
3084
- // Track "columns" candidates: items separated by big gaps
3085
- if (j === 0 || (x - lastX) > 20) {
3086
- itemXs.push(x);
3087
- }
3088
- // Append text style
3089
- const chunkStyle = cssRules([
3090
- ['font-size', `${fontSize}px`],
3091
- ['font-weight', isBold ? '700' : ''],
3092
- ['font-style', isItalic ? 'italic' : ''],
3093
- ]);
3094
- let chunk = `<span${styleAttr(chunkStyle)}>${escapeHtml(item.str)}</span>`;
3095
- lineText += item.str;
3096
- lineHtmlContent += chunk;
3097
- lastX = x + width;
3098
- }
3099
- // === Structure Detection ===
3100
- // Max Font Size in line
3101
- const maxH = Math.max(...line.items.map((i) => Math.abs(i.transform[3])));
3102
- const isHeader = maxH > medianHeight * 1.2;
3103
- // List Detection
3104
- const isBullet = /^[•\-\*]\s/.test(lineText);
3105
- const isNumber = /^\d+[\.\)]\s/.test(lineText);
3106
- // Table Detection Logic
3107
- // A line starts a table if it has distinct "columns" (multiple items with large gaps)
3108
- // Or if we are already in a table and this line aligns with columns
3109
- let isTableLine = false;
3110
- // If in table, check alignment
3111
- if (inTable) {
3112
- // Check if items align with known columns
3113
- // Simple loose check: do any of the itemXs align with tableColumns?
3114
- // Or is the line just sparsely populated but roughly compatible?
3115
- // We'll continually simple-add rows for now until a Paragraph break (plain text, no gaps) is found.
3116
- // If line looks like normal paragraph (no large gaps, starts at left margin), close table
3117
- const isPlainParagraph = gaps.length === 0 && itemXs[0] < 50 && lineText.length > 50;
3118
- // Allow wrapping text in table cells, which might look like lines with no gaps?
3119
- // Table wrapping usually is indented or aligns with a column > 0.
3120
- const alignsWithColumn = itemXs.some(x => tableColumns.some(cx => Math.abs(x - cx) < 20));
3121
- if (alignsWithColumn || (itemXs[0] > 50)) {
3122
- isTableLine = true;
3123
- }
3124
- else {
3125
- // Maybe a new row starting at col 0?
3126
- // If it aligns with col 0.
3127
- if (Math.abs(itemXs[0] - tableColumns[0]) < 20) {
3128
- isTableLine = true;
3129
- }
3130
- }
3131
- }
3132
- else {
3133
- // Potential start of table: multiple items separated by gaps, AND next line likely follows suit?
3134
- // Or simply: It has > 1 column significantly spaced.
3135
- if (itemXs.length >= 2 && gaps.some(g => g > 30)) {
3136
- isTableLine = true;
3137
- // Establish columns
3138
- tableColumns = [...itemXs];
3139
- }
3140
- }
3141
- // --- Apply Logic ---
3142
- if (isTableLine) {
3143
- closeList();
3144
- if (!inTable) {
3145
- inTable = true;
3146
- // Start table
3147
- }
3148
- // Build Row
3149
- // We need to map items to cells based on tableColumns.
3150
- // Naive approach: Items close to col X go to col X.
3151
- let rowHtml = '<tr>';
3152
- // We assume `tableColumns` defines the start of each cell.
3153
- // We create a cell for each column.
3154
- // Collect content for each bucket.
3155
- const cellContents = new Array(tableColumns.length).fill('');
3156
- let currentItemHtml = '';
3157
- let currentItemStart = -1;
3158
- // process items again to slot them
3159
- let currentLineX = 0;
3160
- for (const item of line.items) {
3161
- const x = item.transform[4];
3162
- const w = item.width;
3163
- const txt = item.str;
3164
- const fontObj = styles[item.fontName];
3165
- const fontFamily = fontObj?.fontFamily?.toLowerCase() || '';
3166
- const isBold = fontFamily.includes('bold');
3167
- const isItalic = fontFamily.includes('italic') || fontFamily.includes('oblique');
3168
- const fontSize = Math.max(8, Math.round(Math.abs(item.transform[3])));
3169
- const styledTxt = `<span${styleAttr(cssRules([
3170
- ['font-size', `${fontSize}px`],
3171
- ['font-weight', isBold ? '700' : ''],
3172
- ['font-style', isItalic ? 'italic' : ''],
3173
- ]))}>${escapeHtml(txt)}</span>`;
3174
- // Decide which column this belongs to
3175
- // Find closest column to the left (or close enough)
3176
- let colIdx = 0;
3177
- let minDiff = 9999;
3178
- for (let c = 0; c < tableColumns.length; c++) {
3179
- const colX = tableColumns[c];
3180
- // If item starts near colX or after it (but before next col)
3181
- // Actually, just find the "controlling" column (closest start to the left)
3182
- if (x >= colX - 10) {
3183
- colIdx = c;
3184
- }
3185
- }
3186
- // Append space if needed
3187
- if (cellContents[colIdx])
3188
- cellContents[colIdx] += ' ';
3189
- cellContents[colIdx] += styledTxt;
3190
- }
3191
- cellContents.forEach(content => {
3192
- rowHtml += `<td style="border:1px solid #d1d5db;padding:8px;vertical-align:top;">${content || '&nbsp;'}</td>`;
3193
- });
3194
- rowHtml += '</tr>';
3195
- tableHtml += rowHtml;
3196
- }
3197
- else {
3198
- closeTable();
3199
- if (isBullet || isNumber) {
3200
- const listType = isBullet ? 'ul' : 'ol';
3201
- if (listStack.length === 0 || listStack[listStack.length - 1] !== listType) {
3202
- if (listStack.length > 0)
3203
- closeList(); // Close switch
3204
- html += `<${listType}>`;
3205
- listStack.push(listType);
3206
- }
3207
- // Strip marker
3208
- const content = lineHtmlContent.replace(/^[•\-\*]|\d+[\.\)]/, '').trim();
3209
- html += `<li>${content}</li>`;
3210
- }
3211
- else {
3212
- closeList();
3213
- if (isHeader) {
3214
- const tag = maxH > medianHeight * 1.5 ? 'h2' : 'h3';
3215
- const firstX = line.items[0]?.transform?.[4] || 0;
3216
- const lastItem = line.items[line.items.length - 1];
3217
- const lastRight = lastItem ? lastItem.transform[4] + lastItem.width : firstX;
3218
- const center = (firstX + lastRight) / 2;
3219
- const align = Math.abs(center - viewport.width / 2) < viewport.width * 0.12 ? 'center' : firstX > viewport.width * 0.55 ? 'right' : '';
3220
- html += `<${tag}${styleAttr(cssRules([['text-align', align]]))}>${lineHtmlContent}</${tag}>`;
3221
- }
3222
- else {
3223
- const firstX = line.items[0]?.transform?.[4] || 0;
3224
- const lastItem = line.items[line.items.length - 1];
3225
- const lastRight = lastItem ? lastItem.transform[4] + lastItem.width : firstX;
3226
- const center = (firstX + lastRight) / 2;
3227
- const align = Math.abs(center - viewport.width / 2) < viewport.width * 0.12 ? 'center' : firstX > viewport.width * 0.55 ? 'right' : '';
3228
- html += `<p${styleAttr(cssRules([
3229
- ['text-align', align],
3230
- ['margin-left', firstX > 40 && !align ? `${Math.round(firstX)}px` : ''],
3231
- ]))}>${lineHtmlContent}</p>`;
3232
- }
3233
- }
3234
- }
3235
- }
3236
- closeList();
3237
- closeTable();
3238
- fullHtml += html;
3239
- }
3240
- insertImportedHtml(fullHtml, mode, { preserveColors: true, preserveDocumentLayout: true });
3241
- }
3242
- catch (error) {
3243
- console.error('Error reading PDF:', error);
3244
- // Optional: show user error
3245
- }
3246
- finally {
3247
- setLoadingPdf(false);
3248
- }
3249
- };
3250
- const handleDocxFiles = (files) => {
3251
- if (!files || files.length === 0)
3252
- return;
3253
- const file = files[0];
3254
- if (!file.name.endsWith('.docx'))
3255
- return;
3256
- // Check if editor has content
3257
- const el = editableRef.current;
3258
- const hasContent = el && el.textContent && el.textContent.trim().length > 0;
3259
- if (hasContent) {
3260
- setPendingImport({ file, type: 'docx' });
3261
- }
3262
- else {
3263
- processImport(file, 'docx', 'replace');
3264
- }
3265
- };
3266
- const processDocx = async (file, mode) => {
3267
- try {
3268
- setLoadingDocx(true);
3269
- const arrayBuffer = await file.arrayBuffer();
3270
- const html = preserveDocxStyles
3271
- ? await convertDocxToStyledHtml(arrayBuffer)
3272
- : await convertDocxWithMammoth(arrayBuffer);
3273
- if (html) {
3274
- insertImportedHtml(`<div class="srte-preserve-colors">${html}</div>`, mode, {
3275
- preserveColors: preserveDocxStyles,
3276
- preserveDocumentLayout: preserveDocxStyles,
3277
- });
3278
- }
3279
- }
3280
- catch (error) {
3281
- console.error('Error reading DOCX:', error);
3282
- }
3283
- finally {
3284
- setLoadingDocx(false);
3285
- }
3286
- };
3287
- const convertDocxWithMammoth = async (arrayBuffer) => {
3288
- const result = await mammoth.convertToHtml({ arrayBuffer });
3289
- const temp = document.createElement('div');
3290
- temp.innerHTML = result.value;
3291
- enhanceImportedTables(temp);
3292
- return temp.innerHTML;
3293
- };
3294
- const convertDocxToStyledHtml = async (arrayBuffer) => {
3295
- try {
3296
- const zip = await JSZip.loadAsync(arrayBuffer);
3297
- const documentXml = await zip.file('word/document.xml')?.async('text');
3298
- if (!documentXml)
3299
- return convertDocxWithMammoth(arrayBuffer);
3300
- const parser = new DOMParser();
3301
- const doc = parser.parseFromString(documentXml, 'application/xml');
3302
- if (doc.querySelector('parsererror'))
3303
- return convertDocxWithMammoth(arrayBuffer);
3304
- const body = Array.from(doc.getElementsByTagName('*')).find((node) => node.localName === 'body');
3305
- if (!body)
3306
- return convertDocxWithMammoth(arrayBuffer);
3307
- const html = directChildren(body)
3308
- .filter((node) => node.localName !== 'sectPr')
3309
- .map((node) => {
3310
- if (node.localName === 'p')
3311
- return convertDocxParagraph(node);
3312
- if (node.localName === 'tbl')
3313
- return convertDocxTable(node);
3314
- return '';
3315
- })
3316
- .join('');
3317
- if (!html.trim())
3318
- return convertDocxWithMammoth(arrayBuffer);
3319
- const temp = document.createElement('div');
3320
- temp.innerHTML = html;
3321
- enhanceImportedTables(temp);
3322
- return temp.innerHTML;
3323
- }
3324
- catch (error) {
3325
- console.warn('Falling back to Mammoth DOCX import:', error);
3326
- return convertDocxWithMammoth(arrayBuffer);
3327
- }
3328
- };
3329
- const directChildren = (node) => Array.from(node.children);
3330
- const firstChildByName = (node, localName) => node
3331
- ? directChildren(node).find((child) => child.localName === localName)
3332
- : undefined;
3333
- const childrenByName = (node, localName) => node
3334
- ? directChildren(node).filter((child) => child.localName === localName)
3335
- : [];
3336
- const docxAttr = (node, name) => {
3337
- if (!node)
3338
- return '';
3339
- return (node.getAttribute(`w:${name}`) ||
3340
- node.getAttribute(name) ||
3341
- node.getAttributeNS('http://schemas.openxmlformats.org/wordprocessingml/2006/main', name) ||
3342
- '');
3343
- };
3344
- const docxHexColor = (value) => {
3345
- if (!value || value.toLowerCase() === 'auto')
3346
- return '';
3347
- const normalized = value.replace(/[^0-9a-f]/gi, '');
3348
- return normalized.length === 6 ? `#${normalized}` : '';
3349
- };
3350
- const twipsToPt = (value) => {
3351
- const n = Number(value);
3352
- return Number.isFinite(n) ? `${Math.max(n / 20, 0)}pt` : '';
3353
- };
3354
- const halfPointsToPt = (value) => {
3355
- const n = Number(value);
3356
- return Number.isFinite(n) ? `${Math.max(n / 2, 1)}pt` : '';
3357
- };
3358
- const cssRules = (rules) => rules
3359
- .filter(([, value]) => Boolean(value))
3360
- .map(([name, value]) => `${name}: ${value}`)
3361
- .join('; ');
3362
- const styleAttr = (style) => (style ? ` style="${escapeHtml(style)}"` : '');
3363
- const convertDocxParagraphStyle = (paragraph) => {
3364
- const pPr = firstChildByName(paragraph, 'pPr');
3365
- if (!pPr)
3366
- return '';
3367
- const spacing = firstChildByName(pPr, 'spacing');
3368
- const jc = firstChildByName(pPr, 'jc');
3369
- const indent = firstChildByName(pPr, 'ind');
3370
- const borderBottom = firstChildByName(firstChildByName(pPr, 'pBdr'), 'bottom');
3371
- const line = docxAttr(spacing, 'line');
3372
- const lineRule = docxAttr(spacing, 'lineRule');
3373
- return cssRules([
3374
- ['text-align', docxAttr(jc, 'val')],
3375
- ['margin-top', twipsToPt(docxAttr(spacing, 'before'))],
3376
- ['margin-bottom', twipsToPt(docxAttr(spacing, 'after'))],
3377
- ['margin-left', twipsToPt(docxAttr(indent, 'left'))],
3378
- ['text-indent', twipsToPt(docxAttr(indent, 'firstLine'))],
3379
- ['line-height', line && lineRule === 'auto' ? `${Number(line) / 240}` : ''],
3380
- ['border-bottom', docxBorderCss(borderBottom)],
3381
- ]);
3382
- };
3383
- const convertDocxRunStyle = (run) => {
3384
- const rPr = firstChildByName(run, 'rPr');
3385
- if (!rPr)
3386
- return '';
3387
- const color = docxHexColor(docxAttr(firstChildByName(rPr, 'color'), 'val'));
3388
- const highlight = docxHexColor(docxAttr(firstChildByName(rPr, 'highlight'), 'val'));
3389
- const shade = docxHexColor(docxAttr(firstChildByName(rPr, 'shd'), 'fill'));
3390
- const size = halfPointsToPt(docxAttr(firstChildByName(rPr, 'sz'), 'val'));
3391
- const underline = firstChildByName(rPr, 'u');
3392
- return cssRules([
3393
- ['font-weight', firstChildByName(rPr, 'b') ? '700' : ''],
3394
- ['font-style', firstChildByName(rPr, 'i') ? 'italic' : ''],
3395
- ['text-decoration', underline ? 'underline' : ''],
3396
- ['color', color],
3397
- ['background-color', highlight || shade],
3398
- ['font-size', size],
3399
- ]);
3400
- };
3401
- const convertDocxRun = (run) => {
3402
- const rPr = firstChildByName(run, 'rPr');
3403
- const vertAlign = docxAttr(firstChildByName(rPr, 'vertAlign'), 'val');
3404
- const style = convertDocxRunStyle(run);
3405
- const content = directChildren(run)
3406
- .map((child) => {
3407
- if (child.localName === 't')
3408
- return escapeHtml(child.textContent || '');
3409
- if (child.localName === 'tab')
3410
- return '&emsp;';
3411
- if (child.localName === 'br') {
3412
- return docxAttr(child, 'type') === 'page'
3413
- ? '<hr class="srte-docx-page-break">'
3414
- : '<br>';
3415
- }
3416
- return '';
3417
- })
3418
- .join('');
3419
- if (!content)
3420
- return '';
3421
- const tag = vertAlign === 'superscript' ? 'sup' : vertAlign === 'subscript' ? 'sub' : 'span';
3422
- return `<${tag}${styleAttr(style)}>${content}</${tag}>`;
3423
- };
3424
- const convertDocxParagraph = (paragraph) => {
3425
- const style = convertDocxParagraphStyle(paragraph);
3426
- const content = childrenByName(paragraph, 'r').map(convertDocxRun).join('');
3427
- return `<p${styleAttr(style)}>${content || '<br>'}</p>`;
3428
- };
3429
- const docxBorderCss = (border) => {
3430
- if (!border)
3431
- return '';
3432
- const val = docxAttr(border, 'val');
3433
- if (!val || val === 'nil' || val === 'none')
3434
- return '';
3435
- const size = Number(docxAttr(border, 'sz')) || 4;
3436
- const width = Math.max(size / 8, 0.5);
3437
- const color = docxHexColor(docxAttr(border, 'color')) || '#d1d5db';
3438
- return `${width}px solid ${color}`;
3439
- };
3440
- const convertDocxCellStyle = (cell) => {
3441
- const tcPr = firstChildByName(cell, 'tcPr');
3442
- const width = twipsToPt(docxAttr(firstChildByName(tcPr, 'tcW'), 'w'));
3443
- const shade = docxHexColor(docxAttr(firstChildByName(tcPr, 'shd'), 'fill'));
3444
- const borders = firstChildByName(tcPr, 'tcBorders');
3445
- const top = docxBorderCss(firstChildByName(borders, 'top'));
3446
- const right = docxBorderCss(firstChildByName(borders, 'right'));
3447
- const bottom = docxBorderCss(firstChildByName(borders, 'bottom'));
3448
- const left = docxBorderCss(firstChildByName(borders, 'left'));
3449
- return cssRules([
3450
- ['width', width],
3451
- ['background-color', shade],
3452
- ['border-top', top],
3453
- ['border-right', right],
3454
- ['border-bottom', bottom],
3455
- ['border-left', left],
3456
- ['padding', '8px'],
3457
- ['vertical-align', 'top'],
3458
- ]);
3459
- };
3460
- const convertDocxTable = (table) => {
3461
- const rows = childrenByName(table, 'tr')
3462
- .map((row) => {
3463
- const cells = childrenByName(row, 'tc')
3464
- .map((cell) => {
3465
- const content = directChildren(cell)
3466
- .filter((child) => child.localName === 'p' || child.localName === 'tbl')
3467
- .map((child) => child.localName === 'p' ? convertDocxParagraph(child) : convertDocxTable(child))
3468
- .join('');
3469
- return `<td${styleAttr(convertDocxCellStyle(cell))}>${content || '<p><br></p>'}</td>`;
3470
- })
3471
- .join('');
3472
- return `<tr>${cells}</tr>`;
3473
- })
3474
- .join('');
3475
- return `<table style="border-collapse: collapse; width: 100%; margin: 12px 0;"><tbody>${rows}</tbody></table>`;
3476
- };
3477
- const enhanceImportedTables = (root) => {
3478
- const tables = root.querySelectorAll('table');
3479
- tables.forEach(tbl => {
3480
- tbl.style.borderCollapse = tbl.style.borderCollapse || 'collapse';
3481
- tbl.style.width = tbl.style.width || '100%';
3482
- const cells = tbl.querySelectorAll('td, th');
3483
- cells.forEach(cell => {
3484
- const el = cell;
3485
- if (!el.style.border && !el.style.borderTop && !el.style.borderRight && !el.style.borderBottom && !el.style.borderLeft) {
3486
- el.style.border = '1px solid #d1d5db';
3487
- }
3488
- el.style.padding = el.style.padding || '8px';
3489
- el.style.verticalAlign = el.style.verticalAlign || 'top';
3490
- });
3491
- });
3492
- };
3493
- const escapeHtml = (value) => value
3494
- .replace(/&/g, "&amp;")
3495
- .replace(/</g, "&lt;")
3496
- .replace(/>/g, "&gt;");
3497
- const htmlToMarkdown = (html) => {
3498
- const root = document.createElement("div");
3499
- root.innerHTML = html;
3500
- const walk = (node) => {
3501
- if (node.nodeType === Node.TEXT_NODE)
3502
- return node.textContent || "";
3503
- if (!(node instanceof HTMLElement))
3504
- return "";
3505
- const content = Array.from(node.childNodes).map(walk).join("");
3506
- const tag = node.tagName.toLowerCase();
3507
- if (tag === "strong" || tag === "b")
3508
- return `**${content}**`;
3509
- if (tag === "em" || tag === "i")
3510
- return `*${content}*`;
3511
- if (tag === "code")
3512
- return `\`${content}\``;
3513
- if (tag === "br")
3514
- return "\n";
3515
- if (/h[1-6]/.test(tag))
3516
- return `${"#".repeat(Number(tag[1]))} ${content.trim()}\n\n`;
3517
- if (tag === "p")
3518
- return `${content.trim()}\n\n`;
3519
- if (tag === "li")
3520
- return `- ${content.trim()}\n`;
3521
- if (tag === "ul" || tag === "ol")
3522
- return `${content}\n`;
3523
- if (tag === "blockquote")
3524
- return `> ${content.trim()}\n\n`;
3525
- if (tag === "table")
3526
- return `${node.outerHTML}\n\n`;
3527
- if (tag === "img")
3528
- return `![${node.getAttribute("alt") || ""}](${node.getAttribute("src") || ""})`;
3529
- if (tag === "a")
3530
- return `[${content}](${node.getAttribute("href") || ""})`;
3531
- return content;
3532
- };
3533
- return Array.from(root.childNodes).map(walk).join("").replace(/\n{3,}/g, "\n\n").trim();
3534
- };
3535
- const importTextFile = async (files, type) => {
3536
- if (!files || files.length === 0)
3537
- return;
3538
- const file = files[0];
3539
- const text = await file.text();
3540
- const html = type === "html" ? text : markdownToCompatibilityHtml(text);
3541
- const el = editableRef.current;
3542
- const hasContent = el && el.textContent && el.textContent.trim().length > 0;
3543
- insertImportedHtml(html, hasContent ? "append" : "replace", {
3544
- preserveColors: true,
3545
- preserveDocumentLayout: true,
3546
- });
3547
- };
3548
- const downloadText = (filename, content, mimeType) => {
3549
- const blob = new Blob([content], { type: mimeType });
3550
- const url = URL.createObjectURL(blob);
3551
- const link = document.createElement("a");
3552
- link.href = url;
3553
- link.download = filename;
3554
- document.body.appendChild(link);
3555
- link.click();
3556
- link.remove();
3557
- URL.revokeObjectURL(url);
3558
- };
3559
- const buildStandaloneHtmlDocument = (html) => `<!doctype html>
3560
- <html lang="en">
3561
- <head>
3562
- <meta charset="utf-8">
3563
- <meta name="viewport" content="width=device-width, initial-scale=1">
3564
- <title>Smart RTE Export</title>
3565
- <style>
3566
- :root {
3567
- --srte-bg: #ffffff;
3568
- --srte-text: #111111;
3569
- --srte-text-muted: #4b5563;
3570
- --srte-border: #d1d5db;
3571
- --srte-border-light: #e5e7eb;
3572
- --srte-accent: #1e90ff;
3573
- --srte-accent-bg: rgba(30, 144, 255, 0.15);
3574
- --srte-surface-subtle: #f3f4f6;
3575
- }
3576
- html, body {
3577
- margin: 0;
3578
- background: var(--srte-bg);
3579
- color: var(--srte-text);
3580
- font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
3581
- line-height: 1.6;
3582
- }
3583
- body {
3584
- padding: 32px;
3585
- }
3586
- .srte-export {
3587
- max-width: 960px;
3588
- margin: 0 auto;
3589
- }
3590
- .srte-export h1,
3591
- .srte-export h2,
3592
- .srte-export h3,
3593
- .srte-export h4,
3594
- .srte-export h5,
3595
- .srte-export h6 {
3596
- line-height: 1.3;
3597
- margin: 1.25em 0 0.5em;
3598
- font-weight: 700;
3599
- color: var(--srte-text);
3600
- }
3601
- .srte-export p {
3602
- margin: 0 0 0.85em;
3603
- }
3604
- .srte-export blockquote {
3605
- border-left: 4px solid var(--srte-accent);
3606
- margin: 0.75em 0;
3607
- padding: 0.5em 1em;
3608
- background: var(--srte-surface-subtle);
3609
- color: var(--srte-text);
3610
- }
3611
- .srte-export ul,
3612
- .srte-export ol {
3613
- margin: 0.75em 0;
3614
- padding-left: 1.75em;
3615
- list-style-position: outside;
3616
- }
3617
- .srte-export ul {
3618
- list-style-type: disc;
3619
- }
3620
- .srte-export ol {
3621
- list-style-type: decimal;
3622
- }
3623
- .srte-export li {
3624
- display: list-item;
3625
- margin: 0.25em 0;
3626
- padding-left: 0.25em;
3627
- }
3628
- .srte-export table {
3629
- border-collapse: collapse;
3630
- width: 100%;
3631
- margin: 12px 0;
3632
- }
3633
- .srte-export td,
3634
- .srte-export th {
3635
- border: 1px solid var(--srte-border);
3636
- padding: 8px;
3637
- vertical-align: top;
3638
- text-align: left;
3639
- }
3640
- .srte-export th {
3641
- background: var(--srte-surface-subtle);
3642
- font-weight: 700;
3643
- }
3644
- .srte-export img {
3645
- max-width: 100%;
3646
- height: auto;
3647
- }
3648
- .srte-export pre,
3649
- .srte-export code {
3650
- background: var(--srte-surface-subtle);
3651
- color: var(--srte-text);
3652
- border-radius: 4px;
3653
- }
3654
- .srte-export pre {
3655
- padding: 12px;
3656
- overflow-x: auto;
3657
- white-space: pre-wrap;
3658
- }
3659
- @media print {
3660
- body {
3661
- padding: 0;
3662
- }
3663
- .srte-export {
3664
- max-width: none;
3665
- }
3666
- }
3667
- </style>
3668
- </head>
3669
- <body>
3670
- <main class="srte-export">${html || "<p></p>"}</main>
3671
- </body>
3672
- </html>`;
3673
- const exportHtml = () => {
3674
- const html = editableRef.current?.innerHTML || "";
3675
- downloadText("smart-rte-export.html", buildStandaloneHtmlDocument(html), "text/html;charset=utf-8");
3676
- };
3677
- const exportMarkdown = () => {
3678
- const html = editableRef.current?.innerHTML || "";
3679
- downloadText("smart-rte-export.md", htmlToMarkdown(html), "text/markdown");
3680
- };
3681
- const htmlToDocxXml = (html) => {
3682
- const root = document.createElement("div");
3683
- root.innerHTML = html;
3684
- const xmlEscape = (value) => value
3685
- .replace(/&/g, "&amp;")
3686
- .replace(/</g, "&lt;")
3687
- .replace(/>/g, "&gt;")
3688
- .replace(/"/g, "&quot;");
3689
- const colorValue = (value) => {
3690
- const hex = /^#([0-9a-f]{6})$/i.exec(value.trim());
3691
- if (hex)
3692
- return hex[1].toUpperCase();
3693
- const rgb = /^rgb\(\s*(\d+),\s*(\d+),\s*(\d+)\s*\)$/i.exec(value.trim());
3694
- if (!rgb)
3695
- return "";
3696
- return [rgb[1], rgb[2], rgb[3]]
3697
- .map((part) => Math.max(0, Math.min(255, Number(part))).toString(16).padStart(2, "0"))
3698
- .join("")
3699
- .toUpperCase();
3700
- };
3701
- const sizeToHalfPoints = (value) => {
3702
- const trimmed = value.trim();
3703
- const match = /^([\d.]+)(px|pt)$/i.exec(trimmed);
3704
- if (!match)
3705
- return "";
3706
- const raw = Number(match[1]);
3707
- const pt = match[2].toLowerCase() === "px" ? raw * 0.75 : raw;
3708
- return String(Math.max(2, Math.round(pt * 2)));
3709
- };
3710
- const runProperties = (el) => {
3711
- const style = el.style;
3712
- const color = colorValue(style.color);
3713
- const size = sizeToHalfPoints(style.fontSize);
3714
- const isBold = el.tagName === "B" || el.tagName === "STRONG" || /bold|700|800|900/.test(style.fontWeight);
3715
- const isItalic = el.tagName === "I" || el.tagName === "EM" || style.fontStyle === "italic";
3716
- const isUnderline = el.tagName === "U" || style.textDecoration.includes("underline");
3717
- return [
3718
- isBold ? "<w:b/>" : "",
3719
- isItalic ? "<w:i/>" : "",
3720
- isUnderline ? '<w:u w:val="single"/>' : "",
3721
- color ? `<w:color w:val="${color}"/>` : "",
3722
- size ? `<w:sz w:val="${size}"/>` : "",
3723
- ].join("");
3724
- };
3725
- const runs = (node, inheritedProps = "") => {
3726
- if (node.nodeType === Node.TEXT_NODE) {
3727
- const text = node.textContent || "";
3728
- return text ? `<w:r>${inheritedProps ? `<w:rPr>${inheritedProps}</w:rPr>` : ""}<w:t xml:space="preserve">${xmlEscape(text)}</w:t></w:r>` : "";
3729
- }
3730
- if (!(node instanceof HTMLElement))
3731
- return "";
3732
- if (node.tagName === "BR")
3733
- return "<w:r><w:br/></w:r>";
3734
- if (node.tagName === "IMG") {
3735
- const alt = node.getAttribute("alt") || node.getAttribute("title") || "Image";
3736
- return `<w:r><w:t>[Image: ${xmlEscape(alt)}]</w:t></w:r>`;
3737
- }
3738
- const props = `${inheritedProps}${runProperties(node)}`;
3739
- return Array.from(node.childNodes).map((child) => runs(child, props)).join("");
3740
- };
3741
- const paragraph = (el, fallbackTag = "p") => {
3742
- const tag = el.tagName.toLowerCase();
3743
- const headingMatch = /^h([1-6])$/.exec(tag);
3744
- const style = el.style;
3745
- const align = style.textAlign ? `<w:jc w:val="${xmlEscape(style.textAlign)}"/>` : "";
3746
- const headingSize = headingMatch ? `<w:rPr><w:b/><w:sz w:val="${Math.max(24, 40 - Number(headingMatch[1]) * 4)}"/></w:rPr>` : "";
3747
- const body = runs(el);
3748
- return `<w:p><w:pPr>${align}${headingSize}</w:pPr>${body || "<w:r><w:t></w:t></w:r>"}</w:p>`;
3749
- };
3750
- const tableCell = (cell) => {
3751
- const fill = colorValue(cell.style.backgroundColor);
3752
- const shading = fill ? `<w:shd w:val="clear" w:color="auto" w:fill="${fill}"/>` : "";
3753
- const cellContent = Array.from(cell.childNodes)
3754
- .map((child) => child instanceof HTMLElement && ["P", "DIV", "H1", "H2", "H3", "H4", "H5", "H6"].includes(child.tagName)
3755
- ? paragraph(child)
3756
- : `<w:p>${runs(child)}</w:p>`)
3757
- .join("");
3758
- return `<w:tc><w:tcPr>${shading}<w:tcBorders><w:top w:val="single" w:sz="4" w:color="D1D5DB"/><w:left w:val="single" w:sz="4" w:color="D1D5DB"/><w:bottom w:val="single" w:sz="4" w:color="D1D5DB"/><w:right w:val="single" w:sz="4" w:color="D1D5DB"/></w:tcBorders></w:tcPr>${cellContent || "<w:p/>"}</w:tc>`;
3759
- };
3760
- const tableXml = (table) => {
3761
- const rows = Array.from(table.querySelectorAll("tr"));
3762
- return `<w:tbl><w:tblPr><w:tblW w:w="0" w:type="auto"/><w:tblBorders><w:top w:val="single" w:sz="4" w:color="D1D5DB"/><w:left w:val="single" w:sz="4" w:color="D1D5DB"/><w:bottom w:val="single" w:sz="4" w:color="D1D5DB"/><w:right w:val="single" w:sz="4" w:color="D1D5DB"/><w:insideH w:val="single" w:sz="4" w:color="D1D5DB"/><w:insideV w:val="single" w:sz="4" w:color="D1D5DB"/></w:tblBorders></w:tblPr>${rows.map((row) => `<w:tr>${Array.from(row.children).map((cell) => tableCell(cell)).join("")}</w:tr>`).join("")}</w:tbl>`;
3763
- };
3764
- const blockXml = (node) => {
3765
- if (node.nodeType === Node.TEXT_NODE) {
3766
- const text = node.textContent?.trim();
3767
- return text ? `<w:p>${runs(node)}</w:p>` : "";
3768
- }
3769
- if (!(node instanceof HTMLElement))
3770
- return "";
3771
- if (node.tagName === "TABLE")
3772
- return tableXml(node);
3773
- if (node.tagName === "UL" || node.tagName === "OL") {
3774
- return Array.from(node.children).map((li) => `<w:p><w:r><w:t>• </w:t></w:r>${runs(li)}</w:p>`).join("");
3775
- }
3776
- if (node.tagName === "BLOCKQUOTE") {
3777
- return `<w:p><w:pPr><w:ind w:left="720"/></w:pPr>${runs(node)}</w:p>`;
3778
- }
3779
- if (node.tagName === "HR")
3780
- return '<w:p><w:pPr><w:pBdr><w:bottom w:val="single" w:sz="6" w:color="D1D5DB"/></w:pBdr></w:pPr></w:p>';
3781
- if (["P", "DIV", "PRE", "H1", "H2", "H3", "H4", "H5", "H6"].includes(node.tagName))
3782
- return paragraph(node);
3783
- return Array.from(node.childNodes).map(blockXml).join("");
3784
- };
3785
- const body = Array.from(root.childNodes).map(blockXml).join("");
3786
- return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>${body}<w:sectPr><w:pgSz w:w="12240" w:h="15840"/><w:pgMar w:top="720" w:right="720" w:bottom="720" w:left="720"/></w:sectPr></w:body></w:document>`;
3787
- };
3788
- const exportDocx = async () => {
3789
- const html = editableRef.current?.innerHTML || "";
3790
- const zip = new JSZip();
3791
- zip.file("[Content_Types].xml", `<?xml version="1.0" encoding="UTF-8"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/></Types>`);
3792
- zip.folder("_rels")?.file(".rels", `<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>`);
3793
- zip.folder("word")?.file("document.xml", htmlToDocxXml(html));
3794
- zip.folder("word")?.folder("_rels")?.file("document.xml.rels", `<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"></Relationships>`);
3795
- const blob = await zip.generateAsync({ type: "blob", mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" });
3796
- const url = URL.createObjectURL(blob);
3797
- const link = document.createElement("a");
3798
- link.href = url;
3799
- link.download = "smart-rte-export.docx";
3800
- document.body.appendChild(link);
3801
- link.click();
3802
- link.remove();
3803
- URL.revokeObjectURL(url);
3804
- };
3805
- const exportPdf = () => {
3806
- const html = editableRef.current?.innerHTML || "";
3807
- const printWindow = window.open("", "_blank", "width=900,height=700");
3808
- if (!printWindow)
3809
- return;
3810
- printWindow.document.open();
3811
- printWindow.document.write(`<!doctype html><html><head><title>Export PDF</title><style>@page{margin:18mm}html,body{background:#fff}body{font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;line-height:1.6;padding:32px;color:#111}table{border-collapse:collapse;width:100%;margin:12px 0;break-inside:auto}tr,img,blockquote,pre{break-inside:avoid}td,th{border:1px solid #d1d5db;padding:8px;vertical-align:top}img{max-width:100%;height:auto}blockquote{border-left:4px solid #d1d5db;padding-left:12px;color:#374151}pre,code{background:#f3f4f6}pre{padding:12px;white-space:pre-wrap}@media print{body{padding:0}}</style></head><body>${html || "<p></p>"}<script>window.addEventListener("load",function(){setTimeout(function(){window.focus();window.print();},150);});</script></body></html>`);
3812
- printWindow.document.close();
3813
- };
3814
- const fixNegativeMargins = (root) => {
3815
- try {
3816
- const nodes = root.querySelectorAll('*');
3817
- for (let i = 0; i < nodes.length; i++) {
3818
- const node = nodes[i];
3819
- if (node.style && node.style.marginLeft && node.style.marginLeft.trim().startsWith('-')) {
3820
- node.style.marginLeft = '0px';
3561
+ const nodes = root.querySelectorAll('*');
3562
+ for (let i = 0; i < nodes.length; i++) {
3563
+ const node = nodes[i];
3564
+ if (node.style && node.style.marginLeft && node.style.marginLeft.trim().startsWith('-')) {
3565
+ node.style.marginLeft = '0px';
3821
3566
  }
3822
3567
  }
3823
3568
  }
@@ -3885,6 +3630,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
3885
3630
  const style = node.getAttribute('style');
3886
3631
  if (!style)
3887
3632
  return;
3633
+ // blockquote/pre carry their own consistent border+padding from the
3634
+ // editor stylesheet; an inline border/padding pasted in from the
3635
+ // source page (e.g. a "border: 0" reset) silently wins the cascade
3636
+ // over that stylesheet rule and leaves them looking broken.
3637
+ const tagName = node.tagName.toLowerCase();
3638
+ const dropsBorderAndPadding = tagName === 'blockquote' || tagName === 'pre';
3888
3639
  const safeRules = style
3889
3640
  .split(';')
3890
3641
  .map((rule) => rule.trim())
@@ -3897,6 +3648,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
3897
3648
  const value = rule.slice(separator + 1).trim().toLowerCase();
3898
3649
  if (!allowedStyleNames.has(name))
3899
3650
  return false;
3651
+ if (dropsBorderAndPadding && (name.startsWith('border') || name.startsWith('padding')))
3652
+ return false;
3900
3653
  if (value.includes('position') || value.includes('expression') || value.includes('javascript:'))
3901
3654
  return false;
3902
3655
  if (name === 'white-space' && value !== 'pre-wrap')
@@ -3917,9 +3670,55 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
3917
3670
  if (!el)
3918
3671
  return;
3919
3672
  fixNegativeMargins(el);
3673
+ normalizeListNesting(el);
3920
3674
  ensureTableWrappers(el);
3921
3675
  addTableResizeHandles();
3922
3676
  };
3677
+ /** Repairs list markup emitted by Google Docs and browsers where a nested
3678
+ * list is serialized as a sibling of its parent LI instead of a child. */
3679
+ const normalizeListNesting = (root) => {
3680
+ root.querySelectorAll("ul,ol").forEach((list) => {
3681
+ let currentItem = null;
3682
+ Array.from(list.children).forEach((child) => {
3683
+ if (!(child instanceof HTMLElement))
3684
+ return;
3685
+ if (child.tagName === "LI") {
3686
+ currentItem = child;
3687
+ return;
3688
+ }
3689
+ if (currentItem && child.parentElement === list && (child.tagName === "UL" || child.tagName === "OL")) {
3690
+ currentItem.appendChild(child);
3691
+ }
3692
+ });
3693
+ const firstItem = list.querySelector(":scope > li");
3694
+ if (!list.style.listStyleType && firstItem?.style.listStyleType) {
3695
+ list.style.listStyleType = firstItem.style.listStyleType;
3696
+ }
3697
+ // Imported document HTML often places list-style-type on every LI.
3698
+ // That overrides later changes made on the UL/OL and makes the toolbar
3699
+ // appear inert. The canonical DOM stores marker style on the list only.
3700
+ list.querySelectorAll(":scope > li").forEach((item) => {
3701
+ item.style.removeProperty("list-style-type");
3702
+ });
3703
+ list.querySelectorAll(":scope > li > ul, :scope > li > ol").forEach((nested) => {
3704
+ const nestedItem = nested.querySelector(":scope > li");
3705
+ if (!nested.style.listStyleType && nestedItem?.style.listStyleType) {
3706
+ nested.style.listStyleType = nestedItem.style.listStyleType;
3707
+ }
3708
+ });
3709
+ });
3710
+ root.querySelectorAll("ul[data-srte-list-preset],ol[data-srte-list-preset]").forEach((list) => {
3711
+ const preset = list.dataset.srteListPreset;
3712
+ if (!isSmartListPreset(preset)) {
3713
+ clearListPreset(list);
3714
+ return;
3715
+ }
3716
+ const ancestorWithPreset = list.parentElement?.closest("ul[data-srte-list-preset],ol[data-srte-list-preset]");
3717
+ if (ancestorWithPreset?.dataset.srteListPreset === preset)
3718
+ return;
3719
+ applyListPresetHierarchy(list, preset);
3720
+ });
3721
+ };
3923
3722
  const insertHtmlAtEnd = (html) => {
3924
3723
  const el = editableRef.current;
3925
3724
  if (!el)
@@ -4030,6 +3829,27 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4030
3829
  }
4031
3830
  });
4032
3831
  };
3832
+ // blockquote/pre always get their border+padding from the editor
3833
+ // stylesheet, which an inline style (e.g. carried in from a paste, or set
3834
+ // directly via the value prop) silently overrides regardless of how it
3835
+ // got there. Strip those two properties so the editor's own look always
3836
+ // wins, no matter the element's origin.
3837
+ const normalizeBlockContainerStyling = (root) => {
3838
+ root.querySelectorAll("blockquote,pre").forEach((element) => {
3839
+ element.style.removeProperty("border");
3840
+ element.style.removeProperty("border-top");
3841
+ element.style.removeProperty("border-right");
3842
+ element.style.removeProperty("border-bottom");
3843
+ element.style.removeProperty("border-left");
3844
+ element.style.removeProperty("padding");
3845
+ element.style.removeProperty("padding-top");
3846
+ element.style.removeProperty("padding-right");
3847
+ element.style.removeProperty("padding-bottom");
3848
+ element.style.removeProperty("padding-left");
3849
+ if (!element.getAttribute("style"))
3850
+ element.removeAttribute("style");
3851
+ });
3852
+ };
4033
3853
  const normalizeInvalidQuoteNesting = (root) => {
4034
3854
  root.querySelectorAll("blockquote blockquote").forEach((quote) => {
4035
3855
  const outerQuote = quote.parentElement?.closest("blockquote");
@@ -4057,7 +3877,10 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4057
3877
  outerCodeBlock.parentElement.insertBefore(codeBlock, outerCodeBlock.nextSibling);
4058
3878
  }
4059
3879
  });
4060
- root.querySelectorAll("p,h1,h2,h3,h4,h5,h6,blockquote").forEach((container) => {
3880
+ // p/h1-h6 can only hold inline content, so a pre landing inside one (e.g.
3881
+ // from bad paste HTML) is genuinely invalid and gets hoisted out. A
3882
+ // blockquote is a real block container and is allowed to hold a pre.
3883
+ root.querySelectorAll("p,h1,h2,h3,h4,h5,h6").forEach((container) => {
4061
3884
  const nestedCodeBlocks = Array.from(container.children).filter((child) => child.tagName === "PRE");
4062
3885
  nestedCodeBlocks.forEach((codeBlock) => {
4063
3886
  container.parentElement?.insertBefore(codeBlock, container.nextSibling);
@@ -4102,10 +3925,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4102
3925
  return;
4103
3926
  // Auto-fix negative margins that might cause visibility issues
4104
3927
  fixNegativeMargins(el);
3928
+ normalizeListNesting(el);
4105
3929
  // Quotes are document blocks and cannot be nested by drag and drop.
4106
3930
  normalizeInvalidQuoteNesting(el);
4107
3931
  // Code blocks are document blocks and cannot be nested by drag and drop.
4108
3932
  normalizeInvalidCodeBlockNesting(el);
3933
+ normalizeBlockContainerStyling(el);
4109
3934
  // Tables are document-level blocks and must not remain inside code or list items.
4110
3935
  normalizeInvalidTableNesting(el);
4111
3936
  // Ensure tables are wrapped for horizontal scrolling
@@ -4158,6 +3983,27 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4158
3983
  sel?.removeAllRanges();
4159
3984
  sel?.addRange(range);
4160
3985
  }
3986
+ if (range?.collapsed) {
3987
+ pushEditorHistory();
3988
+ const inserted = editorControllerRef.current.bindRoot(el).insertTable(tableRows, tableCols);
3989
+ if (inserted) {
3990
+ Array.from(inserted.rows).forEach((row, rowIndex) => {
3991
+ row.setAttribute("data-row-index", String(rowIndex));
3992
+ cellsOfRow(row).forEach((cell, cellIndex) => {
3993
+ cell.setAttribute("data-col-index", String(cellIndex));
3994
+ cell.style.border = cell.style.border || "1px solid #d1d5db";
3995
+ cell.style.padding = cell.style.padding || "6px";
3996
+ cell.style.minWidth = cell.style.minWidth || "60px";
3997
+ });
3998
+ });
3999
+ addTableResizeHandles();
4000
+ const firstCell = inserted.querySelector("td,th");
4001
+ if (firstCell instanceof HTMLTableCellElement)
4002
+ moveCaretToCell(firstCell, false);
4003
+ handleInput();
4004
+ return;
4005
+ }
4006
+ }
4161
4007
  const html = buildTableHTML(tableRows, tableCols);
4162
4008
  // Insert via Range for broader support
4163
4009
  const wrapper = document.createElement("div");
@@ -4328,6 +4174,26 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4328
4174
  if (!anchor)
4329
4175
  return;
4330
4176
  pushEditorHistory();
4177
+ clearSelectionDecor();
4178
+ const table = tbody.closest("table");
4179
+ if (table instanceof HTMLTableElement) {
4180
+ const replacement = editorControllerRef.current.bindRoot(editableRef.current).executeTableCommand(table, {
4181
+ id: "table.cell.merge",
4182
+ input: {
4183
+ start: { row: sr, column: sc },
4184
+ end: { row: er, column: ec },
4185
+ },
4186
+ });
4187
+ if (replacement) {
4188
+ const nextBody = replacement.tBodies[0];
4189
+ const nextAnchor = nextBody ? getTableGrid(nextBody).grid[sr]?.[sc] : null;
4190
+ if (nextAnchor)
4191
+ moveCaretToCell(nextAnchor, false);
4192
+ addTableResizeHandles();
4193
+ handleInput();
4194
+ return;
4195
+ }
4196
+ }
4331
4197
  // Collect content and remove other cells
4332
4198
  const contents = [];
4333
4199
  const cellsToMerge = getCellsInGridRect(tbody, sr, sc, er, ec);
@@ -4357,7 +4223,20 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4357
4223
  const pos = getCellPosition(cell);
4358
4224
  if (!pos)
4359
4225
  return;
4360
- const { row, tbody, rIdx } = pos;
4226
+ const { row, tbody, rIdx, table } = pos;
4227
+ const insertIndex = dir === "above" ? rIdx : rIdx + 1;
4228
+ const replacement = editorControllerRef.current.bindRoot(editableRef.current).executeTableCommand(table, {
4229
+ id: "table.row.add",
4230
+ input: { index: insertIndex },
4231
+ });
4232
+ if (replacement) {
4233
+ const nextBody = replacement.tBodies[0];
4234
+ const nextCell = nextBody ? getTableGrid(nextBody).grid[insertIndex]?.[0] : null;
4235
+ if (nextCell)
4236
+ moveCaretToCell(nextCell, false);
4237
+ addTableResizeHandles();
4238
+ return;
4239
+ }
4361
4240
  const newRow = document.createElement("tr");
4362
4241
  const numCols = Array.from(row.children).filter((c) => ["TD", "TH"].includes(c.tagName)).length;
4363
4242
  for (let i = 0; i < numCols; i++) {
@@ -4368,7 +4247,6 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4368
4247
  td.innerHTML = "&nbsp;";
4369
4248
  newRow.appendChild(td);
4370
4249
  }
4371
- const insertIndex = dir === "above" ? rIdx : rIdx + 1;
4372
4250
  const refRow = tbody.children[insertIndex] || null;
4373
4251
  tbody.insertBefore(newRow, refRow);
4374
4252
  };
@@ -4376,7 +4254,20 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4376
4254
  const pos = getCellPosition(cell);
4377
4255
  if (!pos)
4378
4256
  return;
4379
- const { row, tbody, table } = pos;
4257
+ const { row, tbody, table, rIdx } = pos;
4258
+ const replacement = editorControllerRef.current.bindRoot(editableRef.current).executeTableCommand(table, {
4259
+ id: "table.row.remove",
4260
+ input: { index: rIdx },
4261
+ });
4262
+ if (replacement) {
4263
+ const nextBody = replacement.tBodies[0];
4264
+ const nextRow = Math.min(rIdx, Math.max(0, replacement.rows.length - 1));
4265
+ const nextCell = nextBody ? getTableGrid(nextBody).grid[nextRow]?.[0] : null;
4266
+ if (nextCell)
4267
+ moveCaretToCell(nextCell, false);
4268
+ addTableResizeHandles();
4269
+ return;
4270
+ }
4380
4271
  tbody.removeChild(row);
4381
4272
  if (tbody.querySelectorAll("tr").length === 0) {
4382
4273
  table.parentElement?.removeChild(table);
@@ -4387,8 +4278,20 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4387
4278
  if (!pos)
4388
4279
  return;
4389
4280
  const { tbody, cIdx } = pos;
4390
- const rows = Array.from(tbody.querySelectorAll("tr"));
4391
4281
  const insertIndex = dir === "left" ? cIdx : cIdx + 1;
4282
+ const replacement = editorControllerRef.current.bindRoot(editableRef.current).executeTableCommand(pos.table, {
4283
+ id: "table.column.add",
4284
+ input: { index: insertIndex },
4285
+ });
4286
+ if (replacement) {
4287
+ const nextBody = replacement.tBodies[0];
4288
+ const nextCell = nextBody ? getTableGrid(nextBody).grid[0]?.[insertIndex] : null;
4289
+ if (nextCell)
4290
+ moveCaretToCell(nextCell, false);
4291
+ addTableResizeHandles();
4292
+ return;
4293
+ }
4294
+ const rows = Array.from(tbody.querySelectorAll("tr"));
4392
4295
  for (const r of rows) {
4393
4296
  const cells = Array.from(r.children).filter((c) => ["TD", "TH"].includes(c.tagName));
4394
4297
  const td = document.createElement("td");
@@ -4408,6 +4311,20 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4408
4311
  if (!pos)
4409
4312
  return;
4410
4313
  const { tbody, table, cIdx } = pos;
4314
+ const replacement = editorControllerRef.current.bindRoot(editableRef.current).executeTableCommand(table, {
4315
+ id: "table.column.remove",
4316
+ input: { index: cIdx },
4317
+ });
4318
+ if (replacement) {
4319
+ const nextBody = replacement.tBodies[0];
4320
+ const nextGrid = nextBody ? getTableGrid(nextBody).grid : [];
4321
+ const nextColumn = Math.min(cIdx, Math.max(0, (nextGrid[0]?.length || 1) - 1));
4322
+ const nextCell = nextGrid[0]?.[nextColumn] || null;
4323
+ if (nextCell)
4324
+ moveCaretToCell(nextCell, false);
4325
+ addTableResizeHandles();
4326
+ return;
4327
+ }
4411
4328
  const rows = Array.from(tbody.querySelectorAll("tr"));
4412
4329
  for (const r of rows) {
4413
4330
  const cells = Array.from(r.children).filter((c) => ["TD", "TH"].includes(c.tagName));
@@ -4421,7 +4338,21 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4421
4338
  table.parentElement?.removeChild(table);
4422
4339
  };
4423
4340
  const toggleHeaderCell = (cell) => {
4341
+ const pos = getCellPosition(cell);
4342
+ if (!pos)
4343
+ return;
4424
4344
  clearSelectionDecor();
4345
+ const replacement = editorControllerRef.current.bindRoot(editableRef.current).executeTableCommand(pos.table, {
4346
+ id: "table.header.cell.toggle",
4347
+ input: { row: pos.rIdx, column: pos.cIdx },
4348
+ });
4349
+ if (replacement) {
4350
+ const nextCell = getTableGrid(replacement.tBodies[0]).grid[pos.rIdx]?.[pos.cIdx];
4351
+ if (nextCell)
4352
+ moveCaretToCell(nextCell, false);
4353
+ addTableResizeHandles();
4354
+ return;
4355
+ }
4425
4356
  const isTh = cell.tagName === "TH";
4426
4357
  replaceTableCellTag(cell, isTh ? "td" : "th");
4427
4358
  handleInput();
@@ -4431,6 +4362,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4431
4362
  if (!pos)
4432
4363
  return;
4433
4364
  const { table } = pos;
4365
+ if (editorControllerRef.current.bindRoot(editableRef.current).removeTable(table))
4366
+ return;
4434
4367
  table.parentElement?.removeChild(table);
4435
4368
  };
4436
4369
  const splitCell = (cell) => {
@@ -4442,6 +4375,19 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4442
4375
  const cs = Math.max(1, cell.colSpan || 1);
4443
4376
  if (rs === 1 && cs === 1)
4444
4377
  return;
4378
+ clearSelectionDecor();
4379
+ const replacement = editorControllerRef.current.bindRoot(editableRef.current).executeTableCommand(pos.table, {
4380
+ id: "table.cell.split",
4381
+ input: { row: rIdx, column: cIdx },
4382
+ });
4383
+ if (replacement) {
4384
+ const nextBody = replacement.tBodies[0];
4385
+ const nextCell = nextBody ? getTableGrid(nextBody).grid[rIdx]?.[cIdx] : null;
4386
+ if (nextCell)
4387
+ moveCaretToCell(nextCell, false);
4388
+ addTableResizeHandles();
4389
+ return;
4390
+ }
4445
4391
  // Reset current cell
4446
4392
  cell.rowSpan = 1;
4447
4393
  cell.colSpan = 1;
@@ -4478,6 +4424,17 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4478
4424
  if (!pos)
4479
4425
  return;
4480
4426
  clearSelectionDecor();
4427
+ const replacement = editorControllerRef.current.bindRoot(editableRef.current).executeTableCommand(pos.table, {
4428
+ id: "table.header.row.toggle",
4429
+ input: { row: pos.rIdx, column: pos.cIdx },
4430
+ });
4431
+ if (replacement) {
4432
+ const nextCell = getTableGrid(replacement.tBodies[0]).grid[pos.rIdx]?.[pos.cIdx];
4433
+ if (nextCell)
4434
+ moveCaretToCell(nextCell, false);
4435
+ addTableResizeHandles();
4436
+ return;
4437
+ }
4481
4438
  const { row } = pos;
4482
4439
  const cells = cellsOfRow(row);
4483
4440
  const shouldMakeHeader = cells.some((c) => c.tagName !== "TH");
@@ -4497,6 +4454,17 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4497
4454
  if (!pos)
4498
4455
  return;
4499
4456
  clearSelectionDecor();
4457
+ const replacement = editorControllerRef.current.bindRoot(editableRef.current).executeTableCommand(pos.table, {
4458
+ id: "table.header.column.toggle",
4459
+ input: { row: pos.rIdx, column: pos.cIdx },
4460
+ });
4461
+ if (replacement) {
4462
+ const nextCell = getTableGrid(replacement.tBodies[0]).grid[pos.rIdx]?.[pos.cIdx];
4463
+ if (nextCell)
4464
+ moveCaretToCell(nextCell, false);
4465
+ addTableResizeHandles();
4466
+ return;
4467
+ }
4500
4468
  const { tbody, cIdx } = pos;
4501
4469
  const { grid } = getTableGrid(tbody);
4502
4470
  const seen = new Set();
@@ -4514,23 +4482,46 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4514
4482
  }
4515
4483
  handleInput();
4516
4484
  };
4517
- const applyBgToSelection = (hex, fallbackCell) => {
4485
+ const applyBgToSelection = (hex, fallbackCell, explicitCells) => {
4518
4486
  const readableColor = readableTextColorForBackground(hex);
4487
+ const targetCells = explicitCells?.length
4488
+ ? explicitCells
4489
+ : shouldUseTableSelection(fallbackCell)
4490
+ ? getCellsInGridRect(selectionRef.current.tbody, selectionRef.current.sr, selectionRef.current.sc, selectionRef.current.er, selectionRef.current.ec)
4491
+ : fallbackCell ? [fallbackCell] : [];
4492
+ const positions = targetCells
4493
+ .map((target) => getCellPosition(target))
4494
+ .filter((position) => Boolean(position));
4495
+ const table = positions[0]?.table;
4496
+ if (table && positions.every((position) => position.table === table)) {
4497
+ const start = {
4498
+ row: Math.min(...positions.map((position) => position.rIdx)),
4499
+ column: Math.min(...positions.map((position) => position.cIdx)),
4500
+ };
4501
+ const end = {
4502
+ row: Math.max(...positions.map((position) => position.rIdx)),
4503
+ column: Math.max(...positions.map((position) => position.cIdx)),
4504
+ };
4505
+ clearSelectionDecor();
4506
+ const replacement = editorControllerRef.current.bindRoot(editableRef.current).executeTableCommand(table, {
4507
+ id: "table.cell.style.set",
4508
+ input: { start, end, backgroundColor: hex, textColor: readableColor || undefined },
4509
+ });
4510
+ if (replacement) {
4511
+ const nextCell = getTableGrid(replacement.tBodies[0]).grid[start.row]?.[start.column];
4512
+ if (nextCell)
4513
+ moveCaretToCell(nextCell, false);
4514
+ addTableResizeHandles();
4515
+ return;
4516
+ }
4517
+ }
4519
4518
  const applyFill = (cell) => {
4520
4519
  cell.style.background = hex;
4521
4520
  if (readableColor)
4522
4521
  cell.style.color = readableColor;
4523
4522
  };
4524
- const sel = shouldUseTableSelection(fallbackCell) ? selectionRef.current : null;
4525
- if (sel) {
4526
- const cells = getCellsInGridRect(sel.tbody, sel.sr, sel.sc, sel.er, sel.ec);
4527
- clearSelectionDecor();
4528
- cells.forEach(applyFill);
4529
- }
4530
- else if (fallbackCell) {
4531
- clearSelectionDecor();
4532
- applyFill(fallbackCell);
4533
- }
4523
+ clearSelectionDecor();
4524
+ targetCells.forEach(applyFill);
4534
4525
  };
4535
4526
  const tableCellFillHex = (cell) => {
4536
4527
  const storedSelectionBackground = cell.__rtePrevBg;
@@ -4553,18 +4544,35 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4553
4544
  return allSame ? first : tableCellFillHex(cell);
4554
4545
  };
4555
4546
  const toggleBorderSelection = (fallbackCell) => {
4547
+ const targetCells = shouldUseTableSelection(fallbackCell)
4548
+ ? getCellsInGridRect(selectionRef.current.tbody, selectionRef.current.sr, selectionRef.current.sc, selectionRef.current.er, selectionRef.current.ec)
4549
+ : fallbackCell ? [fallbackCell] : [];
4550
+ const positions = targetCells
4551
+ .map((target) => getCellPosition(target))
4552
+ .filter((position) => Boolean(position));
4553
+ const table = positions[0]?.table;
4554
+ if (table && positions.every((position) => position.table === table)) {
4555
+ const start = {
4556
+ row: Math.min(...positions.map((position) => position.rIdx)),
4557
+ column: Math.min(...positions.map((position) => position.cIdx)),
4558
+ };
4559
+ const end = {
4560
+ row: Math.max(...positions.map((position) => position.rIdx)),
4561
+ column: Math.max(...positions.map((position) => position.cIdx)),
4562
+ };
4563
+ clearSelectionDecor();
4564
+ if (editorControllerRef.current.bindRoot(editableRef.current).executeTableCommand(table, {
4565
+ id: "table.cell.border.toggle",
4566
+ input: { start, end },
4567
+ }))
4568
+ return;
4569
+ }
4556
4570
  const applyToggle = (cell) => {
4557
4571
  const cur = cell.style.border;
4558
4572
  cell.style.border =
4559
4573
  cur && cur !== "none" ? "none" : "1px solid #d1d5db";
4560
4574
  };
4561
- const sel = shouldUseTableSelection(fallbackCell) ? selectionRef.current : null;
4562
- if (sel) {
4563
- getCellsInGridRect(sel.tbody, sel.sr, sel.sc, sel.er, sel.ec).forEach(applyToggle);
4564
- }
4565
- else if (fallbackCell) {
4566
- applyToggle(fallbackCell);
4567
- }
4575
+ targetCells.forEach(applyToggle);
4568
4576
  };
4569
4577
  const runTableCellAction = (cell, action) => {
4570
4578
  pushEditorHistory();
@@ -4663,7 +4671,14 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4663
4671
  }
4664
4672
  };
4665
4673
  const handleTableResizeEnd = () => {
4666
- if (tableResizeRef.current) {
4674
+ const resize = tableResizeRef.current;
4675
+ if (resize) {
4676
+ const size = resize.type === "column"
4677
+ ? Number.parseFloat(resize.cells[0]?.style.width || "") || resize.startSize
4678
+ : Number.parseFloat(resize.table.rows[resize.index]?.style.height || "") || resize.startSize;
4679
+ editorControllerRef.current.bindRoot(editableRef.current).executeTableCommand(resize.table, resize.type === "column"
4680
+ ? { id: "table.column.width.set", input: { index: resize.index, widthPx: size } }
4681
+ : { id: "table.row.height.set", input: { index: resize.index, heightPx: size } });
4667
4682
  tableResizeRef.current = null;
4668
4683
  document.body.style.cursor = '';
4669
4684
  document.body.style.userSelect = '';
@@ -4978,6 +4993,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4978
4993
  return sibling;
4979
4994
  };
4980
4995
  const moveSelectedBlocks = (direction) => {
4996
+ if (!moveFeature)
4997
+ return false;
4981
4998
  const editor = editableRef.current;
4982
4999
  const range = getSelectionRangeInEditor();
4983
5000
  if (!editor || !range || range.collapsed)
@@ -4993,6 +5010,19 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4993
5010
  const parent = blocks[0].parentElement;
4994
5011
  if (!parent || blocks.some((block) => block.parentElement !== parent))
4995
5012
  return false;
5013
+ const coreResult = executeDomMoveCommand(blocks, direction, pushEditorHistory);
5014
+ if (coreResult === false)
5015
+ return true;
5016
+ if (coreResult) {
5017
+ const movedRange = document.createRange();
5018
+ movedRange.setStartBefore(coreResult[0]);
5019
+ movedRange.setEndAfter(coreResult[coreResult.length - 1]);
5020
+ safeSelectRange(movedRange);
5021
+ savedRangeRef.current = movedRange.cloneRange();
5022
+ handleInput();
5023
+ requestAnimationFrame(updateActiveState);
5024
+ return true;
5025
+ }
4996
5026
  pushEditorHistory();
4997
5027
  if (direction === "up") {
4998
5028
  const previous = elementSibling(blocks[0], "previous");
@@ -5026,6 +5056,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
5026
5056
  return true;
5027
5057
  };
5028
5058
  const moveCurrentElement = (direction) => {
5059
+ if (!moveFeature)
5060
+ return;
5029
5061
  restoreSavedSelection();
5030
5062
  const editor = editableRef.current;
5031
5063
  const range = getSelectionRangeInEditor();
@@ -5050,6 +5082,23 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
5050
5082
  cell.appendChild(paragraph);
5051
5083
  target = paragraph;
5052
5084
  }
5085
+ if (target.tagName.toLowerCase() !== "li") {
5086
+ const coreResult = executeDomMoveCommand([target], direction, () => {
5087
+ if (!historyPushed)
5088
+ pushEditorHistory();
5089
+ historyPushed = true;
5090
+ });
5091
+ if (coreResult === false)
5092
+ return;
5093
+ if (coreResult) {
5094
+ focusElementEnd(target);
5095
+ setSelectedImage(target.tagName === "IMG" ? target : target.querySelector("img"));
5096
+ scheduleImageOverlay();
5097
+ handleInput();
5098
+ requestAnimationFrame(updateActiveState);
5099
+ return;
5100
+ }
5101
+ }
5053
5102
  if (target.tagName.toLowerCase() === "li") {
5054
5103
  const list = target.parentElement;
5055
5104
  if (!list)
@@ -5169,6 +5218,70 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
5169
5218
  event.preventDefault();
5170
5219
  }
5171
5220
  };
5221
+ const executePluginCommand = (commandId, input) => {
5222
+ const editor = editableRef.current;
5223
+ if (!editor || readOnly)
5224
+ return false;
5225
+ const before = editor.innerHTML;
5226
+ pushEditorHistory();
5227
+ const result = editorControllerRef.current
5228
+ .bindRoot(editor)
5229
+ .execute(commandId, input);
5230
+ if (!result) {
5231
+ editorControllerRef.current.discardLastHistorySnapshot(before);
5232
+ return false;
5233
+ }
5234
+ handleInput();
5235
+ requestAnimationFrame(updateActiveState);
5236
+ return true;
5237
+ };
5238
+ const executePluginToolbarContribution = (contribution) => executePluginCommand(contribution.commandId, contribution.input);
5239
+ const pluginContributionContext = {
5240
+ root: editableRef.current,
5241
+ selection: typeof window === "undefined" ? null : window.getSelection(),
5242
+ readOnly,
5243
+ canExecute: (commandId, input) => editorControllerRef.current.bindRoot(editableRef.current).canExecute(commandId, input),
5244
+ };
5245
+ const isPluginContributionVisible = (contribution) => {
5246
+ try {
5247
+ return contribution.isVisible?.(pluginContributionContext) ?? true;
5248
+ }
5249
+ catch {
5250
+ return false;
5251
+ }
5252
+ };
5253
+ const isPluginContributionEnabled = (contribution) => {
5254
+ if (readOnly)
5255
+ return false;
5256
+ try {
5257
+ if (contribution.isEnabled && !contribution.isEnabled(pluginContributionContext))
5258
+ return false;
5259
+ return !editableRef.current || pluginContributionContext.canExecute(contribution.commandId, contribution.input);
5260
+ }
5261
+ catch {
5262
+ return false;
5263
+ }
5264
+ };
5265
+ const isPluginContributionActive = (contribution) => {
5266
+ try {
5267
+ return contribution.isActive?.(pluginContributionContext) ?? false;
5268
+ }
5269
+ catch {
5270
+ return false;
5271
+ }
5272
+ };
5273
+ const isPluginShortcutAvailable = (shortcut) => {
5274
+ if (readOnly)
5275
+ return false;
5276
+ try {
5277
+ return (shortcut.isVisible?.(pluginContributionContext) ?? true) &&
5278
+ (shortcut.isEnabled?.(pluginContributionContext) ?? true) &&
5279
+ pluginContributionContext.canExecute(shortcut.commandId, shortcut.input);
5280
+ }
5281
+ catch {
5282
+ return false;
5283
+ }
5284
+ };
5172
5285
  const editorClass = `srte-editor${theme === 'dark' ? ' srte-dark' : ''}${className ? ' ' + className : ''}`;
5173
5286
  return (_jsxs("div", { className: editorClass, style: {
5174
5287
  border: "1px solid var(--srte-border)",
@@ -5229,19 +5342,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
5229
5342
  }
5230
5343
  }
5231
5344
  e.currentTarget.value = "";
5232
- } })), _jsx("input", { ref: pdfInputRef, type: "file", accept: "application/pdf", style: { display: "none" }, onChange: (e) => {
5233
- handlePdfFiles(e.currentTarget.files);
5234
- e.currentTarget.value = "";
5235
- } }), _jsx("input", { ref: docxInputRef, type: "file", accept: ".docx", style: { display: "none" }, onChange: (e) => {
5236
- handleDocxFiles(e.currentTarget.files);
5237
- e.currentTarget.value = "";
5238
- } }), _jsx("input", { ref: htmlInputRef, type: "file", accept: ".html,.htm,text/html", style: { display: "none" }, onChange: (e) => {
5239
- importTextFile(e.currentTarget.files, "html");
5240
- e.currentTarget.value = "";
5241
- } }), _jsx("input", { ref: mdInputRef, type: "file", accept: ".md,.markdown,text/markdown,text/plain", style: { display: "none" }, onChange: (e) => {
5242
- importTextFile(e.currentTarget.files, "md");
5243
- e.currentTarget.value = "";
5244
- } }), _jsxs(ToolbarGroup, { label: "History", children: [_jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Undo", title: "Undo", onClick: () => exec("undo"), children: _jsx(ToolbarIcon, { name: "undo" }) }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Redo", title: "Redo", onClick: () => exec("redo"), children: _jsx(ToolbarIcon, { name: "redo" }) })] }), _jsxs("select", { value: currentBlockType, onPointerDown: preserveEditorSelection, onMouseDown: preserveEditorSelection, onChange: (e) => {
5345
+ } })), _jsxs(ToolbarGroup, { label: "History", children: [_jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Undo", title: "Undo", onClick: () => exec("undo"), children: _jsx(ToolbarIcon, { name: "undo" }) }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Redo", title: "Redo", onClick: () => exec("redo"), children: _jsx(ToolbarIcon, { name: "redo" }) })] }), blockTypeFeature && _jsxs("select", { value: currentBlockType, onPointerDown: preserveEditorSelection, onMouseDown: preserveEditorSelection, onChange: (e) => {
5245
5346
  const val = e.target.value;
5246
5347
  if (val === "p")
5247
5348
  applyFormatBlock("<p>");
@@ -5264,56 +5365,57 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
5264
5365
  borderRadius: 6,
5265
5366
  background: "var(--srte-input-bg)",
5266
5367
  color: "var(--srte-input-text)",
5267
- }, children: [_jsx("option", { value: "mixed", disabled: true, children: "Mixed" }), _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("option", { value: "h4", children: "Heading 4" }), _jsx("option", { value: "h5", children: "Heading 5" }), _jsx("option", { value: "h6", children: "Heading 6" })] }), _jsx("button", { type: "button", className: `srte-tool-button${activeState.bold ? " srte-active" : ""}`, title: "Bold", onClick: () => exec("bold"), "aria-pressed": activeState.bold, style: activeButtonStyle(activeState.bold), children: _jsx("span", { style: { fontWeight: 700 }, children: "B" }) }), _jsx("button", { type: "button", className: `srte-tool-button${activeState.italic ? " srte-active" : ""}`, title: "Italic", onClick: () => exec("italic"), "aria-pressed": activeState.italic, style: activeButtonStyle(activeState.italic, { fontStyle: "italic" }), children: "I" }), _jsx("button", { type: "button", className: `srte-tool-button${activeState.underline ? " srte-active" : ""}`, title: "Underline", onClick: () => exec("underline"), "aria-pressed": activeState.underline, style: activeButtonStyle(activeState.underline, { textDecoration: "underline" }), children: "U" }), _jsx("button", { type: "button", className: `srte-tool-button${activeState.strikeThrough ? " srte-active" : ""}`, title: "Strikethrough", onClick: () => exec("strikeThrough"), "aria-pressed": activeState.strikeThrough, style: activeButtonStyle(activeState.strikeThrough, { textDecoration: "line-through" }), children: "S" }), showFontSize && (_jsxs("select", { value: currentFontSize, onPointerDown: preserveEditorSelection, onMouseDown: preserveEditorSelection, onChange: (e) => applyFontSize(e.target.value), title: "Font Size", style: {
5268
- height: 32,
5269
- padding: "0 8px",
5270
- border: "1px solid var(--srte-input-border)",
5271
- borderRadius: 6,
5272
- background: "var(--srte-input-bg)",
5273
- color: "var(--srte-input-text)",
5274
- }, children: [_jsx("option", { value: "", disabled: true, children: "Size" }), currentFontSize && !["8", "9", "10", "11", "12", "14", "16", "18", "24", "30", "36", "48", "60", "72", "96"].includes(currentFontSize) && (_jsx("option", { value: currentFontSize, children: currentFontSize })), _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: "16", children: "16" }), _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: () => {
5275
- const sel = window.getSelection();
5276
- if (sel && sel.rangeCount > 0) {
5277
- const range = sel.getRangeAt(0);
5278
- const editor = editableRef.current;
5279
- if (editor && editor.contains(range.commonAncestorContainer) && !range.collapsed) {
5280
- savedRangeRef.current = range.cloneRange();
5281
- }
5282
- }
5283
- }, onChange: (e) => applyFontFamily(e.target.value), title: "Font Family", style: {
5284
- height: 32,
5285
- padding: "0 8px",
5286
- border: "1px solid var(--srte-input-border)",
5287
- borderRadius: 6,
5288
- background: "var(--srte-input-bg)",
5289
- color: "var(--srte-input-text)",
5290
- maxWidth: 100,
5291
- }, children: [_jsx("option", { value: "", disabled: true, children: "Font" }), fonts.map((f) => (_jsx("option", { value: f.value, children: f.name }, f.value)))] })), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Text color", title: "Text Color", onClick: () => {
5292
- setColorPickerType('text');
5293
- setShowColorPicker(true);
5294
- }, style: {
5295
- height: 32,
5296
- minWidth: 32,
5297
- padding: "0 8px",
5298
- border: "1px solid var(--srte-input-border)",
5299
- borderRadius: 6,
5300
- background: "var(--srte-input-bg)",
5301
- color: "var(--srte-input-text)",
5302
- position: "relative",
5303
- }, children: _jsx("span", { style: { fontWeight: 700, borderBottom: "3px solid currentColor", lineHeight: 1 }, children: "A" }) }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Background color", title: "Background Color", onClick: () => {
5304
- setColorPickerType('background');
5305
- setShowColorPicker(true);
5306
- }, style: {
5307
- height: 32,
5308
- minWidth: 32,
5309
- padding: "0 8px",
5310
- border: "1px solid var(--srte-input-border)",
5311
- borderRadius: 6,
5312
- background: "var(--srte-input-bg)",
5313
- color: "var(--srte-input-text)",
5314
- }, children: _jsx("span", { style: { fontWeight: 700, padding: "1px 4px", background: "var(--srte-accent-bg)", borderRadius: 3 }, children: "A" }) }), _jsxs("button", { type: "button", className: `srte-tool-button${activeState.subscript ? " srte-active" : ""}`, title: "Subscript", onMouseDown: preserveEditorSelection, onClick: () => toggleInlineScript("subscript"), "aria-pressed": activeState.subscript, style: activeButtonStyle(activeState.subscript), children: ["X", _jsx("sub", { children: "2" })] }), _jsxs("button", { type: "button", className: `srte-tool-button${activeState.superscript ? " srte-active" : ""}`, title: "Superscript", onMouseDown: preserveEditorSelection, onClick: () => toggleInlineScript("superscript"), "aria-pressed": activeState.superscript, style: activeButtonStyle(activeState.superscript), children: ["X", _jsx("sup", { children: "2" })] }), [
5368
+ }, children: [_jsx("option", { value: "mixed", disabled: true, children: "Mixed" }), _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("option", { value: "h4", children: "Heading 4" }), _jsx("option", { value: "h5", children: "Heading 5" }), _jsx("option", { value: "h6", children: "Heading 6" })] }), basicFormattingFeature && _jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: `srte-tool-button${activeState.bold ? " srte-active" : ""}`, title: "Bold", onClick: () => exec("bold"), "aria-pressed": activeState.bold, style: activeButtonStyle(activeState.bold), children: _jsx("span", { style: { fontWeight: 700 }, children: "B" }) }), _jsx("button", { type: "button", className: `srte-tool-button${activeState.italic ? " srte-active" : ""}`, title: "Italic", onClick: () => exec("italic"), "aria-pressed": activeState.italic, style: activeButtonStyle(activeState.italic, { fontStyle: "italic" }), children: "I" }), _jsx("button", { type: "button", className: `srte-tool-button${activeState.underline ? " srte-active" : ""}`, title: "Underline", onClick: () => exec("underline"), "aria-pressed": activeState.underline, style: activeButtonStyle(activeState.underline, { textDecoration: "underline" }), children: "U" }), _jsx("button", { type: "button", className: `srte-tool-button${activeState.strikeThrough ? " srte-active" : ""}`, title: "Strikethrough", onClick: () => exec("strikeThrough"), "aria-pressed": activeState.strikeThrough, style: activeButtonStyle(activeState.strikeThrough, { textDecoration: "line-through" }), children: "S" }), showFontSize && (_jsxs("select", { value: currentFontSize, onPointerDown: preserveEditorSelection, onMouseDown: preserveEditorSelection, onChange: (e) => applyFontSize(e.target.value), title: "Font Size", style: {
5369
+ height: 32,
5370
+ padding: "0 8px",
5371
+ border: "1px solid var(--srte-input-border)",
5372
+ borderRadius: 6,
5373
+ background: "var(--srte-input-bg)",
5374
+ color: "var(--srte-input-text)",
5375
+ }, children: [_jsx("option", { value: "", disabled: true, children: "Size" }), currentFontSize && !["8", "9", "10", "11", "12", "14", "16", "18", "24", "30", "36", "48", "60", "72", "96"].includes(currentFontSize) && (_jsx("option", { value: currentFontSize, children: currentFontSize })), _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: "16", children: "16" }), _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: () => {
5376
+ const sel = window.getSelection();
5377
+ if (sel && sel.rangeCount > 0) {
5378
+ const range = sel.getRangeAt(0);
5379
+ const editor = editableRef.current;
5380
+ if (editor && editor.contains(range.commonAncestorContainer) && !range.collapsed) {
5381
+ savedRangeRef.current = range.cloneRange();
5382
+ }
5383
+ }
5384
+ }, onChange: (e) => applyFontFamily(e.target.value), title: "Font Family", style: {
5385
+ height: 32,
5386
+ padding: "0 8px",
5387
+ border: "1px solid var(--srte-input-border)",
5388
+ borderRadius: 6,
5389
+ background: "var(--srte-input-bg)",
5390
+ color: "var(--srte-input-text)",
5391
+ maxWidth: 100,
5392
+ }, children: [_jsx("option", { value: "", disabled: true, children: "Font" }), fonts.map((f) => (_jsx("option", { value: f.value, children: f.name }, f.value)))] })), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Text color", title: "Text Color", onMouseDown: preserveEditorSelection, onClick: () => {
5393
+ setColorPickerType('text');
5394
+ setShowColorPicker(true);
5395
+ }, style: {
5396
+ height: 32,
5397
+ minWidth: 32,
5398
+ padding: "0 8px",
5399
+ border: "1px solid var(--srte-input-border)",
5400
+ borderRadius: 6,
5401
+ background: "var(--srte-input-bg)",
5402
+ color: "var(--srte-input-text)",
5403
+ position: "relative",
5404
+ }, children: _jsx("span", { style: { fontWeight: 700, borderBottom: `3px solid ${currentTextColor}`, lineHeight: 1 }, children: "A" }) }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Background color", title: "Background Color", onMouseDown: preserveEditorSelection, onClick: () => {
5405
+ setColorPickerType('background');
5406
+ setShowColorPicker(true);
5407
+ }, style: {
5408
+ height: 32,
5409
+ minWidth: 32,
5410
+ padding: "0 8px",
5411
+ border: "1px solid var(--srte-input-border)",
5412
+ borderRadius: 6,
5413
+ background: "var(--srte-input-bg)",
5414
+ color: "var(--srte-input-text)",
5415
+ }, children: _jsx("span", { style: { fontWeight: 700, padding: "1px 4px", background: currentBackgroundColor, color: readableTextColorForBackground(currentBackgroundColor) || "currentColor", borderRadius: 3 }, children: "A" }) }), _jsxs("button", { type: "button", className: `srte-tool-button${activeState.subscript ? " srte-active" : ""}`, title: "Subscript", onMouseDown: preserveEditorSelection, onClick: () => toggleInlineScript("subscript"), "aria-pressed": activeState.subscript, style: activeButtonStyle(activeState.subscript), children: ["X", _jsx("sub", { children: "2" })] }), _jsxs("button", { type: "button", className: `srte-tool-button${activeState.superscript ? " srte-active" : ""}`, title: "Superscript", onMouseDown: preserveEditorSelection, onClick: () => toggleInlineScript("superscript"), "aria-pressed": activeState.superscript, style: activeButtonStyle(activeState.superscript), children: ["X", _jsx("sup", { children: "2" })] })] }), [
5315
5416
  {
5316
5417
  key: "check",
5418
+ enabled: checklistFeature,
5317
5419
  icon: "checklist",
5318
5420
  title: "Checklist",
5319
5421
  active: activeState.checklist,
@@ -5322,21 +5424,37 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
5322
5424
  },
5323
5425
  {
5324
5426
  key: "bullet",
5427
+ enabled: listFeature,
5325
5428
  icon: "bullets",
5326
5429
  title: "Bulleted list",
5327
5430
  active: activeState.unorderedList,
5328
- action: () => applyListStyle("bullet:disc"),
5329
- options: [["bullet:disc", "• Disc"], ["bullet:circle", "○ Circle"], ["bullet:square", "▪ Square"]],
5431
+ action: () => toggleList("ul"),
5432
+ options: [
5433
+ ...SMART_LIST_PRESETS.filter((preset) => preset.kind === "bullet")
5434
+ .map((preset) => [`bullet-preset:${preset.id}`, preset.label, false]),
5435
+ ["bullet:disc", "• Disc", true],
5436
+ ["bullet:circle", "○ Circle", true],
5437
+ ["bullet:square", "▪ Square", true],
5438
+ ],
5330
5439
  },
5331
5440
  {
5332
5441
  key: "ordered",
5442
+ enabled: listFeature,
5333
5443
  icon: "numbers",
5334
5444
  title: "Numbered list",
5335
5445
  active: activeState.orderedList,
5336
- action: () => applyListStyle("ordered:decimal"),
5337
- options: [["ordered:decimal", "1. 2. 3."], ["ordered:lower-alpha", "a. b. c."], ["ordered:upper-alpha", "A. B. C."], ["ordered:lower-roman", "i. ii. iii."], ["ordered:upper-roman", "I. II. III."]],
5446
+ action: () => toggleList("ol"),
5447
+ options: [
5448
+ ...SMART_LIST_PRESETS.filter((preset) => preset.kind === "ordered")
5449
+ .map((preset) => [`ordered-preset:${preset.id}`, preset.label, false]),
5450
+ ["ordered:decimal", "1. 2. 3.", true],
5451
+ ["ordered:lower-alpha", "a. b. c.", true],
5452
+ ["ordered:upper-alpha", "A. B. C.", true],
5453
+ ["ordered:lower-roman", "i. ii. iii.", true],
5454
+ ["ordered:upper-roman", "I. II. III.", true],
5455
+ ],
5338
5456
  },
5339
- ].map((control) => (_jsxs("span", { className: "srte-split-control", children: [_jsx("button", { type: "button", className: `srte-tool-button${control.active ? " srte-active" : ""}`, title: control.title, onPointerDown: preserveEditorSelection, onClick: control.action, "aria-pressed": control.active, children: _jsx(ToolbarIcon, { name: control.icon }) }), _jsxs("select", { defaultValue: "", "aria-label": `${control.title} styles`, title: `${control.title} styles`, onPointerDown: preserveEditorSelection, onMouseDown: preserveEditorSelection, onChange: (event) => {
5457
+ ].filter((control) => control.enabled).map((control) => (_jsxs("span", { className: "srte-split-control", children: [_jsx("button", { type: "button", className: `srte-tool-button${control.active ? " srte-active" : ""}`, title: control.title, onPointerDown: preserveEditorSelection, onClick: control.action, "aria-pressed": control.active, children: _jsx(ToolbarIcon, { name: control.icon }) }), _jsxs("select", { defaultValue: "", "aria-label": `${control.title} styles`, title: `${control.title} styles`, onPointerDown: preserveEditorSelection, onMouseDown: preserveEditorSelection, onChange: (event) => {
5340
5458
  const selected = event.currentTarget.value;
5341
5459
  if (selected === "check:plain")
5342
5460
  applyChecklist(false);
@@ -5345,7 +5463,10 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
5345
5463
  else if (selected)
5346
5464
  applyListStyle(selected);
5347
5465
  event.currentTarget.value = "";
5348
- }, children: [_jsx("option", { value: "", disabled: true, children: "Style" }), control.options.map(([value, label]) => _jsx("option", { value: value, children: label }, value))] })] }, control.key))), _jsx("button", { type: "button", className: `srte-tool-button${activeState.blockquote ? " srte-active" : ""}`, title: "Blockquote", onPointerDown: preserveEditorSelection, onClick: toggleBlockquote, "aria-pressed": activeState.blockquote, children: _jsx(ToolbarIcon, { name: "quote" }) }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Special characters", title: "Special characters", onClick: () => setShowSpecialChars(true), children: _jsx(ToolbarIcon, { name: "omega" }) }), _jsx("button", { type: "button", className: `srte-tool-button${activeState.codeBlock ? " srte-active" : ""}`, title: "Code block", onPointerDown: preserveEditorSelection, onClick: toggleCodeBlock, "aria-pressed": activeState.codeBlock, children: _jsx(ToolbarIcon, { name: "code" }) }), _jsx("button", { type: "button", className: `srte-tool-button${activeState.link ? " srte-active" : ""}`, title: "Insert link", "aria-label": "Insert or edit link", "aria-pressed": activeState.link, onPointerDown: preserveEditorSelection, onClick: () => openLinkEditor(), children: _jsx(ToolbarIcon, { name: "link" }) }), (table || media || formula) && (_jsxs(ToolbarMenu, { label: "Insert", icon: "insert", priority: 2, children: [table && _jsx(MenuItem, { icon: "table", label: "Table", title: "Insert table", onClick: () => setShowTableDialog(true) }), media && _jsx(MenuItem, { icon: "image", label: "Upload image", title: "Insert image", onClick: insertImage }), media && _jsx(MenuItem, { icon: "media", label: "Media", title: "Open media", onClick: () => mediaManager ? setShowMediaManager(true) : insertImage() }), formula && _jsx(MenuItem, { icon: "formula", label: "Formula", title: "Insert formula", onClick: () => setShowFormulaDialog(true) })] })), selectedImage && (_jsxs(ToolbarMenu, { label: "Image alignment", icon: "image", children: [_jsx(MenuItem, { icon: "align-center", label: "Center image", onClick: () => {
5466
+ }, children: [_jsx("option", { value: "", disabled: true, children: "Style" }), control.options.map(([value, label, hidden]) => _jsx("option", { value: value, hidden: hidden, children: label }, value))] })] }, control.key))), blockquoteFeature && _jsx("button", { type: "button", className: `srte-tool-button${activeState.blockquote ? " srte-active" : ""}`, title: "Blockquote", onPointerDown: preserveEditorSelection, onClick: toggleBlockquote, "aria-pressed": activeState.blockquote, children: _jsx(ToolbarIcon, { name: "quote" }) }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Special characters", title: "Special characters", onClick: () => setShowSpecialChars(true), children: _jsx(ToolbarIcon, { name: "omega" }) }), codeBlockFeature && _jsx("button", { type: "button", className: `srte-tool-button${activeState.codeBlock ? " srte-active" : ""}`, title: "Code block", onPointerDown: preserveEditorSelection, onClick: toggleCodeBlock, "aria-pressed": activeState.codeBlock, children: _jsx(ToolbarIcon, { name: "code" }) }), basicFormattingFeature && _jsx("button", { type: "button", className: `srte-tool-button${activeState.link ? " srte-active" : ""}`, title: "Insert link", "aria-label": "Insert or edit link", "aria-pressed": activeState.link, onPointerDown: preserveEditorSelection, onClick: () => openLinkEditor(), children: _jsx(ToolbarIcon, { name: "link" }) }), pluginRuntime.toolbar
5467
+ .filter((contribution) => (!contribution.placement || contribution.placement === "main") &&
5468
+ isPluginContributionVisible(contribution))
5469
+ .map((contribution) => (_jsx("button", { type: "button", className: "srte-tool-button", disabled: !isPluginContributionEnabled(contribution), "aria-pressed": isPluginContributionActive(contribution), "aria-label": contribution.label, title: contribution.title || contribution.label, onPointerDown: preserveEditorSelection, onClick: () => executePluginToolbarContribution(contribution), children: contribution.icon || contribution.label.slice(0, 2) }, contribution.id))), (table || media || formula || pluginRuntime.toolbar.some((item) => item.placement === "insert")) && (_jsxs(ToolbarMenu, { label: "Insert", icon: "insert", priority: 2, children: [table && _jsx(MenuItem, { icon: "table", label: "Table", title: "Insert table", onClick: () => setShowTableDialog(true) }), media && _jsx(MenuItem, { icon: "image", label: "Upload image", title: "Insert image", onClick: insertImage }), media && _jsx(MenuItem, { icon: "media", label: "Media", title: "Open media", onClick: () => mediaManager ? setShowMediaManager(true) : insertImage() }), formula && _jsx(MenuItem, { icon: "formula", label: "Formula", title: "Insert formula", onClick: () => setShowFormulaDialog(true) }), pluginRuntime.toolbar.filter((item) => item.placement === "insert" && isPluginContributionVisible(item)).map((contribution) => (_jsxs("button", { type: "button", role: "menuitem", className: "srte-menu-item", disabled: !isPluginContributionEnabled(contribution), title: contribution.title || contribution.label, onClick: () => executePluginToolbarContribution(contribution), children: [_jsx("span", { "aria-hidden": "true", children: contribution.icon || "+" }), _jsx("span", { children: contribution.label })] }, contribution.id)))] })), selectedImage && (_jsxs(ToolbarMenu, { label: "Image alignment", icon: "image", children: [_jsx(MenuItem, { icon: "align-center", label: "Center image", onClick: () => {
5349
5470
  selectedImage.style.display = "block";
5350
5471
  selectedImage.style.margin = "0 auto";
5351
5472
  selectedImage.style.float = "none";
@@ -5363,17 +5484,10 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
5363
5484
  selectedImage.style.margin = "0 0 8px 8px";
5364
5485
  scheduleImageOverlay();
5365
5486
  handleInput();
5366
- } })] })), _jsxs(ToolbarGroup, { label: "Document", priority: 3, children: [_jsxs(ToolbarMenu, { label: loadingPdf || loadingDocx ? "Importing document" : "Import document", icon: "import", children: [_jsx(MenuItem, { icon: "import", label: "PDF (.pdf)", disabled: loadingPdf || loadingDocx, onClick: () => pdfInputRef.current?.click() }), _jsx(MenuItem, { icon: "import", label: "Microsoft Word (.docx)", disabled: loadingPdf || loadingDocx, onClick: () => docxInputRef.current?.click() }), _jsx(MenuItem, { icon: "import", label: "HTML (.html)", disabled: loadingPdf || loadingDocx, onClick: () => htmlInputRef.current?.click() }), _jsx(MenuItem, { icon: "import", label: "Markdown (.md)", disabled: loadingPdf || loadingDocx, onClick: () => mdInputRef.current?.click() })] }), _jsxs(ToolbarMenu, { label: "Export document", icon: "export", children: [_jsx(MenuItem, { icon: "export", label: "PDF", onClick: exportPdf }), _jsx(MenuItem, { icon: "export", label: "Microsoft Word (.docx)", onClick: () => void exportDocx() }), _jsx(MenuItem, { icon: "export", label: "HTML", onClick: exportHtml }), _jsx(MenuItem, { icon: "export", label: "Markdown", onClick: exportMarkdown })] }), _jsxs("select", { className: "srte-command-proxy", "aria-hidden": "true", tabIndex: -1, defaultValue: "", "aria-label": "Import document", title: "Import document", disabled: loadingPdf || loadingDocx, onChange: (event) => {
5487
+ } })] })), formatRuntime.formats.length > 0 && _jsxs(ToolbarGroup, { label: "Document", priority: 3, children: [formatRuntime.imports.length > 0 && _jsx(ToolbarMenu, { label: loadingPdf || loadingDocx ? "Importing document" : "Import document", icon: "import", children: formatRuntime.imports.map((format) => _jsx(MenuItem, { icon: "import", label: `${format.label} (.${format.extension})`, disabled: loadingPdf || loadingDocx, onClick: () => openFormatImport(format.id) }, format.id)) }), formatRuntime.exports.length > 0 && _jsx(ToolbarMenu, { label: "Export document", icon: "export", children: formatRuntime.exports.map((format) => _jsx(MenuItem, { icon: "export", label: format.label, onClick: () => void runFormatExport(format.id) }, format.id)) }), _jsxs("select", { className: "srte-command-proxy", "aria-hidden": "true", tabIndex: -1, defaultValue: "", "aria-label": "Import document", title: "Import document", disabled: loadingPdf || loadingDocx, onChange: (event) => {
5367
5488
  const format = event.currentTarget.value;
5368
5489
  event.currentTarget.value = "";
5369
- if (format === "pdf")
5370
- pdfInputRef.current?.click();
5371
- else if (format === "docx")
5372
- docxInputRef.current?.click();
5373
- else if (format === "html")
5374
- htmlInputRef.current?.click();
5375
- else if (format === "markdown")
5376
- mdInputRef.current?.click();
5490
+ openFormatImport(format);
5377
5491
  }, style: {
5378
5492
  height: 32,
5379
5493
  padding: "0 8px",
@@ -5382,17 +5496,10 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
5382
5496
  background: "var(--srte-input-bg)",
5383
5497
  color: "var(--srte-input-text)",
5384
5498
  opacity: loadingPdf || loadingDocx ? 0.6 : 1,
5385
- }, children: [_jsx("option", { value: "", disabled: true, children: loadingPdf || loadingDocx ? "Importing…" : "Import…" }), _jsx("option", { value: "pdf", children: "PDF (.pdf)" }), _jsx("option", { value: "docx", children: "Word (.docx)" }), _jsx("option", { value: "html", children: "HTML (.html)" }), _jsx("option", { value: "markdown", children: "Markdown (.md)" })] }), _jsxs("select", { className: "srte-command-proxy", "aria-hidden": "true", tabIndex: -1, defaultValue: "", "aria-label": "Export document", title: "Export document", onChange: (event) => {
5499
+ }, children: [_jsx("option", { value: "", disabled: true, children: loadingPdf || loadingDocx ? "Importing…" : "Import…" }), formatRuntime.imports.map((format) => _jsxs("option", { value: format.id, children: [proxyFormatLabel(format), " (.", format.extension, ")"] }, format.id))] }), _jsxs("select", { className: "srte-command-proxy", "aria-hidden": "true", tabIndex: -1, defaultValue: "", "aria-label": "Export document", title: "Export document", onChange: (event) => {
5386
5500
  const format = event.currentTarget.value;
5387
5501
  event.currentTarget.value = "";
5388
- if (format === "html")
5389
- exportHtml();
5390
- else if (format === "markdown")
5391
- exportMarkdown();
5392
- else if (format === "docx")
5393
- void exportDocx();
5394
- else if (format === "pdf")
5395
- exportPdf();
5502
+ void runFormatExport(format);
5396
5503
  }, style: {
5397
5504
  height: 32,
5398
5505
  padding: "0 8px",
@@ -5400,7 +5507,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
5400
5507
  borderRadius: 6,
5401
5508
  background: "var(--srte-input-bg)",
5402
5509
  color: "var(--srte-input-text)",
5403
- }, children: [_jsx("option", { value: "", disabled: true, children: "Export\u2026" }), _jsx("option", { value: "html", children: "HTML (.html)" }), _jsx("option", { value: "markdown", children: "Markdown (.md)" }), _jsx("option", { value: "docx", children: "Word (.docx)" }), _jsx("option", { value: "pdf", children: "PDF (.pdf)" })] })] }), _jsx(ToolbarMenu, { label: "Text alignment", icon: currentAlignment === "center" ? "align-center" : currentAlignment === "right" ? "align-right" : currentAlignment === "justify" ? "justify" : "align-left", active: currentAlignment !== "left", children: ["left", "center", "right", "justify"].map((alignment) => (_jsx(MenuItem, { icon: alignment === "left" ? "align-left" : alignment === "center" ? "align-center" : alignment === "right" ? "align-right" : "justify", label: alignment === "justify" ? "Justify" : `Align ${alignment}`, title: alignment === "justify" ? "Justify" : `Align ${alignment}`, active: currentAlignment === alignment, onClick: () => applyTextAlignment(alignment) }, alignment))) }), _jsxs(ToolbarMenu, { label: "Move and indent", icon: "move", priority: 2, children: [_jsx(MenuItem, { icon: "up", label: "Move block up", title: "Move selected block up", onClick: () => moveCurrentElement("up") }), _jsx(MenuItem, { icon: "down", label: "Move block down", title: "Move selected block down", onClick: () => moveCurrentElement("down") }), _jsx("div", { className: "srte-menu-separator", role: "separator" }), _jsx(MenuItem, { icon: "outdent", label: "Decrease indent", title: "Move selected block left", onClick: () => moveCurrentElement("left") }), _jsx(MenuItem, { icon: "indent", label: "Increase indent", title: "Move selected block right", onClick: () => moveCurrentElement("right") })] }), _jsx("div", { className: "srte-mobile-more", children: _jsxs(ToolbarMenu, { label: "More editor actions", icon: "more", children: [table && _jsx(MenuItem, { icon: "table", label: "Insert table", onClick: () => setShowTableDialog(true) }), media && _jsx(MenuItem, { icon: "image", label: "Upload image", onClick: insertImage }), media && _jsx(MenuItem, { icon: "media", label: "Media", onClick: () => mediaManager ? setShowMediaManager(true) : insertImage() }), formula && _jsx(MenuItem, { icon: "formula", label: "Insert formula", onClick: () => setShowFormulaDialog(true) }), _jsx(MenuItem, { icon: "omega", label: "Special characters", onClick: () => setShowSpecialChars(true) }), _jsx(MenuItem, { icon: "up", label: "Move block up", onClick: () => moveCurrentElement("up") }), _jsx(MenuItem, { icon: "down", label: "Move block down", onClick: () => moveCurrentElement("down") }), _jsx(MenuItem, { icon: "outdent", label: "Decrease indent", onClick: () => moveCurrentElement("left") }), _jsx(MenuItem, { icon: "indent", label: "Increase indent", onClick: () => moveCurrentElement("right") }), _jsx("div", { className: "srte-menu-separator", role: "separator" }), _jsx(MenuItem, { icon: "import", label: "Import Word document", onClick: () => docxInputRef.current?.click() }), _jsx(MenuItem, { icon: "import", label: "Import HTML", onClick: () => htmlInputRef.current?.click() }), _jsx(MenuItem, { icon: "import", label: "Import Markdown", onClick: () => mdInputRef.current?.click() }), _jsx("div", { className: "srte-menu-separator", role: "separator" }), _jsx(MenuItem, { icon: "export", label: "Export PDF", onClick: exportPdf }), _jsx(MenuItem, { icon: "export", label: "Export Word document", onClick: () => void exportDocx() }), _jsx(MenuItem, { icon: "export", label: "Export HTML", onClick: exportHtml }), _jsx(MenuItem, { icon: "export", label: "Export Markdown", onClick: exportMarkdown })] }) })] }), media && mediaManager && (_jsx(MediaManager, { open: showMediaManager, onClose: () => setShowMediaManager(false), adapter: mediaManager, onSelect: (item) => {
5510
+ }, children: [_jsx("option", { value: "", disabled: true, children: "Export\u2026" }), proxyExportFormats.map((format) => _jsxs("option", { value: format.id, children: [proxyFormatLabel(format), " (.", format.extension, ")"] }, format.id))] })] }), alignmentFeature && _jsx(ToolbarMenu, { label: "Text alignment", icon: currentAlignment === "center" ? "align-center" : currentAlignment === "right" ? "align-right" : currentAlignment === "justify" ? "justify" : "align-left", active: currentAlignment !== "left", children: ["left", "center", "right", "justify"].map((alignment) => (_jsx(MenuItem, { icon: alignment === "left" ? "align-left" : alignment === "center" ? "align-center" : alignment === "right" ? "align-right" : "justify", label: alignment === "justify" ? "Justify" : `Align ${alignment}`, title: alignment === "justify" ? "Justify" : `Align ${alignment}`, active: currentAlignment === alignment, onClick: () => applyTextAlignment(alignment) }, alignment))) }), moveFeature && _jsxs(ToolbarMenu, { label: "Move and indent", icon: "move", priority: 2, children: [_jsx(MenuItem, { icon: "up", label: "Move block up", title: "Move selected block up", onClick: () => moveCurrentElement("up") }), _jsx(MenuItem, { icon: "down", label: "Move block down", title: "Move selected block down", onClick: () => moveCurrentElement("down") }), _jsx("div", { className: "srte-menu-separator", role: "separator" }), _jsx(MenuItem, { icon: "outdent", label: "Decrease indent", title: "Move selected block left", onClick: () => moveCurrentElement("left") }), _jsx(MenuItem, { icon: "indent", label: "Increase indent", title: "Move selected block right", onClick: () => moveCurrentElement("right") })] }), _jsx("div", { className: "srte-mobile-more", children: _jsxs(ToolbarMenu, { label: "More editor actions", icon: "more", children: [table && _jsx(MenuItem, { icon: "table", label: "Insert table", onClick: () => setShowTableDialog(true) }), media && _jsx(MenuItem, { icon: "image", label: "Upload image", onClick: insertImage }), media && _jsx(MenuItem, { icon: "media", label: "Media", onClick: () => mediaManager ? setShowMediaManager(true) : insertImage() }), formula && _jsx(MenuItem, { icon: "formula", label: "Insert formula", onClick: () => setShowFormulaDialog(true) }), pluginRuntime.toolbar.filter((item) => item.placement === "more" && isPluginContributionVisible(item)).map((contribution) => (_jsxs("button", { type: "button", role: "menuitem", className: "srte-menu-item", disabled: !isPluginContributionEnabled(contribution), title: contribution.title || contribution.label, onClick: () => executePluginToolbarContribution(contribution), children: [_jsx("span", { "aria-hidden": "true", children: contribution.icon || "•" }), _jsx("span", { children: contribution.label })] }, contribution.id))), _jsx(MenuItem, { icon: "omega", label: "Special characters", onClick: () => setShowSpecialChars(true) }), moveFeature && _jsx(MenuItem, { icon: "up", label: "Move block up", onClick: () => moveCurrentElement("up") }), moveFeature && _jsx(MenuItem, { icon: "down", label: "Move block down", onClick: () => moveCurrentElement("down") }), moveFeature && _jsx(MenuItem, { icon: "outdent", label: "Decrease indent", onClick: () => moveCurrentElement("left") }), moveFeature && _jsx(MenuItem, { icon: "indent", label: "Increase indent", onClick: () => moveCurrentElement("right") }), _jsx("div", { className: "srte-menu-separator", role: "separator" }), formatRuntime.imports.map((format) => _jsx(MenuItem, { icon: "import", label: `Import ${format.label}`, onClick: () => openFormatImport(format.id) }, `mobile-import-${format.id}`)), _jsx("div", { className: "srte-menu-separator", role: "separator" }), formatRuntime.exports.map((format) => _jsx(MenuItem, { icon: "export", label: `Export ${format.label}`, onClick: () => void runFormatExport(format.id) }, `mobile-export-${format.id}`))] }) })] }), media && mediaManager && (_jsx(MediaManager, { open: showMediaManager, onClose: () => setShowMediaManager(false), adapter: mediaManager, onSelect: (item) => {
5404
5511
  if (item?.url)
5405
5512
  insertImageAtSelection(item);
5406
5513
  } })), table && showTableDialog && (_jsx("div", { style: {
@@ -5461,10 +5568,6 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
5461
5568
  zIndex: 100,
5462
5569
  }, onClick: () => {
5463
5570
  setPendingImport(null);
5464
- if (pdfInputRef.current)
5465
- pdfInputRef.current.value = "";
5466
- if (docxInputRef.current)
5467
- docxInputRef.current.value = "";
5468
5571
  }, children: _jsxs("div", { style: {
5469
5572
  background: "var(--srte-modal-bg)",
5470
5573
  color: "var(--srte-modal-text)",
@@ -5473,7 +5576,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
5473
5576
  maxWidth: 400,
5474
5577
  width: "90%",
5475
5578
  boxShadow: "var(--srte-menu-shadow)",
5476
- }, onClick: (e) => e.stopPropagation(), children: [_jsx("h3", { style: { margin: "0 0 12px 0", fontSize: 18, fontWeight: 600 }, children: "Import Content" }), _jsx("p", { style: { margin: "0 0 20px 0", color: "var(--srte-text-muted)", fontSize: 14 }, children: "The editor already contains content. How would you like to handle the imported document?" }), _jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 8 }, children: [_jsx("button", { onClick: () => processImport(pendingImport.file, pendingImport.type, 'replace'), style: {
5579
+ }, onClick: (e) => e.stopPropagation(), children: [_jsx("h3", { style: { margin: "0 0 12px 0", fontSize: 18, fontWeight: 600 }, children: "Import Content" }), _jsx("p", { style: { margin: "0 0 20px 0", color: "var(--srte-text-muted)", fontSize: 14 }, children: "The editor already contains content. How would you like to handle the imported document?" }), _jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 8 }, children: [_jsx("button", { onClick: () => void processFormatImport(pendingImport.definition, pendingImport.file, 'replace'), style: {
5477
5580
  padding: "8px 16px",
5478
5581
  background: "var(--srte-danger)",
5479
5582
  color: "var(--srte-on-primary)",
@@ -5482,7 +5585,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
5482
5585
  cursor: "pointer",
5483
5586
  fontWeight: 500,
5484
5587
  textAlign: "left"
5485
- }, children: "Replace Check existing content (Overwrite)" }), _jsx("button", { onClick: () => processImport(pendingImport.file, pendingImport.type, 'append'), style: {
5588
+ }, children: "Replace Check existing content (Overwrite)" }), _jsx("button", { onClick: () => void processFormatImport(pendingImport.definition, pendingImport.file, 'append'), style: {
5486
5589
  padding: "8px 16px",
5487
5590
  background: "var(--srte-primary)",
5488
5591
  color: "var(--srte-on-primary)",
@@ -5493,10 +5596,6 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
5493
5596
  textAlign: "left"
5494
5597
  }, children: "Append to bottom" }), _jsx("button", { onClick: () => {
5495
5598
  setPendingImport(null);
5496
- if (pdfInputRef.current)
5497
- pdfInputRef.current.value = "";
5498
- if (docxInputRef.current)
5499
- docxInputRef.current.value = "";
5500
5599
  }, style: {
5501
5600
  padding: "8px 16px",
5502
5601
  background: "var(--srte-cancel-bg)",
@@ -5859,6 +5958,14 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
5859
5958
  if (item && list) {
5860
5959
  pushEditorHistory();
5861
5960
  const checked = item.dataset.checked !== "true";
5961
+ const replacement = editorControllerRef.current
5962
+ .bindRoot(editableRef.current)
5963
+ .executeChecklistItemCommand(list, item, checked);
5964
+ if (replacement) {
5965
+ syncChecklistControls(replacement);
5966
+ handleInput();
5967
+ return;
5968
+ }
5862
5969
  item.dataset.checked = checked ? "true" : "false";
5863
5970
  t.dataset.checked = checked ? "true" : "false";
5864
5971
  t.setAttribute("aria-pressed", checked ? "true" : "false");
@@ -5930,7 +6037,30 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
5930
6037
  }
5931
6038
  updateActiveState();
5932
6039
  }, onKeyDown: (e) => {
5933
- if ((e.metaKey || e.ctrlKey) && String(e.key).toLowerCase() === "k") {
6040
+ if (e.key === "Backspace" || e.key === "Delete") {
6041
+ const selection = window.getSelection();
6042
+ const range = selection?.rangeCount ? selection.getRangeAt(0) : null;
6043
+ const atom = range
6044
+ ? adjacentInlineAtom(range, e.key === "Backspace" ? "backward" : "forward")
6045
+ : null;
6046
+ if (atom && editableRef.current) {
6047
+ pushEditorHistory();
6048
+ if (editorControllerRef.current.bindRoot(editableRef.current).deleteInlineAtom(atom)) {
6049
+ e.preventDefault();
6050
+ handleInput();
6051
+ return;
6052
+ }
6053
+ editorControllerRef.current.discardLastHistorySnapshot();
6054
+ }
6055
+ }
6056
+ const pluginShortcut = pluginRuntime.shortcuts.find((shortcut) => matchesPluginShortcut(e, shortcut) &&
6057
+ isPluginShortcutAvailable(shortcut));
6058
+ if (pluginShortcut &&
6059
+ executePluginCommand(pluginShortcut.commandId, pluginShortcut.input)) {
6060
+ e.preventDefault();
6061
+ return;
6062
+ }
6063
+ if (basicFormattingFeature && (e.metaKey || e.ctrlKey) && String(e.key).toLowerCase() === "k") {
5934
6064
  e.preventDefault();
5935
6065
  openLinkEditor();
5936
6066
  return;
@@ -6049,6 +6179,31 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
6049
6179
  window.addEventListener("mouseup", onUp);
6050
6180
  }, onContextMenu: (e) => {
6051
6181
  const target = e.target;
6182
+ const contextItems = pluginRuntime.contextMenu.filter((item) => {
6183
+ try {
6184
+ return isPluginContributionVisible(item) && (!item.when || item.when({
6185
+ root: e.currentTarget,
6186
+ target,
6187
+ selection: window.getSelection(),
6188
+ }));
6189
+ }
6190
+ catch {
6191
+ return false;
6192
+ }
6193
+ });
6194
+ if (contextItems.length) {
6195
+ e.preventDefault();
6196
+ preserveEditorSelection();
6197
+ setPluginContextMenu({
6198
+ x: Math.max(8, Math.min(e.clientX, window.innerWidth - 228)),
6199
+ y: Math.max(8, Math.min(e.clientY, window.innerHeight - 48 - contextItems.length * 36)),
6200
+ items: contextItems,
6201
+ });
6202
+ setImageMenu(null);
6203
+ setTableMenu(null);
6204
+ return;
6205
+ }
6206
+ setPluginContextMenu(null);
6052
6207
  if (target && target.tagName === "IMG") {
6053
6208
  e.preventDefault();
6054
6209
  const vw = window.innerWidth;
@@ -6076,55 +6231,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
6076
6231
  setTableMenu(null);
6077
6232
  setImageMenu(null);
6078
6233
  }
6079
- } }), dragHandle && !readOnly && (_jsx("button", { type: "button", draggable: true, "data-srte-drag-handle": "true", title: "Drag block", "aria-label": "Drag block", onMouseEnter: () => {
6080
- if (dragHandleHideTimerRef.current != null) {
6081
- window.clearTimeout(dragHandleHideTimerRef.current);
6082
- dragHandleHideTimerRef.current = null;
6083
- }
6084
- }, onMouseLeave: () => {
6085
- if (!draggedBlockRef.current)
6086
- scheduleDragHandleHide();
6087
- }, onMouseDown: (e) => {
6088
- e.stopPropagation();
6089
- }, onDragStart: (e) => {
6090
- draggedBlockRef.current = dragHandle.target;
6091
- e.dataTransfer.effectAllowed = "move";
6092
- e.dataTransfer.setData("text/plain", "moving-block");
6093
- try {
6094
- const ghost = dragHandle.target.cloneNode(true);
6095
- ghost.style.position = "fixed";
6096
- ghost.style.left = "-10000px";
6097
- ghost.style.top = "-10000px";
6098
- ghost.style.width = `${Math.min(dragHandle.target.getBoundingClientRect().width, 480)}px`;
6099
- ghost.style.opacity = "0.75";
6100
- document.body.appendChild(ghost);
6101
- e.dataTransfer.setDragImage(ghost, 12, 12);
6102
- window.setTimeout(() => ghost.remove(), 0);
6103
- }
6104
- catch { }
6105
- }, onDragEnd: () => {
6106
- draggedBlockRef.current = null;
6107
- updateDragHandleForTarget(dragHandle.target);
6108
- }, style: {
6109
- position: "absolute",
6110
- left: dragHandle.left,
6111
- top: dragHandle.top + Math.max(0, (dragHandle.height - 24) / 2),
6112
- width: 24,
6113
- height: 24,
6114
- display: "inline-flex",
6115
- alignItems: "center",
6116
- justifyContent: "center",
6117
- border: "1px solid var(--srte-border)",
6118
- borderRadius: 6,
6119
- background: "var(--srte-input-bg)",
6120
- color: "var(--srte-input-text)",
6121
- boxShadow: "var(--srte-menu-shadow)",
6122
- cursor: "grab",
6123
- zIndex: 8,
6124
- fontSize: 14,
6125
- lineHeight: 1,
6126
- padding: 0,
6127
- }, children: "\u22EE\u22EE" })), selectedImage && imageOverlay && (_jsxs("div", { style: {
6234
+ } }), selectedImage && imageOverlay && (_jsxs("div", { style: {
6128
6235
  position: "absolute",
6129
6236
  left: imageOverlay.left,
6130
6237
  top: imageOverlay.top,
@@ -6160,6 +6267,11 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
6160
6267
  window.removeEventListener("mousemove", onMove);
6161
6268
  window.removeEventListener("mouseup", onUp);
6162
6269
  resizingRef.current = null;
6270
+ if (editableRef.current && selectedImage) {
6271
+ editorControllerRef.current.bindRoot(editableRef.current).updateInlineImage(selectedImage, {
6272
+ width: Math.max(1, Math.round(selectedImage.getBoundingClientRect().width)),
6273
+ });
6274
+ }
6163
6275
  handleInput();
6164
6276
  };
6165
6277
  window.addEventListener("mousemove", onMove);
@@ -6198,6 +6310,11 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
6198
6310
  window.removeEventListener("mousemove", onMove);
6199
6311
  window.removeEventListener("mouseup", onUp);
6200
6312
  resizingRef.current = null;
6313
+ if (editableRef.current && selectedImage) {
6314
+ editorControllerRef.current.bindRoot(editableRef.current).updateInlineImage(selectedImage, {
6315
+ width: Math.max(1, Math.round(selectedImage.getBoundingClientRect().width)),
6316
+ });
6317
+ }
6201
6318
  handleInput();
6202
6319
  };
6203
6320
  window.addEventListener("mousemove", onMove);
@@ -6213,7 +6330,24 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
6213
6330
  borderRadius: 2,
6214
6331
  cursor: "ew-resize",
6215
6332
  pointerEvents: "auto",
6216
- } })] }))] }), linkMenu && (_jsx(LinkEditorPopover, { x: linkMenu.x, y: linkMenu.y, initialHref: linkMenu.initialHref, initialText: linkMenu.initialText, initialOpenInNewTab: linkMenu.anchor?.target === "_blank", showTextInput: linkMenu.showTextInput, showOpen: Boolean(linkMenu.anchor), showRemove: Boolean(linkMenu.anchor), onApply: applyLinkEditorValue, onOpen: linkMenu.anchor ? () => {
6333
+ } })] }))] }), pluginContextMenu && (_jsx("div", { style: { position: "fixed", inset: 0, zIndex: 70 }, onMouseDown: () => setPluginContextMenu(null), onContextMenu: (event) => {
6334
+ event.preventDefault();
6335
+ setPluginContextMenu(null);
6336
+ }, children: _jsx("div", { role: "menu", "aria-label": "Plugin actions", style: {
6337
+ position: "fixed",
6338
+ left: pluginContextMenu.x,
6339
+ top: pluginContextMenu.y,
6340
+ minWidth: 200,
6341
+ padding: 6,
6342
+ border: "1px solid var(--srte-border)",
6343
+ borderRadius: 8,
6344
+ background: "var(--srte-input-bg)",
6345
+ boxShadow: "var(--srte-menu-shadow)",
6346
+ }, onMouseDown: (event) => event.stopPropagation(), children: pluginContextMenu.items.map((item) => (_jsx("button", { type: "button", role: "menuitem", className: "srte-menu-item", disabled: !isPluginContributionEnabled(item), onClick: () => {
6347
+ restoreSavedSelection();
6348
+ executePluginCommand(item.commandId, item.input);
6349
+ setPluginContextMenu(null);
6350
+ }, children: _jsx("span", { children: item.label }) }, item.id))) }) })), linkMenu && (_jsx(LinkEditorPopover, { x: linkMenu.x, y: linkMenu.y, initialHref: linkMenu.initialHref, initialText: linkMenu.initialText, initialOpenInNewTab: linkMenu.anchor?.target === "_blank", showTextInput: linkMenu.showTextInput, showOpen: Boolean(linkMenu.anchor), showRemove: Boolean(linkMenu.anchor), onApply: applyLinkEditorValue, onOpen: linkMenu.anchor ? () => {
6217
6351
  openEditorLink(linkMenu.anchor);
6218
6352
  setLinkMenu(null);
6219
6353
  } : undefined, onRemove: linkMenu.anchor ? () => removeAnchorLink(linkMenu.anchor) : undefined, onCancel: () => setLinkMenu(null) })), tableMenu && (_jsx("div", { style: {
@@ -6258,8 +6392,19 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
6258
6392
  alignItems: "center",
6259
6393
  padding: "4px 6px",
6260
6394
  fontSize: 12,
6261
- }, children: [_jsx("span", { children: "Fill:" }), _jsx("input", { type: "color", value: tableMenuFillHex(tableMenu.cell), onChange: (e) => {
6262
- runTableCellAction(tableMenu.cell, (cell) => applyBgToSelection(e.target.value, cell));
6395
+ }, children: [_jsx("span", { children: "Fill:" }), _jsx("input", { type: "color", value: tableMenuFillHex(tableMenu.cell), onPointerDown: () => {
6396
+ const selection = shouldUseTableSelection(tableMenu.cell) ? selectionRef.current : null;
6397
+ tableFillTargetsRef.current = selection
6398
+ ? getCellsInGridRect(selection.tbody, selection.sr, selection.sc, selection.er, selection.ec)
6399
+ : [tableMenu.cell];
6400
+ pushEditorHistory();
6401
+ clearSelectionDecor();
6402
+ }, onClick: (e) => e.stopPropagation(), onInput: (e) => {
6403
+ applyBgToSelection(e.currentTarget.value, tableMenu.cell, tableFillTargetsRef.current);
6404
+ handleInput();
6405
+ }, onChange: (e) => {
6406
+ applyBgToSelection(e.currentTarget.value, tableMenu.cell, tableFillTargetsRef.current);
6407
+ handleInput();
6263
6408
  }, style: {
6264
6409
  width: 28,
6265
6410
  height: 18,
@@ -6570,7 +6715,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
6570
6715
  scheduleImageOverlay();
6571
6716
  handleInput();
6572
6717
  }, children: "Reset" })] }), _jsxs("div", { style: { display: "flex", gap: 6 }, children: [_jsx("button", { style: { color: "var(--srte-danger)" }, onClick: () => {
6573
- imageMenu.img.remove();
6718
+ pushEditorHistory();
6719
+ const removedThroughCore = editableRef.current
6720
+ ? editorControllerRef.current.bindRoot(editableRef.current).deleteInlineAtom(imageMenu.img)
6721
+ : false;
6722
+ if (!removedThroughCore)
6723
+ imageMenu.img.remove();
6574
6724
  setImageMenu(null);
6575
6725
  setSelectedImage(null);
6576
6726
  setImageOverlay(null);