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