smartrte-react 0.3.4 → 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.
- package/CHANGELOG.md +9 -0
- package/README.md +14 -83
- package/dist/adapters/canonicalInlineCommandBridge.d.ts +15 -0
- package/dist/adapters/canonicalInlineCommandBridge.js +269 -0
- package/dist/adapters/canonicalListCommandBridge.d.ts +26 -0
- package/dist/adapters/canonicalListCommandBridge.js +430 -0
- package/dist/adapters/documentFormats.d.ts +26 -0
- package/dist/adapters/documentFormats.js +49 -0
- package/dist/adapters/docxFormat.d.ts +5 -0
- package/dist/adapters/docxFormat.js +272 -0
- package/dist/adapters/domBlockCommandBridge.d.ts +19 -0
- package/dist/adapters/domBlockCommandBridge.js +79 -0
- package/dist/adapters/domChecklistCommandBridge.d.ts +6 -0
- package/dist/adapters/domChecklistCommandBridge.js +33 -0
- package/dist/adapters/domCommandBridge.d.ts +19 -0
- package/dist/adapters/domCommandBridge.js +33 -0
- package/dist/adapters/domInlineAtomCommandBridge.d.ts +14 -0
- package/dist/adapters/domInlineAtomCommandBridge.js +101 -0
- package/dist/adapters/domInlineImageCommandBridge.d.ts +14 -0
- package/dist/adapters/domInlineImageCommandBridge.js +72 -0
- package/dist/adapters/domMoveCommandBridge.d.ts +7 -0
- package/dist/adapters/domMoveCommandBridge.js +54 -0
- package/dist/adapters/domSelectionBridge.d.ts +3 -1
- package/dist/adapters/domSelectionBridge.js +96 -44
- package/dist/adapters/domSmartDocument.js +158 -16
- package/dist/adapters/domTableCommandBridge.d.ts +35 -0
- package/dist/adapters/domTableCommandBridge.js +201 -0
- package/dist/adapters/legacyListShadowComparator.d.ts +26 -0
- package/dist/adapters/legacyListShadowComparator.js +316 -0
- package/dist/adapters/pdfFormat.d.ts +23 -0
- package/dist/adapters/pdfFormat.js +204 -0
- package/dist/adapters/portableDocxAtoms.d.ts +4 -0
- package/dist/adapters/portableDocxAtoms.js +58 -0
- package/dist/adapters/styledDocxFormat.d.ts +8 -0
- package/dist/adapters/styledDocxFormat.js +190 -0
- package/dist/builtInFormatDefinitions.d.ts +5 -0
- package/dist/builtInFormatDefinitions.js +82 -0
- package/dist/canonicalAuthorityFlag.d.ts +32 -0
- package/dist/canonicalAuthorityFlag.js +49 -0
- package/dist/canonicalClipboardRuntime.d.ts +15 -0
- package/dist/canonicalClipboardRuntime.js +76 -0
- package/dist/canonicalEditorRuntime.d.ts +77 -0
- package/dist/canonicalEditorRuntime.js +274 -0
- package/dist/components/CanonicalAuthorityEditor.d.ts +25 -0
- package/dist/components/CanonicalAuthorityEditor.js +518 -0
- package/dist/components/ClassicEditor.d.ts +13 -3
- package/dist/components/ClassicEditor.js +1226 -1114
- package/dist/components/ClassicEditorAuthority.d.ts +38 -0
- package/dist/components/ClassicEditorAuthority.js +49 -0
- package/dist/components/MediaPicker.d.ts +10 -0
- package/dist/components/MediaPicker.js +16 -0
- package/dist/editorController.d.ts +78 -0
- package/dist/editorController.js +298 -0
- package/dist/formatFidelity.d.ts +14 -0
- package/dist/formatFidelity.js +107 -0
- package/dist/formatRuntime.d.ts +50 -0
- package/dist/formatRuntime.js +20 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +10 -0
- package/dist/mediaProvider.d.ts +39 -0
- package/dist/mediaProvider.js +1 -0
- package/dist/pluginRuntime.d.ts +68 -0
- package/dist/pluginRuntime.js +78 -0
- package/dist/standalone/editor.js +722 -495
- package/dist/test-harness/atomShadowComparator.d.ts +14 -0
- package/dist/test-harness/atomShadowComparator.js +63 -0
- package/dist/test-harness/blockShadowComparator.d.ts +30 -0
- package/dist/test-harness/blockShadowComparator.js +140 -0
- package/dist/test-harness/clipboardShadowComparator.d.ts +12 -0
- package/dist/test-harness/clipboardShadowComparator.js +46 -0
- package/dist/test-harness/inlineShadowComparator.d.ts +32 -0
- package/dist/test-harness/inlineShadowComparator.js +152 -0
- package/dist/test-harness/legacyAtomEngine.d.ts +10 -0
- package/dist/test-harness/legacyAtomEngine.js +15 -0
- package/dist/test-harness/legacyBlockEngine.d.ts +31 -0
- package/dist/test-harness/legacyBlockEngine.js +31 -0
- package/dist/test-harness/legacyClipboardEngine.d.ts +14 -0
- package/dist/test-harness/legacyClipboardEngine.js +67 -0
- package/dist/test-harness/legacyInlineEngine.d.ts +19 -0
- package/dist/test-harness/legacyInlineEngine.js +49 -0
- package/dist/test-harness/legacyTableEngine.d.ts +12 -0
- package/dist/test-harness/legacyTableEngine.js +25 -0
- package/dist/test-harness/tableShadowComparator.d.ts +29 -0
- package/dist/test-harness/tableShadowComparator.js +101 -0
- package/dist/theme.d.ts +1 -1
- package/dist/theme.js +28 -0
- 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
|
|
6
|
-
import
|
|
7
|
-
import
|
|
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
|
-
|
|
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);
|
|
@@ -124,6 +155,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
124
155
|
const tableFillTargetsRef = useRef(null);
|
|
125
156
|
const selectingRef = useRef(null);
|
|
126
157
|
const [imageMenu, setImageMenu] = useState(null);
|
|
158
|
+
const [pluginContextMenu, setPluginContextMenu] = useState(null);
|
|
127
159
|
const [linkMenu, setLinkMenu] = useState(null);
|
|
128
160
|
const [showMediaManager, setShowMediaManager] = useState(false);
|
|
129
161
|
const [showColorPicker, setShowColorPicker] = useState(false);
|
|
@@ -132,10 +164,6 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
132
164
|
const savedRangeRef = useRef(null);
|
|
133
165
|
const pendingFontSizeRef = useRef(null);
|
|
134
166
|
const inlineScriptCaretOverrideRef = useRef(null);
|
|
135
|
-
const historyRef = useRef({
|
|
136
|
-
undo: [],
|
|
137
|
-
redo: [],
|
|
138
|
-
});
|
|
139
167
|
const inputHistoryGroupRef = useRef(null);
|
|
140
168
|
const [currentFontSize, setCurrentFontSize] = useState("");
|
|
141
169
|
const [currentFont, setCurrentFont] = useState("");
|
|
@@ -167,6 +195,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
167
195
|
fixNegativeMargins(el);
|
|
168
196
|
normalizeInvalidQuoteNesting(el);
|
|
169
197
|
normalizeInvalidCodeBlockNesting(el);
|
|
198
|
+
normalizeBlockContainerStyling(el);
|
|
170
199
|
normalizeInvalidTableNesting(el);
|
|
171
200
|
ensureTableWrappers(el);
|
|
172
201
|
ensureCaretBoundaryParagraphs(el);
|
|
@@ -174,6 +203,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
174
203
|
}
|
|
175
204
|
normalizeInvalidQuoteNesting(el);
|
|
176
205
|
normalizeInvalidCodeBlockNesting(el);
|
|
206
|
+
normalizeBlockContainerStyling(el);
|
|
177
207
|
normalizeInvalidTableNesting(el);
|
|
178
208
|
ensureCaretBoundaryParagraphs(el);
|
|
179
209
|
// Suppress native context menu inside table cells at capture phase
|
|
@@ -277,8 +307,19 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
277
307
|
if (!inEditor)
|
|
278
308
|
return;
|
|
279
309
|
let node = range.commonAncestorContainer;
|
|
280
|
-
if (node.nodeType === Node.TEXT_NODE)
|
|
310
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
281
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
|
+
}
|
|
282
323
|
const element = node instanceof HTMLElement ? node : null;
|
|
283
324
|
const block = element?.closest("p,h1,h2,h3,h4,h5,h6,li,blockquote,pre,div");
|
|
284
325
|
const tag = block?.tagName.toLowerCase();
|
|
@@ -582,13 +623,39 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
582
623
|
return () => editor.removeEventListener("beforeinput", applyPendingInlineScript);
|
|
583
624
|
}, []);
|
|
584
625
|
const applyFormatBlock = (blockName) => {
|
|
626
|
+
if (!blockTypeFeature)
|
|
627
|
+
return;
|
|
585
628
|
try {
|
|
586
629
|
if (!restoreSavedSelection()) {
|
|
587
630
|
safeSelectRange(getSelectionRangeInEditor());
|
|
588
631
|
}
|
|
589
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
|
+
}
|
|
590
658
|
if (applyFormatBlockFallback(blockName)) {
|
|
591
|
-
const tag = normalizeBlockTag(blockName);
|
|
592
659
|
if (tag === "p" || /^h[1-6]$/.test(tag || "")) {
|
|
593
660
|
setCurrentBlockType(tag);
|
|
594
661
|
}
|
|
@@ -637,37 +704,19 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
637
704
|
cell.style.outline = outline;
|
|
638
705
|
cell.style.outlineOffset = outlineOffset;
|
|
639
706
|
});
|
|
640
|
-
|
|
641
|
-
if (undo[undo.length - 1] !== html) {
|
|
642
|
-
undo.push(html);
|
|
643
|
-
if (undo.length > 100)
|
|
644
|
-
undo.shift();
|
|
645
|
-
}
|
|
646
|
-
historyRef.current.redo = [];
|
|
707
|
+
editorControllerRef.current.bindRoot(editor).recordHistorySnapshot(html);
|
|
647
708
|
};
|
|
648
709
|
const restoreEditorHistory = (dir) => {
|
|
649
710
|
const editor = editableRef.current;
|
|
650
711
|
if (!editor)
|
|
651
712
|
return false;
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
const to = dir === "undo" ? history.redo : history.undo;
|
|
655
|
-
const currentHtml = editor.innerHTML;
|
|
656
|
-
let html;
|
|
657
|
-
while (from.length > 0) {
|
|
658
|
-
const candidate = from.pop();
|
|
659
|
-
if (candidate !== currentHtml) {
|
|
660
|
-
html = candidate;
|
|
661
|
-
break;
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
if (html == null)
|
|
713
|
+
const restored = editorControllerRef.current.bindRoot(editor).restoreHistory(dir);
|
|
714
|
+
if (!restored)
|
|
665
715
|
return false;
|
|
666
|
-
|
|
667
|
-
to.push(currentHtml);
|
|
668
|
-
editor.innerHTML = html;
|
|
716
|
+
const html = restored.html;
|
|
669
717
|
fixNegativeMargins(editor);
|
|
670
718
|
normalizeInvalidCodeBlockNesting(editor);
|
|
719
|
+
normalizeBlockContainerStyling(editor);
|
|
671
720
|
ensureTableWrappers(editor);
|
|
672
721
|
ensureCaretBoundaryParagraphs(editor);
|
|
673
722
|
addTableResizeHandles();
|
|
@@ -850,14 +899,35 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
850
899
|
range.collapse(false);
|
|
851
900
|
return range;
|
|
852
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
|
+
};
|
|
853
917
|
const getCurrentBlock = () => {
|
|
854
918
|
const editor = editableRef.current;
|
|
855
919
|
const range = getSelectionRangeInEditor();
|
|
856
920
|
if (!editor || !range)
|
|
857
921
|
return null;
|
|
858
922
|
let node = range.startContainer;
|
|
859
|
-
if (node.nodeType === Node.TEXT_NODE)
|
|
923
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
860
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
|
+
}
|
|
861
931
|
const element = node instanceof HTMLElement ? node : null;
|
|
862
932
|
const cell = element?.closest("td,th");
|
|
863
933
|
if (cell && editor.contains(cell)) {
|
|
@@ -875,6 +945,22 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
875
945
|
return block;
|
|
876
946
|
};
|
|
877
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
|
+
};
|
|
878
964
|
const getSelectedBlocks = (range) => {
|
|
879
965
|
const editor = editableRef.current;
|
|
880
966
|
if (!editor)
|
|
@@ -888,32 +974,50 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
888
974
|
const startCell = startElement?.closest?.("td,th");
|
|
889
975
|
const endCell = endElement?.closest?.("td,th");
|
|
890
976
|
const scope = startCell && startCell === endCell && editor.contains(startCell) ? startCell : editor;
|
|
891
|
-
const
|
|
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) => {
|
|
892
986
|
if (block === editor || block.getAttribute("data-table-wrapper") === "true")
|
|
893
987
|
return false;
|
|
894
|
-
|
|
895
|
-
|
|
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")
|
|
896
992
|
return false;
|
|
897
|
-
|
|
898
|
-
|
|
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)
|
|
899
999
|
return false;
|
|
900
|
-
const parent = block.parentNode;
|
|
901
|
-
if (parent === range.endContainer) {
|
|
902
|
-
const index = Array.prototype.indexOf.call(parent.childNodes, block);
|
|
903
|
-
if (range.endOffset <= index)
|
|
904
|
-
return false;
|
|
905
|
-
}
|
|
906
|
-
if (parent === range.startContainer) {
|
|
907
|
-
const index = Array.prototype.indexOf.call(parent.childNodes, block);
|
|
908
|
-
if (range.startOffset > index)
|
|
909
|
-
return false;
|
|
910
|
-
}
|
|
911
|
-
return true;
|
|
912
1000
|
}
|
|
913
|
-
|
|
914
|
-
|
|
1001
|
+
if (parent === range.startContainer) {
|
|
1002
|
+
const index = Array.prototype.indexOf.call(parent.childNodes, block);
|
|
1003
|
+
if (range.startOffset > index)
|
|
1004
|
+
return false;
|
|
915
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));
|
|
916
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)));
|
|
917
1021
|
if (blocks.length > 0)
|
|
918
1022
|
return sortInDocumentOrder(blocks);
|
|
919
1023
|
const current = getCurrentBlock();
|
|
@@ -924,9 +1028,15 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
924
1028
|
if (!editor)
|
|
925
1029
|
return [];
|
|
926
1030
|
if (range.collapsed) {
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
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;
|
|
930
1040
|
const item = element?.closest("li");
|
|
931
1041
|
return item && editor.contains(item) ? [item] : [];
|
|
932
1042
|
}
|
|
@@ -942,6 +1052,22 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
942
1052
|
});
|
|
943
1053
|
return sortInDocumentOrder(Array.from(editor.querySelectorAll("li")).filter(ownsSelectedContent));
|
|
944
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
|
+
};
|
|
945
1071
|
const getAlignmentTarget = (block) => {
|
|
946
1072
|
const item = block.tagName === "LI" ? block : block.closest("li");
|
|
947
1073
|
return item instanceof HTMLElement ? item : block;
|
|
@@ -981,6 +1107,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
981
1107
|
: "left";
|
|
982
1108
|
};
|
|
983
1109
|
const applyTextAlignment = (alignment) => {
|
|
1110
|
+
if (!alignmentFeature)
|
|
1111
|
+
return;
|
|
984
1112
|
try {
|
|
985
1113
|
if (!restoreSavedSelection())
|
|
986
1114
|
safeSelectRange(getSelectionRangeInEditor());
|
|
@@ -991,6 +1119,26 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
991
1119
|
if (targets.length === 0)
|
|
992
1120
|
return;
|
|
993
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
|
+
}
|
|
994
1142
|
targets.forEach((target) => {
|
|
995
1143
|
target.style.textAlign = alignment === "left" ? "" : alignment;
|
|
996
1144
|
if (!target.getAttribute("style"))
|
|
@@ -1213,6 +1361,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1213
1361
|
const isCompatible = (candidate) => candidate instanceof HTMLElement &&
|
|
1214
1362
|
candidate.tagName === list.tagName &&
|
|
1215
1363
|
candidate.style.listStyleType === list.style.listStyleType &&
|
|
1364
|
+
candidate.dataset.srteListPreset === list.dataset.srteListPreset &&
|
|
1365
|
+
candidate.dataset.srteListDepth === list.dataset.srteListDepth &&
|
|
1216
1366
|
candidate.dataset.srteChecklist === list.dataset.srteChecklist &&
|
|
1217
1367
|
candidate.dataset.srteChecklistStrike === list.dataset.srteChecklistStrike;
|
|
1218
1368
|
let merged = list;
|
|
@@ -1338,6 +1488,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1338
1488
|
list.parentElement?.replaceChild(replacement, list);
|
|
1339
1489
|
const li = replacement.querySelector("li");
|
|
1340
1490
|
focusElementEnd(li || replacement);
|
|
1491
|
+
return replacement;
|
|
1341
1492
|
};
|
|
1342
1493
|
const getOrCreateNestedList = (item, source) => {
|
|
1343
1494
|
const compatible = Array.from(item.children).find((child) => child instanceof HTMLElement &&
|
|
@@ -1470,9 +1621,28 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1470
1621
|
return list?.parentElement?.tagName === "LI" && ["UL", "OL"].includes(list.parentElement.parentElement?.tagName || "");
|
|
1471
1622
|
});
|
|
1472
1623
|
const applyListDepthChange = (items, direction) => {
|
|
1624
|
+
if (!listFeature)
|
|
1625
|
+
return false;
|
|
1473
1626
|
const enabled = direction === "indent" ? canIndentListItems(items) : canOutdentListItems(items);
|
|
1474
1627
|
if (!enabled)
|
|
1475
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
|
+
}
|
|
1476
1646
|
pushEditorHistory();
|
|
1477
1647
|
const changed = direction === "indent"
|
|
1478
1648
|
? nestSelectedListItems(items)
|
|
@@ -1527,6 +1697,86 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1527
1697
|
});
|
|
1528
1698
|
return lastSelected;
|
|
1529
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
|
+
};
|
|
1530
1780
|
const getDirectSelectionCell = (range) => {
|
|
1531
1781
|
const startCell = getClosestCell(range.startContainer);
|
|
1532
1782
|
const endCell = getClosestCell(range.endContainer);
|
|
@@ -1588,8 +1838,15 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1588
1838
|
return convertDirectTableCellSelectionToList(range, listTag);
|
|
1589
1839
|
};
|
|
1590
1840
|
const applyListStyle = (value) => {
|
|
1591
|
-
|
|
1592
|
-
|
|
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);
|
|
1593
1850
|
if (!restoreSavedSelection())
|
|
1594
1851
|
safeSelectRange(getSelectionRangeInEditor());
|
|
1595
1852
|
const range = getSelectionRangeInEditor();
|
|
@@ -1600,7 +1857,10 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1600
1857
|
pushEditorHistory();
|
|
1601
1858
|
const list = convertDirectTableCellContentsToList(currentBlock, listTag);
|
|
1602
1859
|
if (list) {
|
|
1603
|
-
|
|
1860
|
+
if (preset)
|
|
1861
|
+
applyListPreset(list, preset, 0);
|
|
1862
|
+
else
|
|
1863
|
+
list.style.listStyleType = styleType;
|
|
1604
1864
|
handleInput();
|
|
1605
1865
|
requestAnimationFrame(updateActiveState);
|
|
1606
1866
|
}
|
|
@@ -1610,7 +1870,10 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1610
1870
|
pushEditorHistory();
|
|
1611
1871
|
const list = convertDirectTableCellSelectionToList(range, listTag);
|
|
1612
1872
|
if (list) {
|
|
1613
|
-
|
|
1873
|
+
if (preset)
|
|
1874
|
+
applyListPreset(list, preset, 0);
|
|
1875
|
+
else
|
|
1876
|
+
list.style.listStyleType = styleType;
|
|
1614
1877
|
handleInput();
|
|
1615
1878
|
requestAnimationFrame(updateActiveState);
|
|
1616
1879
|
}
|
|
@@ -1620,8 +1883,52 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1620
1883
|
const selectedItems = resolveSelectedListItems(range);
|
|
1621
1884
|
const selectedPlainBlocks = selectedBlocks.filter((block) => !block.closest("ul,ol"));
|
|
1622
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] : []);
|
|
1623
1897
|
pushEditorHistory();
|
|
1624
|
-
|
|
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
|
+
}
|
|
1625
1932
|
const convertedPlainBlocks = convertSelectedBlocksToList(selectedPlainBlocks, listTag, styleType);
|
|
1626
1933
|
if (!convertedPlainBlocks && lastItem)
|
|
1627
1934
|
focusElementEnd(lastItem);
|
|
@@ -1655,7 +1962,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1655
1962
|
if (convertSelectedBlocksToList(convertibleBlocks, listTag)) {
|
|
1656
1963
|
const createdList = getCurrentBlock()?.closest("ul,ol");
|
|
1657
1964
|
if (createdList) {
|
|
1658
|
-
|
|
1965
|
+
if (preset)
|
|
1966
|
+
applyListPresetHierarchy(createdList, preset);
|
|
1967
|
+
else {
|
|
1968
|
+
clearListPreset(createdList);
|
|
1969
|
+
createdList.style.listStyleType = styleType;
|
|
1970
|
+
}
|
|
1659
1971
|
const mergedList = mergeAdjacentCompatibleLists(createdList);
|
|
1660
1972
|
const lastItem = mergedList.lastElementChild;
|
|
1661
1973
|
if (lastItem)
|
|
@@ -1670,7 +1982,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1670
1982
|
toggleList(listTag);
|
|
1671
1983
|
const createdList = getCurrentBlock()?.closest("ul,ol");
|
|
1672
1984
|
if (createdList) {
|
|
1673
|
-
|
|
1985
|
+
if (preset)
|
|
1986
|
+
applyListPresetHierarchy(createdList, preset);
|
|
1987
|
+
else {
|
|
1988
|
+
clearListPreset(createdList);
|
|
1989
|
+
createdList.style.listStyleType = styleType;
|
|
1990
|
+
}
|
|
1674
1991
|
handleInput();
|
|
1675
1992
|
}
|
|
1676
1993
|
return;
|
|
@@ -1696,7 +2013,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1696
2013
|
list.parentElement?.replaceChild(target, list);
|
|
1697
2014
|
}
|
|
1698
2015
|
clearChecklist(target);
|
|
1699
|
-
|
|
2016
|
+
if (preset)
|
|
2017
|
+
applyListPresetHierarchy(target, preset);
|
|
2018
|
+
else {
|
|
2019
|
+
clearListPreset(target);
|
|
2020
|
+
target.style.listStyleType = styleType;
|
|
2021
|
+
}
|
|
1700
2022
|
lastList = target;
|
|
1701
2023
|
styledLists.push(target);
|
|
1702
2024
|
});
|
|
@@ -1712,6 +2034,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1712
2034
|
requestAnimationFrame(updateActiveState);
|
|
1713
2035
|
};
|
|
1714
2036
|
const applyChecklist = (strikeCompleted = false, toggleOff = false) => {
|
|
2037
|
+
if (!checklistFeature)
|
|
2038
|
+
return;
|
|
1715
2039
|
if (!restoreSavedSelection())
|
|
1716
2040
|
safeSelectRange(getSelectionRangeInEditor());
|
|
1717
2041
|
const range = getSelectionRangeInEditor();
|
|
@@ -1719,6 +2043,32 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1719
2043
|
return;
|
|
1720
2044
|
const selectedItems = resolveSelectedListItems(range);
|
|
1721
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
|
+
}
|
|
1722
2072
|
if (toggleOff && selectedItems.length > 0 && selectedChecklists.length === selectedItems.length) {
|
|
1723
2073
|
pushEditorHistory();
|
|
1724
2074
|
selectedItems.forEach((item) => {
|
|
@@ -1936,9 +2286,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1936
2286
|
return changed;
|
|
1937
2287
|
};
|
|
1938
2288
|
const toggleList = (listTag) => {
|
|
2289
|
+
if (!listFeature)
|
|
2290
|
+
return;
|
|
1939
2291
|
const editor = editableRef.current;
|
|
1940
2292
|
if (!editor)
|
|
1941
2293
|
return;
|
|
2294
|
+
normalizeListNesting(editor);
|
|
1942
2295
|
if (!restoreSavedSelection())
|
|
1943
2296
|
safeSelectRange(getSelectionRangeInEditor());
|
|
1944
2297
|
const setListActiveState = (active) => {
|
|
@@ -1962,10 +2315,41 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1962
2315
|
const blocks = getSelectedBlocks(range);
|
|
1963
2316
|
const selectedListItems = resolveSelectedListItems(range);
|
|
1964
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)));
|
|
1965
2321
|
pushEditorHistory();
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
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);
|
|
1969
2353
|
handleInput();
|
|
1970
2354
|
requestAnimationFrame(updateActiveState);
|
|
1971
2355
|
return;
|
|
@@ -2061,6 +2445,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
2061
2445
|
catch { }
|
|
2062
2446
|
};
|
|
2063
2447
|
const toggleBlockquote = () => {
|
|
2448
|
+
if (!blockquoteFeature)
|
|
2449
|
+
return;
|
|
2064
2450
|
try {
|
|
2065
2451
|
if (!restoreSavedSelection())
|
|
2066
2452
|
safeSelectRange(getSelectionRangeInEditor());
|
|
@@ -2068,6 +2454,40 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
2068
2454
|
const editor = editableRef.current;
|
|
2069
2455
|
if (!editor || !range)
|
|
2070
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
|
+
}
|
|
2071
2491
|
const unwrapQuote = (quote) => {
|
|
2072
2492
|
const parent = quote.parentElement;
|
|
2073
2493
|
if (!parent)
|
|
@@ -2209,6 +2629,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
2209
2629
|
catch { }
|
|
2210
2630
|
};
|
|
2211
2631
|
const toggleCodeBlock = () => {
|
|
2632
|
+
if (!codeBlockFeature)
|
|
2633
|
+
return;
|
|
2212
2634
|
try {
|
|
2213
2635
|
if (!restoreSavedSelection())
|
|
2214
2636
|
safeSelectRange(getSelectionRangeInEditor());
|
|
@@ -2223,10 +2645,27 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
2223
2645
|
return;
|
|
2224
2646
|
const listItems = resolveSelectedListItems(range);
|
|
2225
2647
|
const plainBlocks = blocks.filter((block) => !block.closest("ul,ol") && !block.closest("table"));
|
|
2226
|
-
|
|
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");
|
|
2227
2654
|
const allTargetsActive = listItems.every((item) => Boolean(getItemCode(item))) &&
|
|
2228
2655
|
plainBlocks.every((block) => block.tagName === "PRE") &&
|
|
2229
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
|
+
}
|
|
2230
2669
|
pushEditorHistory();
|
|
2231
2670
|
let lastTarget = null;
|
|
2232
2671
|
listItems.forEach((item) => {
|
|
@@ -2234,8 +2673,15 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
2234
2673
|
if (allTargetsActive && existing) {
|
|
2235
2674
|
const code = existing.querySelector(":scope > code");
|
|
2236
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");
|
|
2237
2680
|
while (source.firstChild)
|
|
2238
|
-
|
|
2681
|
+
paragraph.appendChild(source.firstChild);
|
|
2682
|
+
if (!paragraph.childNodes.length)
|
|
2683
|
+
paragraph.innerHTML = "<br>";
|
|
2684
|
+
item.insertBefore(paragraph, existing);
|
|
2239
2685
|
existing.remove();
|
|
2240
2686
|
lastTarget = item;
|
|
2241
2687
|
return;
|
|
@@ -2249,12 +2695,21 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
2249
2695
|
pre.style.textAlign = "left";
|
|
2250
2696
|
const code = document.createElement("code");
|
|
2251
2697
|
const nestedList = Array.from(item.children).find((child) => child.matches("ul,ol")) || null;
|
|
2252
|
-
Array.from(item.childNodes).
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
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"));
|
|
2258
2713
|
});
|
|
2259
2714
|
if (!code.childNodes.length)
|
|
2260
2715
|
code.innerHTML = "<br>";
|
|
@@ -2813,6 +3268,33 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
2813
3268
|
range.collapse(false);
|
|
2814
3269
|
safeSelectRange(range);
|
|
2815
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
|
+
}
|
|
2816
3298
|
const img = document.createElement("img");
|
|
2817
3299
|
img.src = src;
|
|
2818
3300
|
img.draggable = true;
|
|
@@ -2882,27 +3364,47 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
2882
3364
|
safeSelectRange(range);
|
|
2883
3365
|
}
|
|
2884
3366
|
pushEditorHistory();
|
|
2885
|
-
const span =
|
|
2886
|
-
|
|
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);
|
|
2887
3389
|
try {
|
|
2888
3390
|
// @ts-ignore
|
|
2889
3391
|
const katex = window.katex;
|
|
2890
3392
|
if (katex && typeof katex.render === "function") {
|
|
2891
|
-
katex.render(tex,
|
|
3393
|
+
katex.render(tex, fallbackSpan, { throwOnError: false });
|
|
2892
3394
|
}
|
|
2893
3395
|
else {
|
|
2894
|
-
|
|
3396
|
+
fallbackSpan.textContent = `$${tex}$`;
|
|
2895
3397
|
}
|
|
2896
3398
|
}
|
|
2897
3399
|
catch {
|
|
2898
|
-
|
|
3400
|
+
fallbackSpan.textContent = `$${tex}$`;
|
|
2899
3401
|
}
|
|
2900
3402
|
if (range)
|
|
2901
|
-
range.insertNode(
|
|
3403
|
+
range.insertNode(fallbackSpan);
|
|
2902
3404
|
else
|
|
2903
|
-
host.appendChild(
|
|
3405
|
+
host.appendChild(fallbackSpan);
|
|
2904
3406
|
const r = document.createRange();
|
|
2905
|
-
r.setStartAfter(
|
|
3407
|
+
r.setStartAfter(fallbackSpan);
|
|
2906
3408
|
r.collapse(true);
|
|
2907
3409
|
safeSelectRange(r);
|
|
2908
3410
|
handleInput();
|
|
@@ -2977,864 +3479,84 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
2977
3479
|
});
|
|
2978
3480
|
}
|
|
2979
3481
|
};
|
|
2980
|
-
const
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
if (file.type !== 'application/pdf')
|
|
2985
|
-
return;
|
|
2986
|
-
// Check if editor has content
|
|
2987
|
-
const el = editableRef.current;
|
|
2988
|
-
const hasContent = el && el.textContent && el.textContent.trim().length > 0;
|
|
2989
|
-
if (hasContent) {
|
|
2990
|
-
setPendingImport({ file, type: 'pdf' });
|
|
2991
|
-
}
|
|
2992
|
-
else {
|
|
2993
|
-
processImport(file, 'pdf', 'replace');
|
|
2994
|
-
}
|
|
3482
|
+
const getExportableDocument = () => {
|
|
3483
|
+
return editorControllerRef.current
|
|
3484
|
+
.bindRoot(editableRef.current)
|
|
3485
|
+
.getDocument();
|
|
2995
3486
|
};
|
|
2996
|
-
const
|
|
2997
|
-
if (
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3487
|
+
const downloadFormatResult = (result) => {
|
|
3488
|
+
if (result.kind === "handled")
|
|
3489
|
+
return;
|
|
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);
|
|
3009
3501
|
};
|
|
3010
|
-
const
|
|
3011
|
-
|
|
3502
|
+
const processFormatImport = async (definition, file, mode) => {
|
|
3503
|
+
if (!definition.importFile)
|
|
3504
|
+
return;
|
|
3505
|
+
if (definition.id === "pdf")
|
|
3012
3506
|
setLoadingPdf(true);
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
// 1. Group items into lines
|
|
3022
|
-
const items = textContent.items;
|
|
3023
|
-
// Calculate base statistics
|
|
3024
|
-
const heights = items.map(item => Math.abs(item.transform[3])).filter(h => h > 0);
|
|
3025
|
-
heights.sort((a, b) => a - b);
|
|
3026
|
-
const medianHeight = heights[Math.floor(heights.length / 2)] || 12;
|
|
3027
|
-
// Group by Y (with tolerance)
|
|
3028
|
-
const linesMap = new Map();
|
|
3029
|
-
for (const item of items) {
|
|
3030
|
-
if (!item.str.trim())
|
|
3031
|
-
continue;
|
|
3032
|
-
// Normalize Y to integer buckets to group roughly
|
|
3033
|
-
// PDF Y is bottom-0, so higher Y is higher on page.
|
|
3034
|
-
const y = item.transform[5];
|
|
3035
|
-
// Find closest existing line
|
|
3036
|
-
let foundKey = -1;
|
|
3037
|
-
for (const key of linesMap.keys()) {
|
|
3038
|
-
if (Math.abs(key - y) < medianHeight * 0.5) {
|
|
3039
|
-
foundKey = key;
|
|
3040
|
-
break;
|
|
3041
|
-
}
|
|
3042
|
-
}
|
|
3043
|
-
if (foundKey !== -1) {
|
|
3044
|
-
linesMap.get(foundKey).items.push(item);
|
|
3045
|
-
}
|
|
3046
|
-
else {
|
|
3047
|
-
linesMap.set(y, { y, items: [item] });
|
|
3048
|
-
}
|
|
3049
|
-
}
|
|
3050
|
-
// Convert map to sorted array (top to bottom)
|
|
3051
|
-
const lines = Array.from(linesMap.values()).sort((a, b) => b.y - a.y);
|
|
3052
|
-
// Sort items within lines (left to right)
|
|
3053
|
-
lines.forEach(line => {
|
|
3054
|
-
line.items.sort((a, b) => a.transform[4] - b.transform[4]);
|
|
3055
|
-
});
|
|
3056
|
-
// 2. Identify and Build Structures
|
|
3057
|
-
let html = '';
|
|
3058
|
-
let listStack = []; // 'ul' or 'ol'
|
|
3059
|
-
let inTable = false;
|
|
3060
|
-
let tableColumns = []; // X-coordinates of column starts
|
|
3061
|
-
let tableHtml = '';
|
|
3062
|
-
const closeList = () => {
|
|
3063
|
-
if (listStack.length > 0) {
|
|
3064
|
-
html += `</${listStack.pop()}>`;
|
|
3065
|
-
}
|
|
3066
|
-
};
|
|
3067
|
-
const closeTable = () => {
|
|
3068
|
-
if (inTable) {
|
|
3069
|
-
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>';
|
|
3070
|
-
tableHtml = '';
|
|
3071
|
-
inTable = false;
|
|
3072
|
-
tableColumns = [];
|
|
3073
|
-
}
|
|
3074
|
-
};
|
|
3075
|
-
for (let lIndex = 0; lIndex < lines.length; lIndex++) {
|
|
3076
|
-
const line = lines[lIndex];
|
|
3077
|
-
// Calculate gaps and text
|
|
3078
|
-
let lineText = '';
|
|
3079
|
-
let lineHtmlContent = '';
|
|
3080
|
-
let lastX = -1;
|
|
3081
|
-
let gaps = [];
|
|
3082
|
-
let itemXs = []; // Start X of logical items (words or phrases)
|
|
3083
|
-
// Reconstruct text with spacing detection
|
|
3084
|
-
for (let j = 0; j < line.items.length; j++) {
|
|
3085
|
-
const item = line.items[j];
|
|
3086
|
-
const x = item.transform[4];
|
|
3087
|
-
const width = item.width;
|
|
3088
|
-
const fontName = item.fontName;
|
|
3089
|
-
const fontObj = styles[fontName];
|
|
3090
|
-
const fontFamily = fontObj?.fontFamily?.toLowerCase() || '';
|
|
3091
|
-
const isBold = fontFamily.includes('bold') || false;
|
|
3092
|
-
const isItalic = fontFamily.includes('italic') || fontFamily.includes('oblique');
|
|
3093
|
-
const fontSize = Math.max(8, Math.round(Math.abs(item.transform[3])));
|
|
3094
|
-
if (lastX > 0) {
|
|
3095
|
-
const gap = x - lastX;
|
|
3096
|
-
if (gap > 2) { // Minimal space threshold
|
|
3097
|
-
lineText += ' ';
|
|
3098
|
-
lineHtmlContent += ' ';
|
|
3099
|
-
if (gap > 20) { // Large gap threshold for table detection
|
|
3100
|
-
gaps.push(gap);
|
|
3101
|
-
}
|
|
3102
|
-
}
|
|
3103
|
-
}
|
|
3104
|
-
else {
|
|
3105
|
-
// First item
|
|
3106
|
-
}
|
|
3107
|
-
// Track "columns" candidates: items separated by big gaps
|
|
3108
|
-
if (j === 0 || (x - lastX) > 20) {
|
|
3109
|
-
itemXs.push(x);
|
|
3110
|
-
}
|
|
3111
|
-
// Append text style
|
|
3112
|
-
const chunkStyle = cssRules([
|
|
3113
|
-
['font-size', `${fontSize}px`],
|
|
3114
|
-
['font-weight', isBold ? '700' : ''],
|
|
3115
|
-
['font-style', isItalic ? 'italic' : ''],
|
|
3116
|
-
]);
|
|
3117
|
-
let chunk = `<span${styleAttr(chunkStyle)}>${escapeHtml(item.str)}</span>`;
|
|
3118
|
-
lineText += item.str;
|
|
3119
|
-
lineHtmlContent += chunk;
|
|
3120
|
-
lastX = x + width;
|
|
3121
|
-
}
|
|
3122
|
-
// === Structure Detection ===
|
|
3123
|
-
// Max Font Size in line
|
|
3124
|
-
const maxH = Math.max(...line.items.map((i) => Math.abs(i.transform[3])));
|
|
3125
|
-
const isHeader = maxH > medianHeight * 1.2;
|
|
3126
|
-
// List Detection
|
|
3127
|
-
const isBullet = /^[•\-\*]\s/.test(lineText);
|
|
3128
|
-
const isNumber = /^\d+[\.\)]\s/.test(lineText);
|
|
3129
|
-
// Table Detection Logic
|
|
3130
|
-
// A line starts a table if it has distinct "columns" (multiple items with large gaps)
|
|
3131
|
-
// Or if we are already in a table and this line aligns with columns
|
|
3132
|
-
let isTableLine = false;
|
|
3133
|
-
// If in table, check alignment
|
|
3134
|
-
if (inTable) {
|
|
3135
|
-
// Check if items align with known columns
|
|
3136
|
-
// Simple loose check: do any of the itemXs align with tableColumns?
|
|
3137
|
-
// Or is the line just sparsely populated but roughly compatible?
|
|
3138
|
-
// We'll continually simple-add rows for now until a Paragraph break (plain text, no gaps) is found.
|
|
3139
|
-
// If line looks like normal paragraph (no large gaps, starts at left margin), close table
|
|
3140
|
-
const isPlainParagraph = gaps.length === 0 && itemXs[0] < 50 && lineText.length > 50;
|
|
3141
|
-
// Allow wrapping text in table cells, which might look like lines with no gaps?
|
|
3142
|
-
// Table wrapping usually is indented or aligns with a column > 0.
|
|
3143
|
-
const alignsWithColumn = itemXs.some(x => tableColumns.some(cx => Math.abs(x - cx) < 20));
|
|
3144
|
-
if (alignsWithColumn || (itemXs[0] > 50)) {
|
|
3145
|
-
isTableLine = true;
|
|
3146
|
-
}
|
|
3147
|
-
else {
|
|
3148
|
-
// Maybe a new row starting at col 0?
|
|
3149
|
-
// If it aligns with col 0.
|
|
3150
|
-
if (Math.abs(itemXs[0] - tableColumns[0]) < 20) {
|
|
3151
|
-
isTableLine = true;
|
|
3152
|
-
}
|
|
3153
|
-
}
|
|
3154
|
-
}
|
|
3155
|
-
else {
|
|
3156
|
-
// Potential start of table: multiple items separated by gaps, AND next line likely follows suit?
|
|
3157
|
-
// Or simply: It has > 1 column significantly spaced.
|
|
3158
|
-
if (itemXs.length >= 2 && gaps.some(g => g > 30)) {
|
|
3159
|
-
isTableLine = true;
|
|
3160
|
-
// Establish columns
|
|
3161
|
-
tableColumns = [...itemXs];
|
|
3162
|
-
}
|
|
3163
|
-
}
|
|
3164
|
-
// --- Apply Logic ---
|
|
3165
|
-
if (isTableLine) {
|
|
3166
|
-
closeList();
|
|
3167
|
-
if (!inTable) {
|
|
3168
|
-
inTable = true;
|
|
3169
|
-
// Start table
|
|
3170
|
-
}
|
|
3171
|
-
// Build Row
|
|
3172
|
-
// We need to map items to cells based on tableColumns.
|
|
3173
|
-
// Naive approach: Items close to col X go to col X.
|
|
3174
|
-
let rowHtml = '<tr>';
|
|
3175
|
-
// We assume `tableColumns` defines the start of each cell.
|
|
3176
|
-
// We create a cell for each column.
|
|
3177
|
-
// Collect content for each bucket.
|
|
3178
|
-
const cellContents = new Array(tableColumns.length).fill('');
|
|
3179
|
-
let currentItemHtml = '';
|
|
3180
|
-
let currentItemStart = -1;
|
|
3181
|
-
// process items again to slot them
|
|
3182
|
-
let currentLineX = 0;
|
|
3183
|
-
for (const item of line.items) {
|
|
3184
|
-
const x = item.transform[4];
|
|
3185
|
-
const w = item.width;
|
|
3186
|
-
const txt = item.str;
|
|
3187
|
-
const fontObj = styles[item.fontName];
|
|
3188
|
-
const fontFamily = fontObj?.fontFamily?.toLowerCase() || '';
|
|
3189
|
-
const isBold = fontFamily.includes('bold');
|
|
3190
|
-
const isItalic = fontFamily.includes('italic') || fontFamily.includes('oblique');
|
|
3191
|
-
const fontSize = Math.max(8, Math.round(Math.abs(item.transform[3])));
|
|
3192
|
-
const styledTxt = `<span${styleAttr(cssRules([
|
|
3193
|
-
['font-size', `${fontSize}px`],
|
|
3194
|
-
['font-weight', isBold ? '700' : ''],
|
|
3195
|
-
['font-style', isItalic ? 'italic' : ''],
|
|
3196
|
-
]))}>${escapeHtml(txt)}</span>`;
|
|
3197
|
-
// Decide which column this belongs to
|
|
3198
|
-
// Find closest column to the left (or close enough)
|
|
3199
|
-
let colIdx = 0;
|
|
3200
|
-
let minDiff = 9999;
|
|
3201
|
-
for (let c = 0; c < tableColumns.length; c++) {
|
|
3202
|
-
const colX = tableColumns[c];
|
|
3203
|
-
// If item starts near colX or after it (but before next col)
|
|
3204
|
-
// Actually, just find the "controlling" column (closest start to the left)
|
|
3205
|
-
if (x >= colX - 10) {
|
|
3206
|
-
colIdx = c;
|
|
3207
|
-
}
|
|
3208
|
-
}
|
|
3209
|
-
// Append space if needed
|
|
3210
|
-
if (cellContents[colIdx])
|
|
3211
|
-
cellContents[colIdx] += ' ';
|
|
3212
|
-
cellContents[colIdx] += styledTxt;
|
|
3213
|
-
}
|
|
3214
|
-
cellContents.forEach(content => {
|
|
3215
|
-
rowHtml += `<td style="border:1px solid #d1d5db;padding:8px;vertical-align:top;">${content || ' '}</td>`;
|
|
3216
|
-
});
|
|
3217
|
-
rowHtml += '</tr>';
|
|
3218
|
-
tableHtml += rowHtml;
|
|
3219
|
-
}
|
|
3220
|
-
else {
|
|
3221
|
-
closeTable();
|
|
3222
|
-
if (isBullet || isNumber) {
|
|
3223
|
-
const listType = isBullet ? 'ul' : 'ol';
|
|
3224
|
-
if (listStack.length === 0 || listStack[listStack.length - 1] !== listType) {
|
|
3225
|
-
if (listStack.length > 0)
|
|
3226
|
-
closeList(); // Close switch
|
|
3227
|
-
html += `<${listType}>`;
|
|
3228
|
-
listStack.push(listType);
|
|
3229
|
-
}
|
|
3230
|
-
// Strip marker
|
|
3231
|
-
const content = lineHtmlContent.replace(/^[•\-\*]|\d+[\.\)]/, '').trim();
|
|
3232
|
-
html += `<li>${content}</li>`;
|
|
3233
|
-
}
|
|
3234
|
-
else {
|
|
3235
|
-
closeList();
|
|
3236
|
-
if (isHeader) {
|
|
3237
|
-
const tag = maxH > medianHeight * 1.5 ? 'h2' : 'h3';
|
|
3238
|
-
const firstX = line.items[0]?.transform?.[4] || 0;
|
|
3239
|
-
const lastItem = line.items[line.items.length - 1];
|
|
3240
|
-
const lastRight = lastItem ? lastItem.transform[4] + lastItem.width : firstX;
|
|
3241
|
-
const center = (firstX + lastRight) / 2;
|
|
3242
|
-
const align = Math.abs(center - viewport.width / 2) < viewport.width * 0.12 ? 'center' : firstX > viewport.width * 0.55 ? 'right' : '';
|
|
3243
|
-
html += `<${tag}${styleAttr(cssRules([['text-align', align]]))}>${lineHtmlContent}</${tag}>`;
|
|
3244
|
-
}
|
|
3245
|
-
else {
|
|
3246
|
-
const firstX = line.items[0]?.transform?.[4] || 0;
|
|
3247
|
-
const lastItem = line.items[line.items.length - 1];
|
|
3248
|
-
const lastRight = lastItem ? lastItem.transform[4] + lastItem.width : firstX;
|
|
3249
|
-
const center = (firstX + lastRight) / 2;
|
|
3250
|
-
const align = Math.abs(center - viewport.width / 2) < viewport.width * 0.12 ? 'center' : firstX > viewport.width * 0.55 ? 'right' : '';
|
|
3251
|
-
html += `<p${styleAttr(cssRules([
|
|
3252
|
-
['text-align', align],
|
|
3253
|
-
['margin-left', firstX > 40 && !align ? `${Math.round(firstX)}px` : ''],
|
|
3254
|
-
]))}>${lineHtmlContent}</p>`;
|
|
3255
|
-
}
|
|
3256
|
-
}
|
|
3257
|
-
}
|
|
3258
|
-
}
|
|
3259
|
-
closeList();
|
|
3260
|
-
closeTable();
|
|
3261
|
-
fullHtml += html;
|
|
3262
|
-
}
|
|
3263
|
-
insertImportedHtml(fullHtml, mode, { preserveColors: true, preserveDocumentLayout: 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
|
+
});
|
|
3264
3515
|
}
|
|
3265
3516
|
catch (error) {
|
|
3266
|
-
console.error(
|
|
3267
|
-
// Optional: show user error
|
|
3517
|
+
console.error(`Error importing ${definition.label}:`, error);
|
|
3268
3518
|
}
|
|
3269
3519
|
finally {
|
|
3270
|
-
|
|
3520
|
+
if (definition.id === "pdf")
|
|
3521
|
+
setLoadingPdf(false);
|
|
3522
|
+
if (definition.id === "docx")
|
|
3523
|
+
setLoadingDocx(false);
|
|
3524
|
+
setPendingImport(null);
|
|
3271
3525
|
}
|
|
3272
3526
|
};
|
|
3273
|
-
const
|
|
3274
|
-
|
|
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))
|
|
3275
3550
|
return;
|
|
3276
|
-
|
|
3277
|
-
if (!file.name.endsWith('.docx'))
|
|
3551
|
+
if (!definition.exportDocument)
|
|
3278
3552
|
return;
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
}
|
|
3285
|
-
else {
|
|
3286
|
-
processImport(file, 'docx', 'replace');
|
|
3287
|
-
}
|
|
3553
|
+
const result = await definition.exportDocument(getExportableDocument(), {
|
|
3554
|
+
ownerDocument: document,
|
|
3555
|
+
hostWindow: window,
|
|
3556
|
+
});
|
|
3557
|
+
downloadFormatResult(result);
|
|
3288
3558
|
};
|
|
3289
|
-
const
|
|
3290
|
-
try {
|
|
3291
|
-
setLoadingDocx(true);
|
|
3292
|
-
const arrayBuffer = await file.arrayBuffer();
|
|
3293
|
-
const html = preserveDocxStyles
|
|
3294
|
-
? await convertDocxToStyledHtml(arrayBuffer)
|
|
3295
|
-
: await convertDocxWithMammoth(arrayBuffer);
|
|
3296
|
-
if (html) {
|
|
3297
|
-
insertImportedHtml(`<div class="srte-preserve-colors">${html}</div>`, mode, {
|
|
3298
|
-
preserveColors: preserveDocxStyles,
|
|
3299
|
-
preserveDocumentLayout: preserveDocxStyles,
|
|
3300
|
-
});
|
|
3301
|
-
}
|
|
3302
|
-
}
|
|
3303
|
-
catch (error) {
|
|
3304
|
-
console.error('Error reading DOCX:', error);
|
|
3305
|
-
}
|
|
3306
|
-
finally {
|
|
3307
|
-
setLoadingDocx(false);
|
|
3308
|
-
}
|
|
3309
|
-
};
|
|
3310
|
-
const convertDocxWithMammoth = async (arrayBuffer) => {
|
|
3311
|
-
const result = await mammoth.convertToHtml({ arrayBuffer });
|
|
3312
|
-
const temp = document.createElement('div');
|
|
3313
|
-
temp.innerHTML = result.value;
|
|
3314
|
-
enhanceImportedTables(temp);
|
|
3315
|
-
return temp.innerHTML;
|
|
3316
|
-
};
|
|
3317
|
-
const convertDocxToStyledHtml = async (arrayBuffer) => {
|
|
3318
|
-
try {
|
|
3319
|
-
const zip = await JSZip.loadAsync(arrayBuffer);
|
|
3320
|
-
const documentXml = await zip.file('word/document.xml')?.async('text');
|
|
3321
|
-
if (!documentXml)
|
|
3322
|
-
return convertDocxWithMammoth(arrayBuffer);
|
|
3323
|
-
const parser = new DOMParser();
|
|
3324
|
-
const doc = parser.parseFromString(documentXml, 'application/xml');
|
|
3325
|
-
if (doc.querySelector('parsererror'))
|
|
3326
|
-
return convertDocxWithMammoth(arrayBuffer);
|
|
3327
|
-
const body = Array.from(doc.getElementsByTagName('*')).find((node) => node.localName === 'body');
|
|
3328
|
-
if (!body)
|
|
3329
|
-
return convertDocxWithMammoth(arrayBuffer);
|
|
3330
|
-
const html = directChildren(body)
|
|
3331
|
-
.filter((node) => node.localName !== 'sectPr')
|
|
3332
|
-
.map((node) => {
|
|
3333
|
-
if (node.localName === 'p')
|
|
3334
|
-
return convertDocxParagraph(node);
|
|
3335
|
-
if (node.localName === 'tbl')
|
|
3336
|
-
return convertDocxTable(node);
|
|
3337
|
-
return '';
|
|
3338
|
-
})
|
|
3339
|
-
.join('');
|
|
3340
|
-
if (!html.trim())
|
|
3341
|
-
return convertDocxWithMammoth(arrayBuffer);
|
|
3342
|
-
const temp = document.createElement('div');
|
|
3343
|
-
temp.innerHTML = html;
|
|
3344
|
-
enhanceImportedTables(temp);
|
|
3345
|
-
return temp.innerHTML;
|
|
3346
|
-
}
|
|
3347
|
-
catch (error) {
|
|
3348
|
-
console.warn('Falling back to Mammoth DOCX import:', error);
|
|
3349
|
-
return convertDocxWithMammoth(arrayBuffer);
|
|
3350
|
-
}
|
|
3351
|
-
};
|
|
3352
|
-
const directChildren = (node) => Array.from(node.children);
|
|
3353
|
-
const firstChildByName = (node, localName) => node
|
|
3354
|
-
? directChildren(node).find((child) => child.localName === localName)
|
|
3355
|
-
: undefined;
|
|
3356
|
-
const childrenByName = (node, localName) => node
|
|
3357
|
-
? directChildren(node).filter((child) => child.localName === localName)
|
|
3358
|
-
: [];
|
|
3359
|
-
const docxAttr = (node, name) => {
|
|
3360
|
-
if (!node)
|
|
3361
|
-
return '';
|
|
3362
|
-
return (node.getAttribute(`w:${name}`) ||
|
|
3363
|
-
node.getAttribute(name) ||
|
|
3364
|
-
node.getAttributeNS('http://schemas.openxmlformats.org/wordprocessingml/2006/main', name) ||
|
|
3365
|
-
'');
|
|
3366
|
-
};
|
|
3367
|
-
const docxHexColor = (value) => {
|
|
3368
|
-
if (!value || value.toLowerCase() === 'auto')
|
|
3369
|
-
return '';
|
|
3370
|
-
const normalized = value.replace(/[^0-9a-f]/gi, '');
|
|
3371
|
-
return normalized.length === 6 ? `#${normalized}` : '';
|
|
3372
|
-
};
|
|
3373
|
-
const twipsToPt = (value) => {
|
|
3374
|
-
const n = Number(value);
|
|
3375
|
-
return Number.isFinite(n) ? `${Math.max(n / 20, 0)}pt` : '';
|
|
3376
|
-
};
|
|
3377
|
-
const halfPointsToPt = (value) => {
|
|
3378
|
-
const n = Number(value);
|
|
3379
|
-
return Number.isFinite(n) ? `${Math.max(n / 2, 1)}pt` : '';
|
|
3380
|
-
};
|
|
3381
|
-
const cssRules = (rules) => rules
|
|
3382
|
-
.filter(([, value]) => Boolean(value))
|
|
3383
|
-
.map(([name, value]) => `${name}: ${value}`)
|
|
3384
|
-
.join('; ');
|
|
3385
|
-
const styleAttr = (style) => (style ? ` style="${escapeHtml(style)}"` : '');
|
|
3386
|
-
const convertDocxParagraphStyle = (paragraph) => {
|
|
3387
|
-
const pPr = firstChildByName(paragraph, 'pPr');
|
|
3388
|
-
if (!pPr)
|
|
3389
|
-
return '';
|
|
3390
|
-
const spacing = firstChildByName(pPr, 'spacing');
|
|
3391
|
-
const jc = firstChildByName(pPr, 'jc');
|
|
3392
|
-
const indent = firstChildByName(pPr, 'ind');
|
|
3393
|
-
const borderBottom = firstChildByName(firstChildByName(pPr, 'pBdr'), 'bottom');
|
|
3394
|
-
const line = docxAttr(spacing, 'line');
|
|
3395
|
-
const lineRule = docxAttr(spacing, 'lineRule');
|
|
3396
|
-
return cssRules([
|
|
3397
|
-
['text-align', docxAttr(jc, 'val')],
|
|
3398
|
-
['margin-top', twipsToPt(docxAttr(spacing, 'before'))],
|
|
3399
|
-
['margin-bottom', twipsToPt(docxAttr(spacing, 'after'))],
|
|
3400
|
-
['margin-left', twipsToPt(docxAttr(indent, 'left'))],
|
|
3401
|
-
['text-indent', twipsToPt(docxAttr(indent, 'firstLine'))],
|
|
3402
|
-
['line-height', line && lineRule === 'auto' ? `${Number(line) / 240}` : ''],
|
|
3403
|
-
['border-bottom', docxBorderCss(borderBottom)],
|
|
3404
|
-
]);
|
|
3405
|
-
};
|
|
3406
|
-
const convertDocxRunStyle = (run) => {
|
|
3407
|
-
const rPr = firstChildByName(run, 'rPr');
|
|
3408
|
-
if (!rPr)
|
|
3409
|
-
return '';
|
|
3410
|
-
const color = docxHexColor(docxAttr(firstChildByName(rPr, 'color'), 'val'));
|
|
3411
|
-
const highlight = docxHexColor(docxAttr(firstChildByName(rPr, 'highlight'), 'val'));
|
|
3412
|
-
const shade = docxHexColor(docxAttr(firstChildByName(rPr, 'shd'), 'fill'));
|
|
3413
|
-
const size = halfPointsToPt(docxAttr(firstChildByName(rPr, 'sz'), 'val'));
|
|
3414
|
-
const underline = firstChildByName(rPr, 'u');
|
|
3415
|
-
return cssRules([
|
|
3416
|
-
['font-weight', firstChildByName(rPr, 'b') ? '700' : ''],
|
|
3417
|
-
['font-style', firstChildByName(rPr, 'i') ? 'italic' : ''],
|
|
3418
|
-
['text-decoration', underline ? 'underline' : ''],
|
|
3419
|
-
['color', color],
|
|
3420
|
-
['background-color', highlight || shade],
|
|
3421
|
-
['font-size', size],
|
|
3422
|
-
]);
|
|
3423
|
-
};
|
|
3424
|
-
const convertDocxRun = (run) => {
|
|
3425
|
-
const rPr = firstChildByName(run, 'rPr');
|
|
3426
|
-
const vertAlign = docxAttr(firstChildByName(rPr, 'vertAlign'), 'val');
|
|
3427
|
-
const style = convertDocxRunStyle(run);
|
|
3428
|
-
const content = directChildren(run)
|
|
3429
|
-
.map((child) => {
|
|
3430
|
-
if (child.localName === 't')
|
|
3431
|
-
return escapeHtml(child.textContent || '');
|
|
3432
|
-
if (child.localName === 'tab')
|
|
3433
|
-
return ' ';
|
|
3434
|
-
if (child.localName === 'br') {
|
|
3435
|
-
return docxAttr(child, 'type') === 'page'
|
|
3436
|
-
? '<hr class="srte-docx-page-break">'
|
|
3437
|
-
: '<br>';
|
|
3438
|
-
}
|
|
3439
|
-
return '';
|
|
3440
|
-
})
|
|
3441
|
-
.join('');
|
|
3442
|
-
if (!content)
|
|
3443
|
-
return '';
|
|
3444
|
-
const tag = vertAlign === 'superscript' ? 'sup' : vertAlign === 'subscript' ? 'sub' : 'span';
|
|
3445
|
-
return `<${tag}${styleAttr(style)}>${content}</${tag}>`;
|
|
3446
|
-
};
|
|
3447
|
-
const convertDocxParagraph = (paragraph) => {
|
|
3448
|
-
const style = convertDocxParagraphStyle(paragraph);
|
|
3449
|
-
const content = childrenByName(paragraph, 'r').map(convertDocxRun).join('');
|
|
3450
|
-
return `<p${styleAttr(style)}>${content || '<br>'}</p>`;
|
|
3451
|
-
};
|
|
3452
|
-
const docxBorderCss = (border) => {
|
|
3453
|
-
if (!border)
|
|
3454
|
-
return '';
|
|
3455
|
-
const val = docxAttr(border, 'val');
|
|
3456
|
-
if (!val || val === 'nil' || val === 'none')
|
|
3457
|
-
return '';
|
|
3458
|
-
const size = Number(docxAttr(border, 'sz')) || 4;
|
|
3459
|
-
const width = Math.max(size / 8, 0.5);
|
|
3460
|
-
const color = docxHexColor(docxAttr(border, 'color')) || '#d1d5db';
|
|
3461
|
-
return `${width}px solid ${color}`;
|
|
3462
|
-
};
|
|
3463
|
-
const convertDocxCellStyle = (cell) => {
|
|
3464
|
-
const tcPr = firstChildByName(cell, 'tcPr');
|
|
3465
|
-
const width = twipsToPt(docxAttr(firstChildByName(tcPr, 'tcW'), 'w'));
|
|
3466
|
-
const shade = docxHexColor(docxAttr(firstChildByName(tcPr, 'shd'), 'fill'));
|
|
3467
|
-
const borders = firstChildByName(tcPr, 'tcBorders');
|
|
3468
|
-
const top = docxBorderCss(firstChildByName(borders, 'top'));
|
|
3469
|
-
const right = docxBorderCss(firstChildByName(borders, 'right'));
|
|
3470
|
-
const bottom = docxBorderCss(firstChildByName(borders, 'bottom'));
|
|
3471
|
-
const left = docxBorderCss(firstChildByName(borders, 'left'));
|
|
3472
|
-
return cssRules([
|
|
3473
|
-
['width', width],
|
|
3474
|
-
['background-color', shade],
|
|
3475
|
-
['border-top', top],
|
|
3476
|
-
['border-right', right],
|
|
3477
|
-
['border-bottom', bottom],
|
|
3478
|
-
['border-left', left],
|
|
3479
|
-
['padding', '8px'],
|
|
3480
|
-
['vertical-align', 'top'],
|
|
3481
|
-
]);
|
|
3482
|
-
};
|
|
3483
|
-
const convertDocxTable = (table) => {
|
|
3484
|
-
const rows = childrenByName(table, 'tr')
|
|
3485
|
-
.map((row) => {
|
|
3486
|
-
const cells = childrenByName(row, 'tc')
|
|
3487
|
-
.map((cell) => {
|
|
3488
|
-
const content = directChildren(cell)
|
|
3489
|
-
.filter((child) => child.localName === 'p' || child.localName === 'tbl')
|
|
3490
|
-
.map((child) => child.localName === 'p' ? convertDocxParagraph(child) : convertDocxTable(child))
|
|
3491
|
-
.join('');
|
|
3492
|
-
return `<td${styleAttr(convertDocxCellStyle(cell))}>${content || '<p><br></p>'}</td>`;
|
|
3493
|
-
})
|
|
3494
|
-
.join('');
|
|
3495
|
-
return `<tr>${cells}</tr>`;
|
|
3496
|
-
})
|
|
3497
|
-
.join('');
|
|
3498
|
-
return `<table style="border-collapse: collapse; width: 100%; margin: 12px 0;"><tbody>${rows}</tbody></table>`;
|
|
3499
|
-
};
|
|
3500
|
-
const enhanceImportedTables = (root) => {
|
|
3501
|
-
const tables = root.querySelectorAll('table');
|
|
3502
|
-
tables.forEach(tbl => {
|
|
3503
|
-
tbl.style.borderCollapse = tbl.style.borderCollapse || 'collapse';
|
|
3504
|
-
tbl.style.width = tbl.style.width || '100%';
|
|
3505
|
-
const cells = tbl.querySelectorAll('td, th');
|
|
3506
|
-
cells.forEach(cell => {
|
|
3507
|
-
const el = cell;
|
|
3508
|
-
if (!el.style.border && !el.style.borderTop && !el.style.borderRight && !el.style.borderBottom && !el.style.borderLeft) {
|
|
3509
|
-
el.style.border = '1px solid #d1d5db';
|
|
3510
|
-
}
|
|
3511
|
-
el.style.padding = el.style.padding || '8px';
|
|
3512
|
-
el.style.verticalAlign = el.style.verticalAlign || 'top';
|
|
3513
|
-
});
|
|
3514
|
-
});
|
|
3515
|
-
};
|
|
3516
|
-
const escapeHtml = (value) => value
|
|
3517
|
-
.replace(/&/g, "&")
|
|
3518
|
-
.replace(/</g, "<")
|
|
3519
|
-
.replace(/>/g, ">");
|
|
3520
|
-
const htmlToMarkdown = (html) => {
|
|
3521
|
-
const root = document.createElement("div");
|
|
3522
|
-
root.innerHTML = html;
|
|
3523
|
-
const walk = (node) => {
|
|
3524
|
-
if (node.nodeType === Node.TEXT_NODE)
|
|
3525
|
-
return node.textContent || "";
|
|
3526
|
-
if (!(node instanceof HTMLElement))
|
|
3527
|
-
return "";
|
|
3528
|
-
const content = Array.from(node.childNodes).map(walk).join("");
|
|
3529
|
-
const tag = node.tagName.toLowerCase();
|
|
3530
|
-
if (tag === "strong" || tag === "b")
|
|
3531
|
-
return `**${content}**`;
|
|
3532
|
-
if (tag === "em" || tag === "i")
|
|
3533
|
-
return `*${content}*`;
|
|
3534
|
-
if (tag === "code")
|
|
3535
|
-
return `\`${content}\``;
|
|
3536
|
-
if (tag === "br")
|
|
3537
|
-
return "\n";
|
|
3538
|
-
if (/h[1-6]/.test(tag))
|
|
3539
|
-
return `${"#".repeat(Number(tag[1]))} ${content.trim()}\n\n`;
|
|
3540
|
-
if (tag === "p")
|
|
3541
|
-
return `${content.trim()}\n\n`;
|
|
3542
|
-
if (tag === "li")
|
|
3543
|
-
return `- ${content.trim()}\n`;
|
|
3544
|
-
if (tag === "ul" || tag === "ol")
|
|
3545
|
-
return `${content}\n`;
|
|
3546
|
-
if (tag === "blockquote")
|
|
3547
|
-
return `> ${content.trim()}\n\n`;
|
|
3548
|
-
if (tag === "table")
|
|
3549
|
-
return `${node.outerHTML}\n\n`;
|
|
3550
|
-
if (tag === "img")
|
|
3551
|
-
return ` || ""})`;
|
|
3552
|
-
if (tag === "a")
|
|
3553
|
-
return `[${content}](${node.getAttribute("href") || ""})`;
|
|
3554
|
-
return content;
|
|
3555
|
-
};
|
|
3556
|
-
return Array.from(root.childNodes).map(walk).join("").replace(/\n{3,}/g, "\n\n").trim();
|
|
3557
|
-
};
|
|
3558
|
-
const importTextFile = async (files, type) => {
|
|
3559
|
-
if (!files || files.length === 0)
|
|
3560
|
-
return;
|
|
3561
|
-
const file = files[0];
|
|
3562
|
-
const text = await file.text();
|
|
3563
|
-
const html = type === "html" ? text : markdownToCompatibilityHtml(text);
|
|
3564
|
-
const el = editableRef.current;
|
|
3565
|
-
const hasContent = el && el.textContent && el.textContent.trim().length > 0;
|
|
3566
|
-
insertImportedHtml(html, hasContent ? "append" : "replace", {
|
|
3567
|
-
preserveColors: true,
|
|
3568
|
-
preserveDocumentLayout: true,
|
|
3569
|
-
});
|
|
3570
|
-
};
|
|
3571
|
-
const downloadText = (filename, content, mimeType) => {
|
|
3572
|
-
const blob = new Blob([content], { type: mimeType });
|
|
3573
|
-
const url = URL.createObjectURL(blob);
|
|
3574
|
-
const link = document.createElement("a");
|
|
3575
|
-
link.href = url;
|
|
3576
|
-
link.download = filename;
|
|
3577
|
-
document.body.appendChild(link);
|
|
3578
|
-
link.click();
|
|
3579
|
-
link.remove();
|
|
3580
|
-
URL.revokeObjectURL(url);
|
|
3581
|
-
};
|
|
3582
|
-
const buildStandaloneHtmlDocument = (html) => `<!doctype html>
|
|
3583
|
-
<html lang="en">
|
|
3584
|
-
<head>
|
|
3585
|
-
<meta charset="utf-8">
|
|
3586
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
3587
|
-
<title>Smart RTE Export</title>
|
|
3588
|
-
<style>
|
|
3589
|
-
:root {
|
|
3590
|
-
--srte-bg: #ffffff;
|
|
3591
|
-
--srte-text: #111111;
|
|
3592
|
-
--srte-text-muted: #4b5563;
|
|
3593
|
-
--srte-border: #d1d5db;
|
|
3594
|
-
--srte-border-light: #e5e7eb;
|
|
3595
|
-
--srte-accent: #1e90ff;
|
|
3596
|
-
--srte-accent-bg: rgba(30, 144, 255, 0.15);
|
|
3597
|
-
--srte-surface-subtle: #f3f4f6;
|
|
3598
|
-
}
|
|
3599
|
-
html, body {
|
|
3600
|
-
margin: 0;
|
|
3601
|
-
background: var(--srte-bg);
|
|
3602
|
-
color: var(--srte-text);
|
|
3603
|
-
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
3604
|
-
line-height: 1.6;
|
|
3605
|
-
}
|
|
3606
|
-
body {
|
|
3607
|
-
padding: 32px;
|
|
3608
|
-
}
|
|
3609
|
-
.srte-export {
|
|
3610
|
-
max-width: 960px;
|
|
3611
|
-
margin: 0 auto;
|
|
3612
|
-
}
|
|
3613
|
-
.srte-export h1,
|
|
3614
|
-
.srte-export h2,
|
|
3615
|
-
.srte-export h3,
|
|
3616
|
-
.srte-export h4,
|
|
3617
|
-
.srte-export h5,
|
|
3618
|
-
.srte-export h6 {
|
|
3619
|
-
line-height: 1.3;
|
|
3620
|
-
margin: 1.25em 0 0.5em;
|
|
3621
|
-
font-weight: 700;
|
|
3622
|
-
color: var(--srte-text);
|
|
3623
|
-
}
|
|
3624
|
-
.srte-export p {
|
|
3625
|
-
margin: 0 0 0.85em;
|
|
3626
|
-
}
|
|
3627
|
-
.srte-export blockquote {
|
|
3628
|
-
border-left: 4px solid var(--srte-accent);
|
|
3629
|
-
margin: 0.75em 0;
|
|
3630
|
-
padding: 0.5em 1em;
|
|
3631
|
-
background: var(--srte-surface-subtle);
|
|
3632
|
-
color: var(--srte-text);
|
|
3633
|
-
}
|
|
3634
|
-
.srte-export ul,
|
|
3635
|
-
.srte-export ol {
|
|
3636
|
-
margin: 0.75em 0;
|
|
3637
|
-
padding-left: 1.75em;
|
|
3638
|
-
list-style-position: outside;
|
|
3639
|
-
}
|
|
3640
|
-
.srte-export ul {
|
|
3641
|
-
list-style-type: disc;
|
|
3642
|
-
}
|
|
3643
|
-
.srte-export ol {
|
|
3644
|
-
list-style-type: decimal;
|
|
3645
|
-
}
|
|
3646
|
-
.srte-export li {
|
|
3647
|
-
display: list-item;
|
|
3648
|
-
margin: 0.25em 0;
|
|
3649
|
-
padding-left: 0.25em;
|
|
3650
|
-
}
|
|
3651
|
-
.srte-export table {
|
|
3652
|
-
border-collapse: collapse;
|
|
3653
|
-
width: 100%;
|
|
3654
|
-
margin: 12px 0;
|
|
3655
|
-
}
|
|
3656
|
-
.srte-export td,
|
|
3657
|
-
.srte-export th {
|
|
3658
|
-
border: 1px solid var(--srte-border);
|
|
3659
|
-
padding: 8px;
|
|
3660
|
-
vertical-align: top;
|
|
3661
|
-
text-align: left;
|
|
3662
|
-
}
|
|
3663
|
-
.srte-export th {
|
|
3664
|
-
background: var(--srte-surface-subtle);
|
|
3665
|
-
font-weight: 700;
|
|
3666
|
-
}
|
|
3667
|
-
.srte-export img {
|
|
3668
|
-
max-width: 100%;
|
|
3669
|
-
height: auto;
|
|
3670
|
-
}
|
|
3671
|
-
.srte-export pre,
|
|
3672
|
-
.srte-export code {
|
|
3673
|
-
background: var(--srte-surface-subtle);
|
|
3674
|
-
color: var(--srte-text);
|
|
3675
|
-
border-radius: 4px;
|
|
3676
|
-
}
|
|
3677
|
-
.srte-export pre {
|
|
3678
|
-
padding: 12px;
|
|
3679
|
-
overflow-x: auto;
|
|
3680
|
-
white-space: pre-wrap;
|
|
3681
|
-
}
|
|
3682
|
-
@media print {
|
|
3683
|
-
body {
|
|
3684
|
-
padding: 0;
|
|
3685
|
-
}
|
|
3686
|
-
.srte-export {
|
|
3687
|
-
max-width: none;
|
|
3688
|
-
}
|
|
3689
|
-
}
|
|
3690
|
-
</style>
|
|
3691
|
-
</head>
|
|
3692
|
-
<body>
|
|
3693
|
-
<main class="srte-export">${html || "<p></p>"}</main>
|
|
3694
|
-
</body>
|
|
3695
|
-
</html>`;
|
|
3696
|
-
const exportHtml = () => {
|
|
3697
|
-
const html = editableRef.current?.innerHTML || "";
|
|
3698
|
-
downloadText("smart-rte-export.html", buildStandaloneHtmlDocument(html), "text/html;charset=utf-8");
|
|
3699
|
-
};
|
|
3700
|
-
const exportMarkdown = () => {
|
|
3701
|
-
const html = editableRef.current?.innerHTML || "";
|
|
3702
|
-
downloadText("smart-rte-export.md", htmlToMarkdown(html), "text/markdown");
|
|
3703
|
-
};
|
|
3704
|
-
const htmlToDocxXml = (html) => {
|
|
3705
|
-
const root = document.createElement("div");
|
|
3706
|
-
root.innerHTML = html;
|
|
3707
|
-
const xmlEscape = (value) => value
|
|
3708
|
-
.replace(/&/g, "&")
|
|
3709
|
-
.replace(/</g, "<")
|
|
3710
|
-
.replace(/>/g, ">")
|
|
3711
|
-
.replace(/"/g, """);
|
|
3712
|
-
const colorValue = (value) => {
|
|
3713
|
-
const hex = /^#([0-9a-f]{6})$/i.exec(value.trim());
|
|
3714
|
-
if (hex)
|
|
3715
|
-
return hex[1].toUpperCase();
|
|
3716
|
-
const rgb = /^rgb\(\s*(\d+),\s*(\d+),\s*(\d+)\s*\)$/i.exec(value.trim());
|
|
3717
|
-
if (!rgb)
|
|
3718
|
-
return "";
|
|
3719
|
-
return [rgb[1], rgb[2], rgb[3]]
|
|
3720
|
-
.map((part) => Math.max(0, Math.min(255, Number(part))).toString(16).padStart(2, "0"))
|
|
3721
|
-
.join("")
|
|
3722
|
-
.toUpperCase();
|
|
3723
|
-
};
|
|
3724
|
-
const sizeToHalfPoints = (value) => {
|
|
3725
|
-
const trimmed = value.trim();
|
|
3726
|
-
const match = /^([\d.]+)(px|pt)$/i.exec(trimmed);
|
|
3727
|
-
if (!match)
|
|
3728
|
-
return "";
|
|
3729
|
-
const raw = Number(match[1]);
|
|
3730
|
-
const pt = match[2].toLowerCase() === "px" ? raw * 0.75 : raw;
|
|
3731
|
-
return String(Math.max(2, Math.round(pt * 2)));
|
|
3732
|
-
};
|
|
3733
|
-
const runProperties = (el) => {
|
|
3734
|
-
const style = el.style;
|
|
3735
|
-
const color = colorValue(style.color);
|
|
3736
|
-
const size = sizeToHalfPoints(style.fontSize);
|
|
3737
|
-
const isBold = el.tagName === "B" || el.tagName === "STRONG" || /bold|700|800|900/.test(style.fontWeight);
|
|
3738
|
-
const isItalic = el.tagName === "I" || el.tagName === "EM" || style.fontStyle === "italic";
|
|
3739
|
-
const isUnderline = el.tagName === "U" || style.textDecoration.includes("underline");
|
|
3740
|
-
return [
|
|
3741
|
-
isBold ? "<w:b/>" : "",
|
|
3742
|
-
isItalic ? "<w:i/>" : "",
|
|
3743
|
-
isUnderline ? '<w:u w:val="single"/>' : "",
|
|
3744
|
-
color ? `<w:color w:val="${color}"/>` : "",
|
|
3745
|
-
size ? `<w:sz w:val="${size}"/>` : "",
|
|
3746
|
-
].join("");
|
|
3747
|
-
};
|
|
3748
|
-
const runs = (node, inheritedProps = "") => {
|
|
3749
|
-
if (node.nodeType === Node.TEXT_NODE) {
|
|
3750
|
-
const text = node.textContent || "";
|
|
3751
|
-
return text ? `<w:r>${inheritedProps ? `<w:rPr>${inheritedProps}</w:rPr>` : ""}<w:t xml:space="preserve">${xmlEscape(text)}</w:t></w:r>` : "";
|
|
3752
|
-
}
|
|
3753
|
-
if (!(node instanceof HTMLElement))
|
|
3754
|
-
return "";
|
|
3755
|
-
if (node.tagName === "BR")
|
|
3756
|
-
return "<w:r><w:br/></w:r>";
|
|
3757
|
-
if (node.tagName === "IMG") {
|
|
3758
|
-
const alt = node.getAttribute("alt") || node.getAttribute("title") || "Image";
|
|
3759
|
-
return `<w:r><w:t>[Image: ${xmlEscape(alt)}]</w:t></w:r>`;
|
|
3760
|
-
}
|
|
3761
|
-
const props = `${inheritedProps}${runProperties(node)}`;
|
|
3762
|
-
return Array.from(node.childNodes).map((child) => runs(child, props)).join("");
|
|
3763
|
-
};
|
|
3764
|
-
const paragraph = (el, fallbackTag = "p") => {
|
|
3765
|
-
const tag = el.tagName.toLowerCase();
|
|
3766
|
-
const headingMatch = /^h([1-6])$/.exec(tag);
|
|
3767
|
-
const style = el.style;
|
|
3768
|
-
const align = style.textAlign ? `<w:jc w:val="${xmlEscape(style.textAlign)}"/>` : "";
|
|
3769
|
-
const headingSize = headingMatch ? `<w:rPr><w:b/><w:sz w:val="${Math.max(24, 40 - Number(headingMatch[1]) * 4)}"/></w:rPr>` : "";
|
|
3770
|
-
const body = runs(el);
|
|
3771
|
-
return `<w:p><w:pPr>${align}${headingSize}</w:pPr>${body || "<w:r><w:t></w:t></w:r>"}</w:p>`;
|
|
3772
|
-
};
|
|
3773
|
-
const tableCell = (cell) => {
|
|
3774
|
-
const fill = colorValue(cell.style.backgroundColor);
|
|
3775
|
-
const shading = fill ? `<w:shd w:val="clear" w:color="auto" w:fill="${fill}"/>` : "";
|
|
3776
|
-
const cellContent = Array.from(cell.childNodes)
|
|
3777
|
-
.map((child) => child instanceof HTMLElement && ["P", "DIV", "H1", "H2", "H3", "H4", "H5", "H6"].includes(child.tagName)
|
|
3778
|
-
? paragraph(child)
|
|
3779
|
-
: `<w:p>${runs(child)}</w:p>`)
|
|
3780
|
-
.join("");
|
|
3781
|
-
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>`;
|
|
3782
|
-
};
|
|
3783
|
-
const tableXml = (table) => {
|
|
3784
|
-
const rows = Array.from(table.querySelectorAll("tr"));
|
|
3785
|
-
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>`;
|
|
3786
|
-
};
|
|
3787
|
-
const blockXml = (node) => {
|
|
3788
|
-
if (node.nodeType === Node.TEXT_NODE) {
|
|
3789
|
-
const text = node.textContent?.trim();
|
|
3790
|
-
return text ? `<w:p>${runs(node)}</w:p>` : "";
|
|
3791
|
-
}
|
|
3792
|
-
if (!(node instanceof HTMLElement))
|
|
3793
|
-
return "";
|
|
3794
|
-
if (node.tagName === "TABLE")
|
|
3795
|
-
return tableXml(node);
|
|
3796
|
-
if (node.tagName === "UL" || node.tagName === "OL") {
|
|
3797
|
-
return Array.from(node.children).map((li) => `<w:p><w:r><w:t>• </w:t></w:r>${runs(li)}</w:p>`).join("");
|
|
3798
|
-
}
|
|
3799
|
-
if (node.tagName === "BLOCKQUOTE") {
|
|
3800
|
-
return `<w:p><w:pPr><w:ind w:left="720"/></w:pPr>${runs(node)}</w:p>`;
|
|
3801
|
-
}
|
|
3802
|
-
if (node.tagName === "HR")
|
|
3803
|
-
return '<w:p><w:pPr><w:pBdr><w:bottom w:val="single" w:sz="6" w:color="D1D5DB"/></w:pBdr></w:pPr></w:p>';
|
|
3804
|
-
if (["P", "DIV", "PRE", "H1", "H2", "H3", "H4", "H5", "H6"].includes(node.tagName))
|
|
3805
|
-
return paragraph(node);
|
|
3806
|
-
return Array.from(node.childNodes).map(blockXml).join("");
|
|
3807
|
-
};
|
|
3808
|
-
const body = Array.from(root.childNodes).map(blockXml).join("");
|
|
3809
|
-
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>`;
|
|
3810
|
-
};
|
|
3811
|
-
const exportDocx = async () => {
|
|
3812
|
-
const html = editableRef.current?.innerHTML || "";
|
|
3813
|
-
const zip = new JSZip();
|
|
3814
|
-
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>`);
|
|
3815
|
-
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>`);
|
|
3816
|
-
zip.folder("word")?.file("document.xml", htmlToDocxXml(html));
|
|
3817
|
-
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>`);
|
|
3818
|
-
const blob = await zip.generateAsync({ type: "blob", mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" });
|
|
3819
|
-
const url = URL.createObjectURL(blob);
|
|
3820
|
-
const link = document.createElement("a");
|
|
3821
|
-
link.href = url;
|
|
3822
|
-
link.download = "smart-rte-export.docx";
|
|
3823
|
-
document.body.appendChild(link);
|
|
3824
|
-
link.click();
|
|
3825
|
-
link.remove();
|
|
3826
|
-
URL.revokeObjectURL(url);
|
|
3827
|
-
};
|
|
3828
|
-
const exportPdf = () => {
|
|
3829
|
-
const html = editableRef.current?.innerHTML || "";
|
|
3830
|
-
const printWindow = window.open("", "_blank", "width=900,height=700");
|
|
3831
|
-
if (!printWindow)
|
|
3832
|
-
return;
|
|
3833
|
-
printWindow.document.open();
|
|
3834
|
-
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>`);
|
|
3835
|
-
printWindow.document.close();
|
|
3836
|
-
};
|
|
3837
|
-
const fixNegativeMargins = (root) => {
|
|
3559
|
+
const fixNegativeMargins = (root) => {
|
|
3838
3560
|
try {
|
|
3839
3561
|
const nodes = root.querySelectorAll('*');
|
|
3840
3562
|
for (let i = 0; i < nodes.length; i++) {
|
|
@@ -3908,6 +3630,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3908
3630
|
const style = node.getAttribute('style');
|
|
3909
3631
|
if (!style)
|
|
3910
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';
|
|
3911
3639
|
const safeRules = style
|
|
3912
3640
|
.split(';')
|
|
3913
3641
|
.map((rule) => rule.trim())
|
|
@@ -3920,6 +3648,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3920
3648
|
const value = rule.slice(separator + 1).trim().toLowerCase();
|
|
3921
3649
|
if (!allowedStyleNames.has(name))
|
|
3922
3650
|
return false;
|
|
3651
|
+
if (dropsBorderAndPadding && (name.startsWith('border') || name.startsWith('padding')))
|
|
3652
|
+
return false;
|
|
3923
3653
|
if (value.includes('position') || value.includes('expression') || value.includes('javascript:'))
|
|
3924
3654
|
return false;
|
|
3925
3655
|
if (name === 'white-space' && value !== 'pre-wrap')
|
|
@@ -3940,9 +3670,55 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3940
3670
|
if (!el)
|
|
3941
3671
|
return;
|
|
3942
3672
|
fixNegativeMargins(el);
|
|
3673
|
+
normalizeListNesting(el);
|
|
3943
3674
|
ensureTableWrappers(el);
|
|
3944
3675
|
addTableResizeHandles();
|
|
3945
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
|
+
};
|
|
3946
3722
|
const insertHtmlAtEnd = (html) => {
|
|
3947
3723
|
const el = editableRef.current;
|
|
3948
3724
|
if (!el)
|
|
@@ -4053,6 +3829,27 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
4053
3829
|
}
|
|
4054
3830
|
});
|
|
4055
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
|
+
};
|
|
4056
3853
|
const normalizeInvalidQuoteNesting = (root) => {
|
|
4057
3854
|
root.querySelectorAll("blockquote blockquote").forEach((quote) => {
|
|
4058
3855
|
const outerQuote = quote.parentElement?.closest("blockquote");
|
|
@@ -4080,7 +3877,10 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
4080
3877
|
outerCodeBlock.parentElement.insertBefore(codeBlock, outerCodeBlock.nextSibling);
|
|
4081
3878
|
}
|
|
4082
3879
|
});
|
|
4083
|
-
|
|
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) => {
|
|
4084
3884
|
const nestedCodeBlocks = Array.from(container.children).filter((child) => child.tagName === "PRE");
|
|
4085
3885
|
nestedCodeBlocks.forEach((codeBlock) => {
|
|
4086
3886
|
container.parentElement?.insertBefore(codeBlock, container.nextSibling);
|
|
@@ -4125,10 +3925,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
4125
3925
|
return;
|
|
4126
3926
|
// Auto-fix negative margins that might cause visibility issues
|
|
4127
3927
|
fixNegativeMargins(el);
|
|
3928
|
+
normalizeListNesting(el);
|
|
4128
3929
|
// Quotes are document blocks and cannot be nested by drag and drop.
|
|
4129
3930
|
normalizeInvalidQuoteNesting(el);
|
|
4130
3931
|
// Code blocks are document blocks and cannot be nested by drag and drop.
|
|
4131
3932
|
normalizeInvalidCodeBlockNesting(el);
|
|
3933
|
+
normalizeBlockContainerStyling(el);
|
|
4132
3934
|
// Tables are document-level blocks and must not remain inside code or list items.
|
|
4133
3935
|
normalizeInvalidTableNesting(el);
|
|
4134
3936
|
// Ensure tables are wrapped for horizontal scrolling
|
|
@@ -4181,6 +3983,27 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
4181
3983
|
sel?.removeAllRanges();
|
|
4182
3984
|
sel?.addRange(range);
|
|
4183
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
|
+
}
|
|
4184
4007
|
const html = buildTableHTML(tableRows, tableCols);
|
|
4185
4008
|
// Insert via Range for broader support
|
|
4186
4009
|
const wrapper = document.createElement("div");
|
|
@@ -4351,6 +4174,26 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
4351
4174
|
if (!anchor)
|
|
4352
4175
|
return;
|
|
4353
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
|
+
}
|
|
4354
4197
|
// Collect content and remove other cells
|
|
4355
4198
|
const contents = [];
|
|
4356
4199
|
const cellsToMerge = getCellsInGridRect(tbody, sr, sc, er, ec);
|
|
@@ -4380,7 +4223,20 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
4380
4223
|
const pos = getCellPosition(cell);
|
|
4381
4224
|
if (!pos)
|
|
4382
4225
|
return;
|
|
4383
|
-
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
|
+
}
|
|
4384
4240
|
const newRow = document.createElement("tr");
|
|
4385
4241
|
const numCols = Array.from(row.children).filter((c) => ["TD", "TH"].includes(c.tagName)).length;
|
|
4386
4242
|
for (let i = 0; i < numCols; i++) {
|
|
@@ -4391,7 +4247,6 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
4391
4247
|
td.innerHTML = " ";
|
|
4392
4248
|
newRow.appendChild(td);
|
|
4393
4249
|
}
|
|
4394
|
-
const insertIndex = dir === "above" ? rIdx : rIdx + 1;
|
|
4395
4250
|
const refRow = tbody.children[insertIndex] || null;
|
|
4396
4251
|
tbody.insertBefore(newRow, refRow);
|
|
4397
4252
|
};
|
|
@@ -4399,7 +4254,20 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
4399
4254
|
const pos = getCellPosition(cell);
|
|
4400
4255
|
if (!pos)
|
|
4401
4256
|
return;
|
|
4402
|
-
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
|
+
}
|
|
4403
4271
|
tbody.removeChild(row);
|
|
4404
4272
|
if (tbody.querySelectorAll("tr").length === 0) {
|
|
4405
4273
|
table.parentElement?.removeChild(table);
|
|
@@ -4410,8 +4278,20 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
4410
4278
|
if (!pos)
|
|
4411
4279
|
return;
|
|
4412
4280
|
const { tbody, cIdx } = pos;
|
|
4413
|
-
const rows = Array.from(tbody.querySelectorAll("tr"));
|
|
4414
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"));
|
|
4415
4295
|
for (const r of rows) {
|
|
4416
4296
|
const cells = Array.from(r.children).filter((c) => ["TD", "TH"].includes(c.tagName));
|
|
4417
4297
|
const td = document.createElement("td");
|
|
@@ -4431,6 +4311,20 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
4431
4311
|
if (!pos)
|
|
4432
4312
|
return;
|
|
4433
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
|
+
}
|
|
4434
4328
|
const rows = Array.from(tbody.querySelectorAll("tr"));
|
|
4435
4329
|
for (const r of rows) {
|
|
4436
4330
|
const cells = Array.from(r.children).filter((c) => ["TD", "TH"].includes(c.tagName));
|
|
@@ -4444,7 +4338,21 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
4444
4338
|
table.parentElement?.removeChild(table);
|
|
4445
4339
|
};
|
|
4446
4340
|
const toggleHeaderCell = (cell) => {
|
|
4341
|
+
const pos = getCellPosition(cell);
|
|
4342
|
+
if (!pos)
|
|
4343
|
+
return;
|
|
4447
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
|
+
}
|
|
4448
4356
|
const isTh = cell.tagName === "TH";
|
|
4449
4357
|
replaceTableCellTag(cell, isTh ? "td" : "th");
|
|
4450
4358
|
handleInput();
|
|
@@ -4454,6 +4362,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
4454
4362
|
if (!pos)
|
|
4455
4363
|
return;
|
|
4456
4364
|
const { table } = pos;
|
|
4365
|
+
if (editorControllerRef.current.bindRoot(editableRef.current).removeTable(table))
|
|
4366
|
+
return;
|
|
4457
4367
|
table.parentElement?.removeChild(table);
|
|
4458
4368
|
};
|
|
4459
4369
|
const splitCell = (cell) => {
|
|
@@ -4465,6 +4375,19 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
4465
4375
|
const cs = Math.max(1, cell.colSpan || 1);
|
|
4466
4376
|
if (rs === 1 && cs === 1)
|
|
4467
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
|
+
}
|
|
4468
4391
|
// Reset current cell
|
|
4469
4392
|
cell.rowSpan = 1;
|
|
4470
4393
|
cell.colSpan = 1;
|
|
@@ -4501,6 +4424,17 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
4501
4424
|
if (!pos)
|
|
4502
4425
|
return;
|
|
4503
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
|
+
}
|
|
4504
4438
|
const { row } = pos;
|
|
4505
4439
|
const cells = cellsOfRow(row);
|
|
4506
4440
|
const shouldMakeHeader = cells.some((c) => c.tagName !== "TH");
|
|
@@ -4520,6 +4454,17 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
4520
4454
|
if (!pos)
|
|
4521
4455
|
return;
|
|
4522
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
|
+
}
|
|
4523
4468
|
const { tbody, cIdx } = pos;
|
|
4524
4469
|
const { grid } = getTableGrid(tbody);
|
|
4525
4470
|
const seen = new Set();
|
|
@@ -4539,25 +4484,44 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
4539
4484
|
};
|
|
4540
4485
|
const applyBgToSelection = (hex, fallbackCell, explicitCells) => {
|
|
4541
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
|
+
}
|
|
4542
4518
|
const applyFill = (cell) => {
|
|
4543
4519
|
cell.style.background = hex;
|
|
4544
4520
|
if (readableColor)
|
|
4545
4521
|
cell.style.color = readableColor;
|
|
4546
4522
|
};
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
return;
|
|
4550
|
-
}
|
|
4551
|
-
const sel = shouldUseTableSelection(fallbackCell) ? selectionRef.current : null;
|
|
4552
|
-
if (sel) {
|
|
4553
|
-
const cells = getCellsInGridRect(sel.tbody, sel.sr, sel.sc, sel.er, sel.ec);
|
|
4554
|
-
clearSelectionDecor();
|
|
4555
|
-
cells.forEach(applyFill);
|
|
4556
|
-
}
|
|
4557
|
-
else if (fallbackCell) {
|
|
4558
|
-
clearSelectionDecor();
|
|
4559
|
-
applyFill(fallbackCell);
|
|
4560
|
-
}
|
|
4523
|
+
clearSelectionDecor();
|
|
4524
|
+
targetCells.forEach(applyFill);
|
|
4561
4525
|
};
|
|
4562
4526
|
const tableCellFillHex = (cell) => {
|
|
4563
4527
|
const storedSelectionBackground = cell.__rtePrevBg;
|
|
@@ -4580,18 +4544,35 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
4580
4544
|
return allSame ? first : tableCellFillHex(cell);
|
|
4581
4545
|
};
|
|
4582
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
|
+
}
|
|
4583
4570
|
const applyToggle = (cell) => {
|
|
4584
4571
|
const cur = cell.style.border;
|
|
4585
4572
|
cell.style.border =
|
|
4586
4573
|
cur && cur !== "none" ? "none" : "1px solid #d1d5db";
|
|
4587
4574
|
};
|
|
4588
|
-
|
|
4589
|
-
if (sel) {
|
|
4590
|
-
getCellsInGridRect(sel.tbody, sel.sr, sel.sc, sel.er, sel.ec).forEach(applyToggle);
|
|
4591
|
-
}
|
|
4592
|
-
else if (fallbackCell) {
|
|
4593
|
-
applyToggle(fallbackCell);
|
|
4594
|
-
}
|
|
4575
|
+
targetCells.forEach(applyToggle);
|
|
4595
4576
|
};
|
|
4596
4577
|
const runTableCellAction = (cell, action) => {
|
|
4597
4578
|
pushEditorHistory();
|
|
@@ -4690,7 +4671,14 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
4690
4671
|
}
|
|
4691
4672
|
};
|
|
4692
4673
|
const handleTableResizeEnd = () => {
|
|
4693
|
-
|
|
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 } });
|
|
4694
4682
|
tableResizeRef.current = null;
|
|
4695
4683
|
document.body.style.cursor = '';
|
|
4696
4684
|
document.body.style.userSelect = '';
|
|
@@ -5005,6 +4993,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
5005
4993
|
return sibling;
|
|
5006
4994
|
};
|
|
5007
4995
|
const moveSelectedBlocks = (direction) => {
|
|
4996
|
+
if (!moveFeature)
|
|
4997
|
+
return false;
|
|
5008
4998
|
const editor = editableRef.current;
|
|
5009
4999
|
const range = getSelectionRangeInEditor();
|
|
5010
5000
|
if (!editor || !range || range.collapsed)
|
|
@@ -5020,6 +5010,19 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
5020
5010
|
const parent = blocks[0].parentElement;
|
|
5021
5011
|
if (!parent || blocks.some((block) => block.parentElement !== parent))
|
|
5022
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
|
+
}
|
|
5023
5026
|
pushEditorHistory();
|
|
5024
5027
|
if (direction === "up") {
|
|
5025
5028
|
const previous = elementSibling(blocks[0], "previous");
|
|
@@ -5053,6 +5056,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
5053
5056
|
return true;
|
|
5054
5057
|
};
|
|
5055
5058
|
const moveCurrentElement = (direction) => {
|
|
5059
|
+
if (!moveFeature)
|
|
5060
|
+
return;
|
|
5056
5061
|
restoreSavedSelection();
|
|
5057
5062
|
const editor = editableRef.current;
|
|
5058
5063
|
const range = getSelectionRangeInEditor();
|
|
@@ -5077,6 +5082,23 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
5077
5082
|
cell.appendChild(paragraph);
|
|
5078
5083
|
target = paragraph;
|
|
5079
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
|
+
}
|
|
5080
5102
|
if (target.tagName.toLowerCase() === "li") {
|
|
5081
5103
|
const list = target.parentElement;
|
|
5082
5104
|
if (!list)
|
|
@@ -5196,6 +5218,70 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
5196
5218
|
event.preventDefault();
|
|
5197
5219
|
}
|
|
5198
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
|
+
};
|
|
5199
5285
|
const editorClass = `srte-editor${theme === 'dark' ? ' srte-dark' : ''}${className ? ' ' + className : ''}`;
|
|
5200
5286
|
return (_jsxs("div", { className: editorClass, style: {
|
|
5201
5287
|
border: "1px solid var(--srte-border)",
|
|
@@ -5256,19 +5342,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
5256
5342
|
}
|
|
5257
5343
|
}
|
|
5258
5344
|
e.currentTarget.value = "";
|
|
5259
|
-
} })), _jsx("
|
|
5260
|
-
handlePdfFiles(e.currentTarget.files);
|
|
5261
|
-
e.currentTarget.value = "";
|
|
5262
|
-
} }), _jsx("input", { ref: docxInputRef, type: "file", accept: ".docx", style: { display: "none" }, onChange: (e) => {
|
|
5263
|
-
handleDocxFiles(e.currentTarget.files);
|
|
5264
|
-
e.currentTarget.value = "";
|
|
5265
|
-
} }), _jsx("input", { ref: htmlInputRef, type: "file", accept: ".html,.htm,text/html", style: { display: "none" }, onChange: (e) => {
|
|
5266
|
-
importTextFile(e.currentTarget.files, "html");
|
|
5267
|
-
e.currentTarget.value = "";
|
|
5268
|
-
} }), _jsx("input", { ref: mdInputRef, type: "file", accept: ".md,.markdown,text/markdown,text/plain", style: { display: "none" }, onChange: (e) => {
|
|
5269
|
-
importTextFile(e.currentTarget.files, "md");
|
|
5270
|
-
e.currentTarget.value = "";
|
|
5271
|
-
} }), _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) => {
|
|
5272
5346
|
const val = e.target.value;
|
|
5273
5347
|
if (val === "p")
|
|
5274
5348
|
applyFormatBlock("<p>");
|
|
@@ -5291,56 +5365,57 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
5291
5365
|
borderRadius: 6,
|
|
5292
5366
|
background: "var(--srte-input-bg)",
|
|
5293
5367
|
color: "var(--srte-input-text)",
|
|
5294
|
-
}, 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: {
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
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" })] })] }), [
|
|
5342
5416
|
{
|
|
5343
5417
|
key: "check",
|
|
5418
|
+
enabled: checklistFeature,
|
|
5344
5419
|
icon: "checklist",
|
|
5345
5420
|
title: "Checklist",
|
|
5346
5421
|
active: activeState.checklist,
|
|
@@ -5349,21 +5424,37 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
5349
5424
|
},
|
|
5350
5425
|
{
|
|
5351
5426
|
key: "bullet",
|
|
5427
|
+
enabled: listFeature,
|
|
5352
5428
|
icon: "bullets",
|
|
5353
5429
|
title: "Bulleted list",
|
|
5354
5430
|
active: activeState.unorderedList,
|
|
5355
|
-
action: () =>
|
|
5356
|
-
options: [
|
|
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
|
+
],
|
|
5357
5439
|
},
|
|
5358
5440
|
{
|
|
5359
5441
|
key: "ordered",
|
|
5442
|
+
enabled: listFeature,
|
|
5360
5443
|
icon: "numbers",
|
|
5361
5444
|
title: "Numbered list",
|
|
5362
5445
|
active: activeState.orderedList,
|
|
5363
|
-
action: () =>
|
|
5364
|
-
options: [
|
|
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
|
+
],
|
|
5365
5456
|
},
|
|
5366
|
-
].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) => {
|
|
5367
5458
|
const selected = event.currentTarget.value;
|
|
5368
5459
|
if (selected === "check:plain")
|
|
5369
5460
|
applyChecklist(false);
|
|
@@ -5372,7 +5463,10 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
5372
5463
|
else if (selected)
|
|
5373
5464
|
applyListStyle(selected);
|
|
5374
5465
|
event.currentTarget.value = "";
|
|
5375
|
-
}, 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" }) }),
|
|
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: () => {
|
|
5376
5470
|
selectedImage.style.display = "block";
|
|
5377
5471
|
selectedImage.style.margin = "0 auto";
|
|
5378
5472
|
selectedImage.style.float = "none";
|
|
@@ -5390,17 +5484,10 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
5390
5484
|
selectedImage.style.margin = "0 0 8px 8px";
|
|
5391
5485
|
scheduleImageOverlay();
|
|
5392
5486
|
handleInput();
|
|
5393
|
-
} })] })), _jsxs(ToolbarGroup, { label: "Document", priority: 3, children: [
|
|
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) => {
|
|
5394
5488
|
const format = event.currentTarget.value;
|
|
5395
5489
|
event.currentTarget.value = "";
|
|
5396
|
-
|
|
5397
|
-
pdfInputRef.current?.click();
|
|
5398
|
-
else if (format === "docx")
|
|
5399
|
-
docxInputRef.current?.click();
|
|
5400
|
-
else if (format === "html")
|
|
5401
|
-
htmlInputRef.current?.click();
|
|
5402
|
-
else if (format === "markdown")
|
|
5403
|
-
mdInputRef.current?.click();
|
|
5490
|
+
openFormatImport(format);
|
|
5404
5491
|
}, style: {
|
|
5405
5492
|
height: 32,
|
|
5406
5493
|
padding: "0 8px",
|
|
@@ -5409,17 +5496,10 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
5409
5496
|
background: "var(--srte-input-bg)",
|
|
5410
5497
|
color: "var(--srte-input-text)",
|
|
5411
5498
|
opacity: loadingPdf || loadingDocx ? 0.6 : 1,
|
|
5412
|
-
}, children: [_jsx("option", { value: "", disabled: true, children: loadingPdf || loadingDocx ? "Importing…" : "Import…" }),
|
|
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) => {
|
|
5413
5500
|
const format = event.currentTarget.value;
|
|
5414
5501
|
event.currentTarget.value = "";
|
|
5415
|
-
|
|
5416
|
-
exportHtml();
|
|
5417
|
-
else if (format === "markdown")
|
|
5418
|
-
exportMarkdown();
|
|
5419
|
-
else if (format === "docx")
|
|
5420
|
-
void exportDocx();
|
|
5421
|
-
else if (format === "pdf")
|
|
5422
|
-
exportPdf();
|
|
5502
|
+
void runFormatExport(format);
|
|
5423
5503
|
}, style: {
|
|
5424
5504
|
height: 32,
|
|
5425
5505
|
padding: "0 8px",
|
|
@@ -5427,7 +5507,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
5427
5507
|
borderRadius: 6,
|
|
5428
5508
|
background: "var(--srte-input-bg)",
|
|
5429
5509
|
color: "var(--srte-input-text)",
|
|
5430
|
-
}, children: [_jsx("option", { value: "", disabled: true, children: "Export\u2026" }),
|
|
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) => {
|
|
5431
5511
|
if (item?.url)
|
|
5432
5512
|
insertImageAtSelection(item);
|
|
5433
5513
|
} })), table && showTableDialog && (_jsx("div", { style: {
|
|
@@ -5488,10 +5568,6 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
5488
5568
|
zIndex: 100,
|
|
5489
5569
|
}, onClick: () => {
|
|
5490
5570
|
setPendingImport(null);
|
|
5491
|
-
if (pdfInputRef.current)
|
|
5492
|
-
pdfInputRef.current.value = "";
|
|
5493
|
-
if (docxInputRef.current)
|
|
5494
|
-
docxInputRef.current.value = "";
|
|
5495
5571
|
}, children: _jsxs("div", { style: {
|
|
5496
5572
|
background: "var(--srte-modal-bg)",
|
|
5497
5573
|
color: "var(--srte-modal-text)",
|
|
@@ -5500,7 +5576,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
5500
5576
|
maxWidth: 400,
|
|
5501
5577
|
width: "90%",
|
|
5502
5578
|
boxShadow: "var(--srte-menu-shadow)",
|
|
5503
|
-
}, 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: () =>
|
|
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: {
|
|
5504
5580
|
padding: "8px 16px",
|
|
5505
5581
|
background: "var(--srte-danger)",
|
|
5506
5582
|
color: "var(--srte-on-primary)",
|
|
@@ -5509,7 +5585,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
5509
5585
|
cursor: "pointer",
|
|
5510
5586
|
fontWeight: 500,
|
|
5511
5587
|
textAlign: "left"
|
|
5512
|
-
}, children: "Replace Check existing content (Overwrite)" }), _jsx("button", { onClick: () =>
|
|
5588
|
+
}, children: "Replace Check existing content (Overwrite)" }), _jsx("button", { onClick: () => void processFormatImport(pendingImport.definition, pendingImport.file, 'append'), style: {
|
|
5513
5589
|
padding: "8px 16px",
|
|
5514
5590
|
background: "var(--srte-primary)",
|
|
5515
5591
|
color: "var(--srte-on-primary)",
|
|
@@ -5520,10 +5596,6 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
5520
5596
|
textAlign: "left"
|
|
5521
5597
|
}, children: "Append to bottom" }), _jsx("button", { onClick: () => {
|
|
5522
5598
|
setPendingImport(null);
|
|
5523
|
-
if (pdfInputRef.current)
|
|
5524
|
-
pdfInputRef.current.value = "";
|
|
5525
|
-
if (docxInputRef.current)
|
|
5526
|
-
docxInputRef.current.value = "";
|
|
5527
5599
|
}, style: {
|
|
5528
5600
|
padding: "8px 16px",
|
|
5529
5601
|
background: "var(--srte-cancel-bg)",
|
|
@@ -5886,6 +5958,14 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
5886
5958
|
if (item && list) {
|
|
5887
5959
|
pushEditorHistory();
|
|
5888
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
|
+
}
|
|
5889
5969
|
item.dataset.checked = checked ? "true" : "false";
|
|
5890
5970
|
t.dataset.checked = checked ? "true" : "false";
|
|
5891
5971
|
t.setAttribute("aria-pressed", checked ? "true" : "false");
|
|
@@ -5957,7 +6037,30 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
5957
6037
|
}
|
|
5958
6038
|
updateActiveState();
|
|
5959
6039
|
}, onKeyDown: (e) => {
|
|
5960
|
-
if (
|
|
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") {
|
|
5961
6064
|
e.preventDefault();
|
|
5962
6065
|
openLinkEditor();
|
|
5963
6066
|
return;
|
|
@@ -6076,6 +6179,31 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
6076
6179
|
window.addEventListener("mouseup", onUp);
|
|
6077
6180
|
}, onContextMenu: (e) => {
|
|
6078
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);
|
|
6079
6207
|
if (target && target.tagName === "IMG") {
|
|
6080
6208
|
e.preventDefault();
|
|
6081
6209
|
const vw = window.innerWidth;
|
|
@@ -6103,55 +6231,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
6103
6231
|
setTableMenu(null);
|
|
6104
6232
|
setImageMenu(null);
|
|
6105
6233
|
}
|
|
6106
|
-
} }),
|
|
6107
|
-
if (dragHandleHideTimerRef.current != null) {
|
|
6108
|
-
window.clearTimeout(dragHandleHideTimerRef.current);
|
|
6109
|
-
dragHandleHideTimerRef.current = null;
|
|
6110
|
-
}
|
|
6111
|
-
}, onMouseLeave: () => {
|
|
6112
|
-
if (!draggedBlockRef.current)
|
|
6113
|
-
scheduleDragHandleHide();
|
|
6114
|
-
}, onMouseDown: (e) => {
|
|
6115
|
-
e.stopPropagation();
|
|
6116
|
-
}, onDragStart: (e) => {
|
|
6117
|
-
draggedBlockRef.current = dragHandle.target;
|
|
6118
|
-
e.dataTransfer.effectAllowed = "move";
|
|
6119
|
-
e.dataTransfer.setData("text/plain", "moving-block");
|
|
6120
|
-
try {
|
|
6121
|
-
const ghost = dragHandle.target.cloneNode(true);
|
|
6122
|
-
ghost.style.position = "fixed";
|
|
6123
|
-
ghost.style.left = "-10000px";
|
|
6124
|
-
ghost.style.top = "-10000px";
|
|
6125
|
-
ghost.style.width = `${Math.min(dragHandle.target.getBoundingClientRect().width, 480)}px`;
|
|
6126
|
-
ghost.style.opacity = "0.75";
|
|
6127
|
-
document.body.appendChild(ghost);
|
|
6128
|
-
e.dataTransfer.setDragImage(ghost, 12, 12);
|
|
6129
|
-
window.setTimeout(() => ghost.remove(), 0);
|
|
6130
|
-
}
|
|
6131
|
-
catch { }
|
|
6132
|
-
}, onDragEnd: () => {
|
|
6133
|
-
draggedBlockRef.current = null;
|
|
6134
|
-
updateDragHandleForTarget(dragHandle.target);
|
|
6135
|
-
}, style: {
|
|
6136
|
-
position: "absolute",
|
|
6137
|
-
left: dragHandle.left,
|
|
6138
|
-
top: dragHandle.top + Math.max(0, (dragHandle.height - 24) / 2),
|
|
6139
|
-
width: 24,
|
|
6140
|
-
height: 24,
|
|
6141
|
-
display: "inline-flex",
|
|
6142
|
-
alignItems: "center",
|
|
6143
|
-
justifyContent: "center",
|
|
6144
|
-
border: "1px solid var(--srte-border)",
|
|
6145
|
-
borderRadius: 6,
|
|
6146
|
-
background: "var(--srte-input-bg)",
|
|
6147
|
-
color: "var(--srte-input-text)",
|
|
6148
|
-
boxShadow: "var(--srte-menu-shadow)",
|
|
6149
|
-
cursor: "grab",
|
|
6150
|
-
zIndex: 8,
|
|
6151
|
-
fontSize: 14,
|
|
6152
|
-
lineHeight: 1,
|
|
6153
|
-
padding: 0,
|
|
6154
|
-
}, children: "\u22EE\u22EE" })), selectedImage && imageOverlay && (_jsxs("div", { style: {
|
|
6234
|
+
} }), selectedImage && imageOverlay && (_jsxs("div", { style: {
|
|
6155
6235
|
position: "absolute",
|
|
6156
6236
|
left: imageOverlay.left,
|
|
6157
6237
|
top: imageOverlay.top,
|
|
@@ -6187,6 +6267,11 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
6187
6267
|
window.removeEventListener("mousemove", onMove);
|
|
6188
6268
|
window.removeEventListener("mouseup", onUp);
|
|
6189
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
|
+
}
|
|
6190
6275
|
handleInput();
|
|
6191
6276
|
};
|
|
6192
6277
|
window.addEventListener("mousemove", onMove);
|
|
@@ -6225,6 +6310,11 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
6225
6310
|
window.removeEventListener("mousemove", onMove);
|
|
6226
6311
|
window.removeEventListener("mouseup", onUp);
|
|
6227
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
|
+
}
|
|
6228
6318
|
handleInput();
|
|
6229
6319
|
};
|
|
6230
6320
|
window.addEventListener("mousemove", onMove);
|
|
@@ -6240,7 +6330,24 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
6240
6330
|
borderRadius: 2,
|
|
6241
6331
|
cursor: "ew-resize",
|
|
6242
6332
|
pointerEvents: "auto",
|
|
6243
|
-
} })] }))] }),
|
|
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 ? () => {
|
|
6244
6351
|
openEditorLink(linkMenu.anchor);
|
|
6245
6352
|
setLinkMenu(null);
|
|
6246
6353
|
} : undefined, onRemove: linkMenu.anchor ? () => removeAnchorLink(linkMenu.anchor) : undefined, onCancel: () => setLinkMenu(null) })), tableMenu && (_jsx("div", { style: {
|
|
@@ -6608,7 +6715,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
6608
6715
|
scheduleImageOverlay();
|
|
6609
6716
|
handleInput();
|
|
6610
6717
|
}, children: "Reset" })] }), _jsxs("div", { style: { display: "flex", gap: 6 }, children: [_jsx("button", { style: { color: "var(--srte-danger)" }, onClick: () => {
|
|
6611
|
-
|
|
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();
|
|
6612
6724
|
setImageMenu(null);
|
|
6613
6725
|
setSelectedImage(null);
|
|
6614
6726
|
setImageOverlay(null);
|