smartrte-react 0.2.10 → 0.3.1
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 +15 -0
- package/dist/adapters/coreBold.d.ts +5 -0
- package/dist/adapters/coreBold.js +3 -0
- package/dist/adapters/domSelectionBridge.d.ts +6 -0
- package/dist/adapters/domSelectionBridge.js +185 -0
- package/dist/adapters/domSmartDocument.d.ts +10 -0
- package/dist/adapters/domSmartDocument.js +216 -0
- package/dist/adapters/domTargets.d.ts +3 -0
- package/dist/adapters/domTargets.js +3 -0
- package/dist/adapters/inlineMarkCoreExecution.d.ts +9 -0
- package/dist/adapters/inlineMarkCoreExecution.js +25 -0
- package/dist/adapters/internalFlags.d.ts +13 -0
- package/dist/adapters/internalFlags.js +36 -0
- package/dist/adapters/shadowMode.d.ts +23 -0
- package/dist/adapters/shadowMode.js +33 -0
- package/dist/components/ClassicEditor.js +1505 -350
- package/dist/components/LinkEditorPopover.d.ts +20 -0
- package/dist/components/LinkEditorPopover.js +87 -0
- package/dist/standalone/editor.js +841 -0
- package/dist/theme.d.ts +1 -1
- package/dist/theme.js +61 -0
- package/package.json +12 -7
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef, useState } from "react";
|
|
3
3
|
import { MediaManager } from "./MediaManager.js";
|
|
4
|
+
import { LinkEditorPopover } from "./LinkEditorPopover.js";
|
|
4
5
|
import * as pdfjsLib from 'pdfjs-dist';
|
|
5
6
|
import mammoth from 'mammoth';
|
|
6
7
|
import JSZip from 'jszip';
|
|
8
|
+
import { applyLink, applyTextColor as coreApplyTextColor, markdownToCompatibilityHtml, removeLink, sanitizeLinkHref, toggleBold, toggleItalic, toggleSubscript, toggleSuperscript, toggleUnderline } from 'smartrte-core';
|
|
9
|
+
import { restoreSelectionToDom, selectionFromDom } from '../adapters/domSelectionBridge.js';
|
|
10
|
+
import { serializeSmartDocument, smartDocumentFromEditorRoot } from '../adapters/domSmartDocument.js';
|
|
11
|
+
import { isShadowModeEnabled, runShadowCommand } from '../adapters/shadowMode.js';
|
|
12
|
+
import { getCoreInlineMarkResult, isCoreInlineMarkEnabled } from '../adapters/inlineMarkCoreExecution.js';
|
|
13
|
+
import { closestFromTarget, isNode } from '../adapters/domTargets.js';
|
|
7
14
|
import { ensureStyleSheet } from '../theme.js';
|
|
8
15
|
// Initialize PDF.js worker
|
|
9
16
|
if (typeof window !== 'undefined') {
|
|
@@ -50,11 +57,13 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
50
57
|
const selectionRef = useRef(null);
|
|
51
58
|
const selectingRef = useRef(null);
|
|
52
59
|
const [imageMenu, setImageMenu] = useState(null);
|
|
60
|
+
const [linkMenu, setLinkMenu] = useState(null);
|
|
53
61
|
const [showMediaManager, setShowMediaManager] = useState(false);
|
|
54
62
|
const [showColorPicker, setShowColorPicker] = useState(false);
|
|
55
63
|
const [showSpecialChars, setShowSpecialChars] = useState(false);
|
|
56
64
|
const [colorPickerType, setColorPickerType] = useState('text');
|
|
57
65
|
const savedRangeRef = useRef(null);
|
|
66
|
+
const pendingFontSizeRef = useRef(null);
|
|
58
67
|
const inlineScriptCaretOverrideRef = useRef(null);
|
|
59
68
|
const historyRef = useRef({
|
|
60
69
|
undo: [],
|
|
@@ -64,6 +73,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
64
73
|
const [currentFontSize, setCurrentFontSize] = useState("");
|
|
65
74
|
const [currentFont, setCurrentFont] = useState("");
|
|
66
75
|
const [currentBlockType, setCurrentBlockType] = useState("p");
|
|
76
|
+
const [currentAlignment, setCurrentAlignment] = useState("left");
|
|
67
77
|
const [activeState, setActiveState] = useState({
|
|
68
78
|
bold: false,
|
|
69
79
|
italic: false,
|
|
@@ -71,10 +81,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
71
81
|
strikeThrough: false,
|
|
72
82
|
subscript: false,
|
|
73
83
|
superscript: false,
|
|
84
|
+
checklist: false,
|
|
74
85
|
unorderedList: false,
|
|
75
86
|
orderedList: false,
|
|
76
87
|
blockquote: false,
|
|
77
88
|
codeBlock: false,
|
|
89
|
+
link: false,
|
|
78
90
|
});
|
|
79
91
|
useEffect(() => {
|
|
80
92
|
const el = editableRef.current;
|
|
@@ -84,9 +96,17 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
84
96
|
if (typeof value === "string" && value !== el.innerHTML) {
|
|
85
97
|
el.innerHTML = value || "";
|
|
86
98
|
fixNegativeMargins(el);
|
|
99
|
+
normalizeInvalidQuoteNesting(el);
|
|
100
|
+
normalizeInvalidCodeBlockNesting(el);
|
|
101
|
+
normalizeInvalidTableNesting(el);
|
|
87
102
|
ensureTableWrappers(el);
|
|
103
|
+
ensureCaretBoundaryParagraphs(el);
|
|
88
104
|
addTableResizeHandles();
|
|
89
105
|
}
|
|
106
|
+
normalizeInvalidQuoteNesting(el);
|
|
107
|
+
normalizeInvalidCodeBlockNesting(el);
|
|
108
|
+
normalizeInvalidTableNesting(el);
|
|
109
|
+
ensureCaretBoundaryParagraphs(el);
|
|
90
110
|
// Suppress native context menu inside table cells at capture phase
|
|
91
111
|
const onCtx = (evt) => {
|
|
92
112
|
const target = evt.target;
|
|
@@ -100,6 +120,83 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
100
120
|
el.removeEventListener("contextmenu", onCtx, { capture: true });
|
|
101
121
|
};
|
|
102
122
|
}, [value]);
|
|
123
|
+
const parseFontSizePx = (value) => {
|
|
124
|
+
const match = /^([\d.]+)(px|pt)?$/i.exec(value.trim());
|
|
125
|
+
if (!match)
|
|
126
|
+
return null;
|
|
127
|
+
const numeric = Number(match[1]);
|
|
128
|
+
if (!Number.isFinite(numeric) || numeric <= 0)
|
|
129
|
+
return null;
|
|
130
|
+
return match[2]?.toLowerCase() === "pt" ? numeric * 4 / 3 : numeric;
|
|
131
|
+
};
|
|
132
|
+
const explicitFontSizeAt = (node) => {
|
|
133
|
+
let element = node instanceof HTMLElement ? node : node.parentElement;
|
|
134
|
+
while (element && element !== editableRef.current) {
|
|
135
|
+
const parsed = parseFontSizePx(element.style.fontSize);
|
|
136
|
+
if (parsed)
|
|
137
|
+
return parsed;
|
|
138
|
+
element = element.parentElement;
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
};
|
|
142
|
+
const normalizeFontSizeSpans = (root) => {
|
|
143
|
+
Array.from(root.querySelectorAll('span[style*="font-size"]')).reverse().forEach((span) => {
|
|
144
|
+
const parent = span.parentElement;
|
|
145
|
+
if (parent?.tagName === "SPAN" &&
|
|
146
|
+
parseFontSizePx(parent.style.fontSize) === parseFontSizePx(span.style.fontSize) &&
|
|
147
|
+
span.attributes.length === 1) {
|
|
148
|
+
while (span.firstChild)
|
|
149
|
+
parent.insertBefore(span.firstChild, span);
|
|
150
|
+
span.remove();
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
Array.from(root.querySelectorAll('span[style*="font-size"]')).forEach((span) => {
|
|
154
|
+
let next = span.nextElementSibling;
|
|
155
|
+
while (next?.tagName === "SPAN" &&
|
|
156
|
+
next.getAttribute("style") === span.getAttribute("style") &&
|
|
157
|
+
next.attributes.length === span.attributes.length) {
|
|
158
|
+
while (next.firstChild)
|
|
159
|
+
span.appendChild(next.firstChild);
|
|
160
|
+
const following = next.nextElementSibling;
|
|
161
|
+
next.remove();
|
|
162
|
+
next = following;
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
const resolveFontSizeForRange = (range) => {
|
|
167
|
+
const pending = pendingFontSizeRef.current;
|
|
168
|
+
if (range.collapsed && pending &&
|
|
169
|
+
range.startContainer === pending.container &&
|
|
170
|
+
range.startOffset === pending.offset)
|
|
171
|
+
return String(Math.round(pending.valuePx));
|
|
172
|
+
if (range.collapsed) {
|
|
173
|
+
const explicit = explicitFontSizeAt(range.startContainer);
|
|
174
|
+
return explicit ? String(Math.round(explicit)) : "";
|
|
175
|
+
}
|
|
176
|
+
const editor = editableRef.current;
|
|
177
|
+
if (!editor)
|
|
178
|
+
return "";
|
|
179
|
+
const sizes = new Set();
|
|
180
|
+
const walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT);
|
|
181
|
+
let node = walker.nextNode();
|
|
182
|
+
while (node) {
|
|
183
|
+
try {
|
|
184
|
+
if (range.intersectsNode(node) && node.textContent) {
|
|
185
|
+
const size = explicitFontSizeAt(node);
|
|
186
|
+
if (size)
|
|
187
|
+
sizes.add(Math.round(size));
|
|
188
|
+
else
|
|
189
|
+
sizes.add(0);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch { }
|
|
193
|
+
if (sizes.size > 1)
|
|
194
|
+
return "";
|
|
195
|
+
node = walker.nextNode();
|
|
196
|
+
}
|
|
197
|
+
const only = Array.from(sizes)[0];
|
|
198
|
+
return only ? String(only) : "";
|
|
199
|
+
};
|
|
103
200
|
const updateActiveState = () => {
|
|
104
201
|
const editor = editableRef.current;
|
|
105
202
|
if (!editor)
|
|
@@ -116,6 +213,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
116
213
|
const element = node instanceof HTMLElement ? node : null;
|
|
117
214
|
const block = element?.closest("p,h1,h2,h3,h4,h5,h6,li,blockquote,pre,div");
|
|
118
215
|
const tag = block?.tagName.toLowerCase();
|
|
216
|
+
const queryState = (command) => typeof document.queryCommandState === "function" && document.queryCommandState(command);
|
|
119
217
|
const scriptOverride = inlineScriptCaretOverrideRef.current;
|
|
120
218
|
const overrideApplies = Boolean(scriptOverride &&
|
|
121
219
|
range.collapsed &&
|
|
@@ -124,24 +222,46 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
124
222
|
if (scriptOverride && !overrideApplies) {
|
|
125
223
|
inlineScriptCaretOverrideRef.current = null;
|
|
126
224
|
}
|
|
127
|
-
const subscriptActive =
|
|
128
|
-
const superscriptActive =
|
|
129
|
-
|
|
225
|
+
const subscriptActive = queryState("subscript") || Boolean(element?.closest("sub"));
|
|
226
|
+
const superscriptActive = queryState("superscript") || Boolean(element?.closest("sup"));
|
|
227
|
+
if (!range.collapsed) {
|
|
228
|
+
const formattingBlocks = getSelectedBlocks(range);
|
|
229
|
+
[range.startContainer, range.endContainer].forEach((endpoint) => {
|
|
230
|
+
const endpointElement = endpoint instanceof HTMLElement ? endpoint : endpoint.parentElement;
|
|
231
|
+
const endpointBlock = endpointElement?.closest("p,h1,h2,h3,h4,h5,h6,li");
|
|
232
|
+
if (endpointBlock && editor.contains(endpointBlock) && !formattingBlocks.includes(endpointBlock)) {
|
|
233
|
+
formattingBlocks.push(endpointBlock);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
const types = new Set(formattingBlocks.map((selectedBlock) => {
|
|
237
|
+
const contentBlock = selectedBlock.tagName === "LI"
|
|
238
|
+
? selectedBlock.querySelector(":scope > p,:scope > h1,:scope > h2,:scope > h3,:scope > h4,:scope > h5,:scope > h6")
|
|
239
|
+
: selectedBlock;
|
|
240
|
+
const selectedTag = contentBlock?.tagName.toLowerCase() || "p";
|
|
241
|
+
return /^h[1-6]$/.test(selectedTag) ? selectedTag : "p";
|
|
242
|
+
}));
|
|
243
|
+
setCurrentBlockType(types.size > 1 ? "mixed" : (Array.from(types)[0] || "p"));
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
setCurrentBlockType(/^h[1-6]$/.test(tag || "") ? tag : "p");
|
|
247
|
+
}
|
|
248
|
+
setCurrentFontSize(resolveFontSizeForRange(range));
|
|
249
|
+
const alignmentTargets = getAlignmentTargets(range);
|
|
250
|
+
const alignments = new Set(alignmentTargets.map(readTextAlignment));
|
|
251
|
+
setCurrentAlignment(alignments.size > 1 ? "mixed" : Array.from(alignments)[0] || "left");
|
|
130
252
|
setActiveState({
|
|
131
|
-
bold:
|
|
132
|
-
italic:
|
|
133
|
-
underline:
|
|
134
|
-
strikeThrough:
|
|
135
|
-
subscript: overrideApplies
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
? false
|
|
140
|
-
: superscriptActive,
|
|
141
|
-
unorderedList: Boolean(element?.closest("ul")),
|
|
253
|
+
bold: queryState("bold"),
|
|
254
|
+
italic: queryState("italic"),
|
|
255
|
+
underline: queryState("underline"),
|
|
256
|
+
strikeThrough: queryState("strikeThrough"),
|
|
257
|
+
subscript: overrideApplies ? scriptOverride?.command === "subscript" : subscriptActive,
|
|
258
|
+
superscript: overrideApplies ? scriptOverride?.command === "superscript" : superscriptActive,
|
|
259
|
+
checklist: Boolean(element?.closest('[data-srte-checklist="true"]')),
|
|
260
|
+
unorderedList: Boolean(element?.closest("ul:not([data-srte-checklist=\"true\"])")),
|
|
142
261
|
orderedList: Boolean(element?.closest("ol")),
|
|
143
262
|
blockquote: Boolean(element?.closest("blockquote")),
|
|
144
263
|
codeBlock: Boolean(element?.closest("pre")),
|
|
264
|
+
link: Boolean(element?.closest("a")),
|
|
145
265
|
});
|
|
146
266
|
}
|
|
147
267
|
catch { }
|
|
@@ -154,6 +274,10 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
154
274
|
const range = sel.getRangeAt(0);
|
|
155
275
|
const editor = editableRef.current;
|
|
156
276
|
if (editor && editor.contains(range.commonAncestorContainer)) {
|
|
277
|
+
const pending = pendingFontSizeRef.current;
|
|
278
|
+
if (pending &&
|
|
279
|
+
(!range.collapsed || range.startContainer !== pending.container || range.startOffset !== pending.offset))
|
|
280
|
+
pendingFontSizeRef.current = null;
|
|
157
281
|
savedRangeRef.current = range.cloneRange();
|
|
158
282
|
updateActiveState();
|
|
159
283
|
}
|
|
@@ -182,6 +306,52 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
182
306
|
return;
|
|
183
307
|
}
|
|
184
308
|
pushEditorHistory();
|
|
309
|
+
const editor = editableRef.current;
|
|
310
|
+
const coreMark = { bold: "bold", italic: "italic", underline: "underline", superscript: "superscript", subscript: "subscript" }[command];
|
|
311
|
+
if (coreMark && editor && isCoreInlineMarkEnabled(coreMark)) {
|
|
312
|
+
try {
|
|
313
|
+
const result = getCoreInlineMarkResult(editor, coreMark);
|
|
314
|
+
if (result) {
|
|
315
|
+
editor.innerHTML = result.html;
|
|
316
|
+
ensureTableWrappers(editor);
|
|
317
|
+
addTableResizeHandles();
|
|
318
|
+
if (!restoreSelectionToDom(editor, result.selectionAfter)) {
|
|
319
|
+
const fallback = document.createRange();
|
|
320
|
+
fallback.selectNodeContents(editor);
|
|
321
|
+
fallback.collapse(false);
|
|
322
|
+
safeSelectRange(fallback);
|
|
323
|
+
if (isShadowModeEnabled())
|
|
324
|
+
console.warn(`[Smart RTE] Core ${coreMark} could not restore its exact selection.`);
|
|
325
|
+
}
|
|
326
|
+
handleInput();
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
if (isShadowModeEnabled())
|
|
332
|
+
console.warn(`[Smart RTE] Core ${coreMark} fell back to legacy execution.`, error);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
const shadowCommand = {
|
|
336
|
+
bold: toggleBold,
|
|
337
|
+
italic: toggleItalic,
|
|
338
|
+
underline: toggleUnderline,
|
|
339
|
+
superscript: toggleSuperscript,
|
|
340
|
+
subscript: toggleSubscript,
|
|
341
|
+
foreColor: coreApplyTextColor,
|
|
342
|
+
createLink: applyLink,
|
|
343
|
+
unlink: removeLink,
|
|
344
|
+
}[command];
|
|
345
|
+
const shadowInput = command === "createLink" ? { href: valueArg || "" } : valueArg;
|
|
346
|
+
const shadowState = shadowCommand && editor && isShadowModeEnabled()
|
|
347
|
+
? (() => {
|
|
348
|
+
const selection = selectionFromDom(editor, window.getSelection());
|
|
349
|
+
if (!selection)
|
|
350
|
+
return null;
|
|
351
|
+
const { document: coreDocument } = smartDocumentFromEditorRoot(editor);
|
|
352
|
+
return { document: coreDocument, selection };
|
|
353
|
+
})()
|
|
354
|
+
: null;
|
|
185
355
|
const beforeHtml = editableRef.current?.innerHTML || "";
|
|
186
356
|
const ok = document.execCommand(command, false, valueArg);
|
|
187
357
|
const afterHtml = editableRef.current?.innerHTML || "";
|
|
@@ -191,6 +361,16 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
191
361
|
if (command === "formatBlock" && valueArg)
|
|
192
362
|
applyFormatBlockFallback(valueArg);
|
|
193
363
|
}
|
|
364
|
+
if (shadowCommand && shadowState) {
|
|
365
|
+
runShadowCommand({
|
|
366
|
+
command: shadowCommand,
|
|
367
|
+
context: { document: shadowState.document, selection: shadowState.selection },
|
|
368
|
+
input: shadowInput,
|
|
369
|
+
state: shadowState,
|
|
370
|
+
legacyHtml: afterHtml,
|
|
371
|
+
serialize: (state) => serializeSmartDocument(state.document),
|
|
372
|
+
});
|
|
373
|
+
}
|
|
194
374
|
handleInput();
|
|
195
375
|
}
|
|
196
376
|
catch { }
|
|
@@ -214,70 +394,136 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
214
394
|
const range = getSelectionRangeInEditor();
|
|
215
395
|
if (!editor || !range)
|
|
216
396
|
return;
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
? {
|
|
227
|
-
command,
|
|
228
|
-
container: currentRange.startContainer,
|
|
229
|
-
offset: currentRange.startOffset,
|
|
230
|
-
}
|
|
231
|
-
: null;
|
|
232
|
-
setActiveState((current) => ({
|
|
233
|
-
...current,
|
|
234
|
-
subscript: command === "subscript" ? false : current.subscript,
|
|
235
|
-
superscript: command === "superscript" ? false : current.superscript,
|
|
236
|
-
}));
|
|
237
|
-
handleInput();
|
|
238
|
-
requestAnimationFrame(updateActiveState);
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
if (script) {
|
|
242
|
-
const nextRange = document.createRange();
|
|
243
|
-
nextRange.setStartAfter(script);
|
|
244
|
-
nextRange.collapse(true);
|
|
245
|
-
safeSelectRange(nextRange);
|
|
246
|
-
savedRangeRef.current = nextRange.cloneRange();
|
|
397
|
+
if (range.collapsed) {
|
|
398
|
+
const tagName = command === "subscript" ? "sub" : "sup";
|
|
399
|
+
const pending = inlineScriptCaretOverrideRef.current;
|
|
400
|
+
const pendingApplies = pending &&
|
|
401
|
+
pending.container === range.startContainer && pending.offset === range.startOffset;
|
|
402
|
+
const active = pendingApplies
|
|
403
|
+
? pending.command === command
|
|
404
|
+
: Boolean(getScriptAncestor(range.startContainer, tagName));
|
|
405
|
+
const nextCommand = active ? "normal" : command;
|
|
247
406
|
inlineScriptCaretOverrideRef.current = {
|
|
248
|
-
command,
|
|
249
|
-
container:
|
|
250
|
-
offset:
|
|
407
|
+
command: nextCommand,
|
|
408
|
+
container: range.startContainer,
|
|
409
|
+
offset: range.startOffset,
|
|
251
410
|
};
|
|
252
411
|
setActiveState((current) => ({
|
|
253
412
|
...current,
|
|
254
|
-
subscript:
|
|
255
|
-
superscript:
|
|
413
|
+
subscript: nextCommand === "subscript",
|
|
414
|
+
superscript: nextCommand === "superscript",
|
|
256
415
|
}));
|
|
257
|
-
|
|
416
|
+
savedRangeRef.current = range.cloneRange();
|
|
258
417
|
return;
|
|
259
418
|
}
|
|
260
419
|
inlineScriptCaretOverrideRef.current = null;
|
|
261
|
-
|
|
420
|
+
pushEditorHistory();
|
|
421
|
+
const result = getCoreInlineMarkResult(editor, command);
|
|
422
|
+
if (result) {
|
|
423
|
+
editor.innerHTML = result.html;
|
|
424
|
+
ensureTableWrappers(editor);
|
|
425
|
+
addTableResizeHandles();
|
|
426
|
+
restoreSelectionToDom(editor, result.selectionAfter);
|
|
427
|
+
handleInput();
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
exec(command);
|
|
431
|
+
}
|
|
262
432
|
requestAnimationFrame(updateActiveState);
|
|
263
433
|
}
|
|
264
434
|
catch { }
|
|
265
435
|
};
|
|
436
|
+
useEffect(() => {
|
|
437
|
+
const editor = editableRef.current;
|
|
438
|
+
if (!editor)
|
|
439
|
+
return;
|
|
440
|
+
const splitScriptAtRange = (script, range) => {
|
|
441
|
+
const rightRange = document.createRange();
|
|
442
|
+
rightRange.selectNodeContents(script);
|
|
443
|
+
rightRange.setStart(range.startContainer, range.startOffset);
|
|
444
|
+
const rightContent = rightRange.extractContents();
|
|
445
|
+
const rightScript = script.cloneNode(false);
|
|
446
|
+
rightScript.appendChild(rightContent);
|
|
447
|
+
script.parentNode?.insertBefore(rightScript, script.nextSibling);
|
|
448
|
+
const insertion = document.createRange();
|
|
449
|
+
insertion.setStartAfter(script);
|
|
450
|
+
insertion.collapse(true);
|
|
451
|
+
if (!script.textContent)
|
|
452
|
+
script.remove();
|
|
453
|
+
if (!rightScript.textContent)
|
|
454
|
+
rightScript.remove();
|
|
455
|
+
return insertion;
|
|
456
|
+
};
|
|
457
|
+
const applyPendingInlineScript = (event) => {
|
|
458
|
+
const pending = inlineScriptCaretOverrideRef.current;
|
|
459
|
+
if (!pending || event.inputType !== "insertText" || !event.data)
|
|
460
|
+
return;
|
|
461
|
+
const range = getSelectionRangeInEditor();
|
|
462
|
+
if (!range?.collapsed || range.startContainer !== pending.container || range.startOffset !== pending.offset)
|
|
463
|
+
return;
|
|
464
|
+
event.preventDefault();
|
|
465
|
+
event.stopImmediatePropagation();
|
|
466
|
+
pushEditorHistory();
|
|
467
|
+
const currentScript = getScriptAncestor(range.startContainer, "sub") ||
|
|
468
|
+
getScriptAncestor(range.startContainer, "sup");
|
|
469
|
+
const desiredTag = pending.command === "normal"
|
|
470
|
+
? null
|
|
471
|
+
: pending.command === "subscript" ? "sub" : "sup";
|
|
472
|
+
let insertion = range;
|
|
473
|
+
if (currentScript && currentScript.tagName.toLowerCase() !== desiredTag) {
|
|
474
|
+
insertion = splitScriptAtRange(currentScript, range);
|
|
475
|
+
}
|
|
476
|
+
const text = document.createTextNode(event.data);
|
|
477
|
+
let inserted = text;
|
|
478
|
+
if (desiredTag && currentScript?.tagName.toLowerCase() !== desiredTag) {
|
|
479
|
+
const script = document.createElement(desiredTag);
|
|
480
|
+
script.appendChild(text);
|
|
481
|
+
inserted = script;
|
|
482
|
+
}
|
|
483
|
+
const pendingSize = pendingFontSizeRef.current;
|
|
484
|
+
if (pendingSize) {
|
|
485
|
+
const span = document.createElement("span");
|
|
486
|
+
span.style.fontSize = `${pendingSize.valuePx}px`;
|
|
487
|
+
span.appendChild(inserted);
|
|
488
|
+
inserted = span;
|
|
489
|
+
}
|
|
490
|
+
insertion.insertNode(inserted);
|
|
491
|
+
const nextRange = document.createRange();
|
|
492
|
+
nextRange.setStartAfter(text);
|
|
493
|
+
nextRange.collapse(true);
|
|
494
|
+
safeSelectRange(nextRange);
|
|
495
|
+
inlineScriptCaretOverrideRef.current = {
|
|
496
|
+
command: pending.command,
|
|
497
|
+
container: nextRange.startContainer,
|
|
498
|
+
offset: nextRange.startOffset,
|
|
499
|
+
};
|
|
500
|
+
if (pendingSize)
|
|
501
|
+
pendingFontSizeRef.current = {
|
|
502
|
+
valuePx: pendingSize.valuePx,
|
|
503
|
+
container: nextRange.startContainer,
|
|
504
|
+
offset: nextRange.startOffset,
|
|
505
|
+
};
|
|
506
|
+
savedRangeRef.current = nextRange.cloneRange();
|
|
507
|
+
handleInput();
|
|
508
|
+
};
|
|
509
|
+
editor.addEventListener("beforeinput", applyPendingInlineScript);
|
|
510
|
+
return () => editor.removeEventListener("beforeinput", applyPendingInlineScript);
|
|
511
|
+
}, []);
|
|
266
512
|
const applyFormatBlock = (blockName) => {
|
|
267
513
|
try {
|
|
268
514
|
if (!restoreSavedSelection()) {
|
|
269
515
|
safeSelectRange(getSelectionRangeInEditor());
|
|
270
516
|
}
|
|
517
|
+
pushEditorHistory();
|
|
271
518
|
if (applyFormatBlockFallback(blockName)) {
|
|
272
519
|
const tag = normalizeBlockTag(blockName);
|
|
273
|
-
if (tag === "p" || tag
|
|
520
|
+
if (tag === "p" || /^h[1-6]$/.test(tag || "")) {
|
|
274
521
|
setCurrentBlockType(tag);
|
|
275
522
|
}
|
|
276
523
|
handleInput();
|
|
277
524
|
requestAnimationFrame(updateActiveState);
|
|
278
525
|
return;
|
|
279
526
|
}
|
|
280
|
-
exec("formatBlock", blockName);
|
|
281
527
|
}
|
|
282
528
|
catch { }
|
|
283
529
|
};
|
|
@@ -349,7 +595,9 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
349
595
|
to.push(currentHtml);
|
|
350
596
|
editor.innerHTML = html;
|
|
351
597
|
fixNegativeMargins(editor);
|
|
598
|
+
normalizeInvalidCodeBlockNesting(editor);
|
|
352
599
|
ensureTableWrappers(editor);
|
|
600
|
+
ensureCaretBoundaryParagraphs(editor);
|
|
353
601
|
addTableResizeHandles();
|
|
354
602
|
clearSelectionDecor();
|
|
355
603
|
setTableMenu(null);
|
|
@@ -398,18 +646,111 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
398
646
|
savedRangeRef.current = range.cloneRange();
|
|
399
647
|
}
|
|
400
648
|
};
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
649
|
+
const escapeAttribute = (value) => value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
650
|
+
const getSelectedAnchor = () => {
|
|
651
|
+
const editor = editableRef.current;
|
|
652
|
+
const range = getSelectionRangeInEditor();
|
|
653
|
+
if (!editor || !range)
|
|
654
|
+
return null;
|
|
655
|
+
const node = range.startContainer.nodeType === Node.TEXT_NODE ? range.startContainer.parentElement : range.startContainer;
|
|
656
|
+
const anchor = node?.closest?.("a");
|
|
657
|
+
return anchor && editor.contains(anchor) ? anchor : null;
|
|
658
|
+
};
|
|
659
|
+
const selectAnchorContents = (anchor) => {
|
|
660
|
+
const range = document.createRange();
|
|
661
|
+
range.selectNodeContents(anchor);
|
|
662
|
+
safeSelectRange(range);
|
|
663
|
+
savedRangeRef.current = range.cloneRange();
|
|
664
|
+
};
|
|
665
|
+
const getRangeRect = (range) => {
|
|
666
|
+
if (!range || typeof range.getBoundingClientRect !== "function")
|
|
667
|
+
return null;
|
|
668
|
+
const rect = range.getBoundingClientRect();
|
|
669
|
+
return rect.width || rect.height ? rect : null;
|
|
670
|
+
};
|
|
671
|
+
const openLinkEditor = (existingAnchor) => {
|
|
672
|
+
const editor = editableRef.current;
|
|
673
|
+
if (!editor)
|
|
674
|
+
return;
|
|
675
|
+
const anchor = existingAnchor || getSelectedAnchor();
|
|
676
|
+
const range = anchor
|
|
677
|
+
? (() => {
|
|
678
|
+
const anchorRange = document.createRange();
|
|
679
|
+
anchorRange.selectNodeContents(anchor);
|
|
680
|
+
return anchorRange;
|
|
681
|
+
})()
|
|
682
|
+
: getSelectionRangeInEditor();
|
|
683
|
+
const rect = anchor?.getBoundingClientRect() || getRangeRect(range) || editor.getBoundingClientRect();
|
|
684
|
+
setLinkMenu({
|
|
685
|
+
x: Math.max(8, Math.min(rect.left, window.innerWidth - 320)),
|
|
686
|
+
y: Math.max(8, Math.min(rect.bottom + 8, window.innerHeight - 180)),
|
|
687
|
+
anchor: anchor || undefined,
|
|
688
|
+
range: range ? range.cloneRange() : null,
|
|
689
|
+
initialHref: anchor?.getAttribute("href") || "",
|
|
690
|
+
initialText: anchor?.textContent || (range && !range.collapsed ? range.toString() : ""),
|
|
691
|
+
showTextInput: Boolean(anchor || !range || range.collapsed),
|
|
692
|
+
});
|
|
693
|
+
};
|
|
694
|
+
const applyLinkEditorValue = (value) => {
|
|
695
|
+
const state = linkMenu;
|
|
696
|
+
if (!state)
|
|
697
|
+
return;
|
|
698
|
+
setLinkMenu(null);
|
|
699
|
+
if (state.anchor) {
|
|
700
|
+
pushEditorHistory();
|
|
701
|
+
state.anchor.setAttribute("href", value.href);
|
|
702
|
+
updateAnchorTarget(state.anchor, value.openInNewTab);
|
|
703
|
+
if (value.text != null && value.text !== state.anchor.textContent) {
|
|
704
|
+
state.anchor.textContent = value.text;
|
|
705
|
+
}
|
|
706
|
+
selectAnchorContents(state.anchor);
|
|
707
|
+
handleInput();
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
if (state.range)
|
|
711
|
+
safeSelectRange(state.range.cloneRange());
|
|
712
|
+
if (state.range && !state.range.collapsed) {
|
|
713
|
+
exec("createLink", value.href);
|
|
714
|
+
const createdAnchor = getSelectedAnchor();
|
|
715
|
+
if (createdAnchor) {
|
|
716
|
+
updateAnchorTarget(createdAnchor, value.openInNewTab);
|
|
717
|
+
handleInput();
|
|
718
|
+
}
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
if (!value.text)
|
|
722
|
+
return;
|
|
723
|
+
pushEditorHistory();
|
|
724
|
+
const targetAttributes = value.openInNewTab ? ' target="_blank" rel="noopener noreferrer"' : "";
|
|
725
|
+
document.execCommand("insertHTML", false, `<a href="${escapeAttribute(value.href)}"${targetAttributes}>${escapeAttribute(value.text)}</a>`);
|
|
726
|
+
handleInput();
|
|
727
|
+
};
|
|
728
|
+
const updateAnchorTarget = (anchor, openInNewTab) => {
|
|
729
|
+
const otherRelValues = (anchor.getAttribute("rel") || "")
|
|
730
|
+
.split(/\s+/)
|
|
731
|
+
.filter((value) => value && value !== "noopener" && value !== "noreferrer");
|
|
732
|
+
if (openInNewTab) {
|
|
733
|
+
anchor.target = "_blank";
|
|
734
|
+
anchor.rel = [...otherRelValues, "noopener", "noreferrer"].join(" ");
|
|
404
735
|
return;
|
|
405
|
-
|
|
736
|
+
}
|
|
737
|
+
anchor.removeAttribute("target");
|
|
738
|
+
if (otherRelValues.length)
|
|
739
|
+
anchor.rel = otherRelValues.join(" ");
|
|
740
|
+
else
|
|
741
|
+
anchor.removeAttribute("rel");
|
|
742
|
+
};
|
|
743
|
+
const removeAnchorLink = (anchor) => {
|
|
744
|
+
selectAnchorContents(anchor);
|
|
745
|
+
exec("unlink");
|
|
746
|
+
setLinkMenu(null);
|
|
406
747
|
};
|
|
407
748
|
const openEditorLink = (anchor) => {
|
|
408
|
-
const
|
|
409
|
-
if (!
|
|
749
|
+
const safeHref = sanitizeLinkHref(anchor.getAttribute("href"));
|
|
750
|
+
if (!safeHref)
|
|
410
751
|
return;
|
|
411
752
|
try {
|
|
412
|
-
const url = new URL(
|
|
753
|
+
const url = new URL(safeHref, window.location.href);
|
|
413
754
|
if (!["http:", "https:", "mailto:", "tel:"].includes(url.protocol))
|
|
414
755
|
return;
|
|
415
756
|
const target = anchor.getAttribute("target") || "_blank";
|
|
@@ -442,12 +783,22 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
442
783
|
const range = getSelectionRangeInEditor();
|
|
443
784
|
if (!editor || !range)
|
|
444
785
|
return null;
|
|
445
|
-
let node = range.
|
|
786
|
+
let node = range.startContainer;
|
|
446
787
|
if (node.nodeType === Node.TEXT_NODE)
|
|
447
788
|
node = node.parentNode;
|
|
448
789
|
const element = node instanceof HTMLElement ? node : null;
|
|
790
|
+
const cell = element?.closest("td,th");
|
|
791
|
+
if (cell && editor.contains(cell)) {
|
|
792
|
+
const cellBlock = element?.closest("p,h1,h2,h3,h4,h5,h6,blockquote,pre");
|
|
793
|
+
if (cellBlock && cell.contains(cellBlock))
|
|
794
|
+
return cellBlock;
|
|
795
|
+
const directBlock = Array.from(cell.children).find((child) => child.matches("p,h1,h2,h3,h4,h5,h6,blockquote,pre"));
|
|
796
|
+
if (directBlock instanceof HTMLElement)
|
|
797
|
+
return directBlock;
|
|
798
|
+
return cell;
|
|
799
|
+
}
|
|
449
800
|
const block = element?.closest("p,h1,h2,h3,h4,h5,h6,li,blockquote,pre,div");
|
|
450
|
-
if (!block || block === editor || !editor.contains(block))
|
|
801
|
+
if (!block || block === editor || block.getAttribute("data-table-wrapper") === "true" || !editor.contains(block))
|
|
451
802
|
return null;
|
|
452
803
|
return block;
|
|
453
804
|
};
|
|
@@ -456,22 +807,43 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
456
807
|
const editor = editableRef.current;
|
|
457
808
|
if (!editor)
|
|
458
809
|
return [];
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
810
|
+
if (range.collapsed) {
|
|
811
|
+
const current = getCurrentBlock();
|
|
812
|
+
return current ? [current] : [];
|
|
813
|
+
}
|
|
814
|
+
const startElement = range.startContainer.nodeType === Node.TEXT_NODE ? range.startContainer.parentElement : range.startContainer;
|
|
815
|
+
const endElement = range.endContainer.nodeType === Node.TEXT_NODE ? range.endContainer.parentElement : range.endContainer;
|
|
816
|
+
const startCell = startElement?.closest?.("td,th");
|
|
817
|
+
const endCell = endElement?.closest?.("td,th");
|
|
818
|
+
const scope = startCell && startCell === endCell && editor.contains(startCell) ? startCell : editor;
|
|
819
|
+
const blocks = Array.from(scope.querySelectorAll(blockSelector)).filter((block) => {
|
|
820
|
+
if (block === editor || block.getAttribute("data-table-wrapper") === "true")
|
|
462
821
|
return false;
|
|
463
822
|
const parentBlock = block.parentElement?.closest(blockSelector);
|
|
464
|
-
if (parentBlock && parentBlock
|
|
823
|
+
if (parentBlock && scope.contains(parentBlock) && parentBlock !== scope)
|
|
465
824
|
return false;
|
|
466
825
|
try {
|
|
467
|
-
|
|
826
|
+
if (!range.intersectsNode(block))
|
|
827
|
+
return false;
|
|
828
|
+
const parent = block.parentNode;
|
|
829
|
+
if (parent === range.endContainer) {
|
|
830
|
+
const index = Array.prototype.indexOf.call(parent.childNodes, block);
|
|
831
|
+
if (range.endOffset <= index)
|
|
832
|
+
return false;
|
|
833
|
+
}
|
|
834
|
+
if (parent === range.startContainer) {
|
|
835
|
+
const index = Array.prototype.indexOf.call(parent.childNodes, block);
|
|
836
|
+
if (range.startOffset > index)
|
|
837
|
+
return false;
|
|
838
|
+
}
|
|
839
|
+
return true;
|
|
468
840
|
}
|
|
469
841
|
catch {
|
|
470
842
|
return false;
|
|
471
843
|
}
|
|
472
844
|
});
|
|
473
845
|
if (blocks.length > 0)
|
|
474
|
-
return blocks;
|
|
846
|
+
return sortInDocumentOrder(blocks);
|
|
475
847
|
const current = getCurrentBlock();
|
|
476
848
|
return current ? [current] : [];
|
|
477
849
|
};
|
|
@@ -489,6 +861,67 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
489
861
|
});
|
|
490
862
|
return items;
|
|
491
863
|
};
|
|
864
|
+
const getAlignmentTarget = (block) => {
|
|
865
|
+
const item = block.tagName === "LI" ? block : block.closest("li");
|
|
866
|
+
return item instanceof HTMLElement ? item : block;
|
|
867
|
+
};
|
|
868
|
+
const getAlignmentTargets = (range) => {
|
|
869
|
+
const editor = editableRef.current;
|
|
870
|
+
if (!editor)
|
|
871
|
+
return [];
|
|
872
|
+
const tableSelection = selectionRef.current;
|
|
873
|
+
const rangeElement = range.startContainer instanceof HTMLElement ? range.startContainer : range.startContainer.parentElement;
|
|
874
|
+
const rangeCell = rangeElement?.closest("td,th");
|
|
875
|
+
const selectedCells = tableSelection && rangeCell && isCellInsideSelection(rangeCell)
|
|
876
|
+
? getCellsInGridRect(tableSelection.tbody, tableSelection.sr, tableSelection.sc, tableSelection.er, tableSelection.ec)
|
|
877
|
+
: [];
|
|
878
|
+
const candidates = selectedCells.length > 0
|
|
879
|
+
? selectedCells.flatMap((cell) => {
|
|
880
|
+
const blocks = Array.from(cell.children).filter((child) => child instanceof HTMLElement && child.matches("p,h1,h2,h3,h4,h5,h6,blockquote,pre"));
|
|
881
|
+
return blocks.length > 0 ? blocks : [cell];
|
|
882
|
+
})
|
|
883
|
+
: getSelectedBlocks(range).map(getAlignmentTarget);
|
|
884
|
+
const seen = new Set();
|
|
885
|
+
return candidates.filter((target) => {
|
|
886
|
+
if (!editor.contains(target) || seen.has(target))
|
|
887
|
+
return false;
|
|
888
|
+
seen.add(target);
|
|
889
|
+
return true;
|
|
890
|
+
});
|
|
891
|
+
};
|
|
892
|
+
const readTextAlignment = (target) => {
|
|
893
|
+
const explicit = target.style.textAlign;
|
|
894
|
+
if (explicit === "center" || explicit === "right" || explicit === "justify")
|
|
895
|
+
return explicit;
|
|
896
|
+
const inherited = target.closest("blockquote[style*='text-align']");
|
|
897
|
+
const inheritedValue = inherited?.style.textAlign;
|
|
898
|
+
return inheritedValue === "center" || inheritedValue === "right" || inheritedValue === "justify"
|
|
899
|
+
? inheritedValue
|
|
900
|
+
: "left";
|
|
901
|
+
};
|
|
902
|
+
const applyTextAlignment = (alignment) => {
|
|
903
|
+
try {
|
|
904
|
+
if (!restoreSavedSelection())
|
|
905
|
+
safeSelectRange(getSelectionRangeInEditor());
|
|
906
|
+
const range = getSelectionRangeInEditor();
|
|
907
|
+
if (!range)
|
|
908
|
+
return;
|
|
909
|
+
const targets = getAlignmentTargets(range);
|
|
910
|
+
if (targets.length === 0)
|
|
911
|
+
return;
|
|
912
|
+
pushEditorHistory();
|
|
913
|
+
targets.forEach((target) => {
|
|
914
|
+
target.style.textAlign = alignment === "left" ? "" : alignment;
|
|
915
|
+
if (!target.getAttribute("style"))
|
|
916
|
+
target.removeAttribute("style");
|
|
917
|
+
});
|
|
918
|
+
safeSelectRange(range);
|
|
919
|
+
savedRangeRef.current = range.cloneRange();
|
|
920
|
+
handleInput();
|
|
921
|
+
requestAnimationFrame(updateActiveState);
|
|
922
|
+
}
|
|
923
|
+
catch { }
|
|
924
|
+
};
|
|
492
925
|
const copyCellOrBlockStyles = (from, to) => {
|
|
493
926
|
to.innerHTML = from.innerHTML || "<br>";
|
|
494
927
|
const style = from.getAttribute("style");
|
|
@@ -555,6 +988,39 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
555
988
|
block.parentElement?.replaceChild(replacement, block);
|
|
556
989
|
return replacement;
|
|
557
990
|
};
|
|
991
|
+
const clearExplicitFontSizes = (block) => {
|
|
992
|
+
Array.from(block.querySelectorAll('[style*="font-size"]')).forEach((element) => {
|
|
993
|
+
element.style.fontSize = "";
|
|
994
|
+
if (!element.style.cssText)
|
|
995
|
+
element.removeAttribute("style");
|
|
996
|
+
if (element.tagName === "SPAN" && !element.getAttribute("style") && element.attributes.length === 0) {
|
|
997
|
+
const parent = element.parentNode;
|
|
998
|
+
if (!parent)
|
|
999
|
+
return;
|
|
1000
|
+
while (element.firstChild)
|
|
1001
|
+
parent.insertBefore(element.firstChild, element);
|
|
1002
|
+
element.remove();
|
|
1003
|
+
}
|
|
1004
|
+
});
|
|
1005
|
+
};
|
|
1006
|
+
const replaceListItemContentTag = (item, tag) => {
|
|
1007
|
+
const directBlock = Array.from(item.children).find((child) => child.matches("p,h1,h2,h3,h4,h5,h6"));
|
|
1008
|
+
if (directBlock)
|
|
1009
|
+
return replaceBlockTag(directBlock, tag);
|
|
1010
|
+
const block = document.createElement(tag);
|
|
1011
|
+
const boundary = Array.from(item.children).find((child) => child.matches("ul,ol,blockquote,pre")) || null;
|
|
1012
|
+
Array.from(item.childNodes).forEach((node) => {
|
|
1013
|
+
if (node === boundary)
|
|
1014
|
+
return;
|
|
1015
|
+
if (node instanceof HTMLElement && node.dataset.srteCheck === "true")
|
|
1016
|
+
return;
|
|
1017
|
+
block.appendChild(node);
|
|
1018
|
+
});
|
|
1019
|
+
if (!block.childNodes.length)
|
|
1020
|
+
block.innerHTML = "<br>";
|
|
1021
|
+
item.insertBefore(block, boundary);
|
|
1022
|
+
return block;
|
|
1023
|
+
};
|
|
558
1024
|
const applyFormatBlockFallback = (blockName) => {
|
|
559
1025
|
const editor = editableRef.current;
|
|
560
1026
|
const range = getSelectionRangeInEditor();
|
|
@@ -570,23 +1036,36 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
570
1036
|
}
|
|
571
1037
|
if (range.collapsed) {
|
|
572
1038
|
const block = getCurrentBlock();
|
|
573
|
-
if (!block || block === editor ||
|
|
1039
|
+
if (!block || block === editor || !block.parentElement)
|
|
574
1040
|
return false;
|
|
575
|
-
const
|
|
1041
|
+
const item = block.closest("li");
|
|
1042
|
+
const replacement = item ? replaceListItemContentTag(item, tag) : replaceBlockTag(block, tag);
|
|
1043
|
+
if (/^h[1-6]$/.test(tag))
|
|
1044
|
+
clearExplicitFontSizes(replacement);
|
|
576
1045
|
focusElementEnd(replacement);
|
|
577
1046
|
return true;
|
|
578
1047
|
}
|
|
579
1048
|
const selectedBlocks = sortInDocumentOrder(getSelectedBlocks(range)).filter((block) => {
|
|
580
1049
|
if (!editor.contains(block) || block === editor)
|
|
581
1050
|
return false;
|
|
582
|
-
if (block.closest("ul,ol"))
|
|
583
|
-
return false;
|
|
584
1051
|
return Boolean(block.parentElement);
|
|
585
1052
|
});
|
|
586
1053
|
if (selectedBlocks.length > 0) {
|
|
587
1054
|
let lastReplacement = null;
|
|
1055
|
+
const handledItems = new Set();
|
|
588
1056
|
selectedBlocks.forEach((block) => {
|
|
589
|
-
|
|
1057
|
+
const item = block.closest("li");
|
|
1058
|
+
if (item) {
|
|
1059
|
+
if (handledItems.has(item))
|
|
1060
|
+
return;
|
|
1061
|
+
handledItems.add(item);
|
|
1062
|
+
lastReplacement = replaceListItemContentTag(item, tag);
|
|
1063
|
+
}
|
|
1064
|
+
else {
|
|
1065
|
+
lastReplacement = replaceBlockTag(block, tag);
|
|
1066
|
+
}
|
|
1067
|
+
if (lastReplacement && /^h[1-6]$/.test(tag))
|
|
1068
|
+
clearExplicitFontSizes(lastReplacement);
|
|
590
1069
|
});
|
|
591
1070
|
if (lastReplacement)
|
|
592
1071
|
focusElementEnd(lastReplacement);
|
|
@@ -606,6 +1085,64 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
606
1085
|
Array.from(list.attributes).forEach((attr) => clone.setAttribute(attr.name, attr.value));
|
|
607
1086
|
return clone;
|
|
608
1087
|
};
|
|
1088
|
+
const mergeAdjacentCompatibleLists = (list) => {
|
|
1089
|
+
const isCompatible = (candidate) => candidate instanceof HTMLElement &&
|
|
1090
|
+
candidate.tagName === list.tagName &&
|
|
1091
|
+
candidate.style.listStyleType === list.style.listStyleType &&
|
|
1092
|
+
candidate.dataset.srteChecklist === list.dataset.srteChecklist &&
|
|
1093
|
+
candidate.dataset.srteChecklistStrike === list.dataset.srteChecklistStrike;
|
|
1094
|
+
let merged = list;
|
|
1095
|
+
const previous = merged.previousElementSibling;
|
|
1096
|
+
if (isCompatible(previous)) {
|
|
1097
|
+
while (merged.firstChild)
|
|
1098
|
+
previous.appendChild(merged.firstChild);
|
|
1099
|
+
merged.remove();
|
|
1100
|
+
merged = previous;
|
|
1101
|
+
}
|
|
1102
|
+
const next = merged.nextElementSibling;
|
|
1103
|
+
if (isCompatible(next)) {
|
|
1104
|
+
while (next.firstChild)
|
|
1105
|
+
merged.appendChild(next.firstChild);
|
|
1106
|
+
next.remove();
|
|
1107
|
+
}
|
|
1108
|
+
return merged;
|
|
1109
|
+
};
|
|
1110
|
+
const clearChecklist = (list) => {
|
|
1111
|
+
delete list.dataset.srteChecklist;
|
|
1112
|
+
delete list.dataset.srteChecklistStrike;
|
|
1113
|
+
list.querySelectorAll(':scope > li > [data-srte-check]').forEach((control) => control.remove());
|
|
1114
|
+
Array.from(list.children).forEach((item) => {
|
|
1115
|
+
if (item instanceof HTMLElement) {
|
|
1116
|
+
delete item.dataset.checked;
|
|
1117
|
+
item.style.textDecoration = "";
|
|
1118
|
+
}
|
|
1119
|
+
});
|
|
1120
|
+
};
|
|
1121
|
+
const decorateChecklist = (list, strikeCompleted) => {
|
|
1122
|
+
list.dataset.srteChecklist = "true";
|
|
1123
|
+
list.dataset.srteChecklistStrike = strikeCompleted ? "true" : "false";
|
|
1124
|
+
list.style.listStyleType = "none";
|
|
1125
|
+
list.style.paddingInlineStart = "1.5em";
|
|
1126
|
+
Array.from(list.children).forEach((item) => {
|
|
1127
|
+
if (!(item instanceof HTMLElement) || item.tagName !== "LI")
|
|
1128
|
+
return;
|
|
1129
|
+
const legacyCheckbox = item.querySelector(':scope > input[data-srte-check]');
|
|
1130
|
+
const checked = item.dataset.checked === "true" || Boolean(legacyCheckbox?.checked);
|
|
1131
|
+
item.querySelectorAll(':scope > [data-srte-check]').forEach((control) => control.remove());
|
|
1132
|
+
item.dataset.checked = checked ? "true" : "false";
|
|
1133
|
+
const control = document.createElement("button");
|
|
1134
|
+
control.type = "button";
|
|
1135
|
+
control.dataset.srteCheck = "true";
|
|
1136
|
+
control.contentEditable = "false";
|
|
1137
|
+
control.tabIndex = -1;
|
|
1138
|
+
control.setAttribute("aria-label", checked ? "Mark incomplete" : "Mark complete");
|
|
1139
|
+
control.textContent = checked ? "☑" : "☐";
|
|
1140
|
+
control.style.cssText = "margin-inline:-1.45em .45em;border:0;padding:0;background:transparent;color:inherit;font:inherit;cursor:pointer";
|
|
1141
|
+
item.prepend(control);
|
|
1142
|
+
item.style.textDecoration = strikeCompleted && checked ? "line-through" : "";
|
|
1143
|
+
});
|
|
1144
|
+
return mergeAdjacentCompatibleLists(list);
|
|
1145
|
+
};
|
|
609
1146
|
const focusElementEnd = (element) => {
|
|
610
1147
|
const range = document.createRange();
|
|
611
1148
|
range.selectNodeContents(element);
|
|
@@ -632,6 +1169,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
632
1169
|
return;
|
|
633
1170
|
const paragraph = document.createElement("p");
|
|
634
1171
|
paragraph.innerHTML = li.innerHTML || "<br>";
|
|
1172
|
+
paragraph.style.textAlign = li.style.textAlign;
|
|
635
1173
|
const beforeList = cloneListShell(list);
|
|
636
1174
|
const afterList = cloneListShell(list);
|
|
637
1175
|
while (list.firstChild && list.firstChild !== li) {
|
|
@@ -721,6 +1259,50 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
721
1259
|
requestAnimationFrame(updateActiveState);
|
|
722
1260
|
}
|
|
723
1261
|
};
|
|
1262
|
+
const restyleSelectedListItems = (items, listTag, styleType) => {
|
|
1263
|
+
const editor = editableRef.current;
|
|
1264
|
+
if (!editor || items.length === 0)
|
|
1265
|
+
return null;
|
|
1266
|
+
const selected = new Set(items);
|
|
1267
|
+
const sourceLists = Array.from(new Set(items.map((item) => item.parentElement)));
|
|
1268
|
+
let lastSelected = null;
|
|
1269
|
+
sourceLists.forEach((source) => {
|
|
1270
|
+
const parent = source.parentElement;
|
|
1271
|
+
if (!parent || !editor.contains(source))
|
|
1272
|
+
return;
|
|
1273
|
+
const outputs = [];
|
|
1274
|
+
let pending = null;
|
|
1275
|
+
let pendingSelected = null;
|
|
1276
|
+
Array.from(source.children).forEach((child) => {
|
|
1277
|
+
if (!(child instanceof HTMLElement) || child.tagName !== "LI")
|
|
1278
|
+
return;
|
|
1279
|
+
const isSelected = selected.has(child);
|
|
1280
|
+
if (!pending || pendingSelected !== isSelected) {
|
|
1281
|
+
pending = isSelected ? document.createElement(listTag) : cloneListShell(source);
|
|
1282
|
+
if (isSelected)
|
|
1283
|
+
pending.style.listStyleType = styleType;
|
|
1284
|
+
outputs.push(pending);
|
|
1285
|
+
pendingSelected = isSelected;
|
|
1286
|
+
}
|
|
1287
|
+
if (isSelected) {
|
|
1288
|
+
child.querySelectorAll(':scope > [data-srte-check]').forEach((control) => control.remove());
|
|
1289
|
+
delete child.dataset.checked;
|
|
1290
|
+
child.style.textDecoration = "";
|
|
1291
|
+
lastSelected = child;
|
|
1292
|
+
}
|
|
1293
|
+
pending.appendChild(child);
|
|
1294
|
+
});
|
|
1295
|
+
outputs.forEach((output) => parent.insertBefore(output, source));
|
|
1296
|
+
source.remove();
|
|
1297
|
+
outputs.forEach((output) => {
|
|
1298
|
+
if (output.tagName.toLowerCase() === listTag && output.style.listStyleType === styleType) {
|
|
1299
|
+
clearChecklist(output);
|
|
1300
|
+
}
|
|
1301
|
+
mergeAdjacentCompatibleLists(output);
|
|
1302
|
+
});
|
|
1303
|
+
});
|
|
1304
|
+
return lastSelected;
|
|
1305
|
+
};
|
|
724
1306
|
const applyListStyle = (value) => {
|
|
725
1307
|
const listTag = value.startsWith("ordered:") ? "ol" : "ul";
|
|
726
1308
|
const styleType = value.replace(/^(ordered|bullet):/, "");
|
|
@@ -729,6 +1311,19 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
729
1311
|
const range = getSelectionRangeInEditor();
|
|
730
1312
|
if (!range)
|
|
731
1313
|
return;
|
|
1314
|
+
const selectedBlocks = getSelectedBlocks(range);
|
|
1315
|
+
const selectedItems = getSelectedListItems(selectedBlocks);
|
|
1316
|
+
const selectedPlainBlocks = selectedBlocks.filter((block) => !block.closest("ul,ol"));
|
|
1317
|
+
if (selectedItems.length > 0) {
|
|
1318
|
+
pushEditorHistory();
|
|
1319
|
+
const lastItem = restyleSelectedListItems(selectedItems, listTag, styleType);
|
|
1320
|
+
const convertedPlainBlocks = convertSelectedBlocksToList(selectedPlainBlocks, listTag, styleType);
|
|
1321
|
+
if (!convertedPlainBlocks && lastItem)
|
|
1322
|
+
focusElementEnd(lastItem);
|
|
1323
|
+
handleInput();
|
|
1324
|
+
requestAnimationFrame(updateActiveState);
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
732
1327
|
const lists = new Set();
|
|
733
1328
|
getSelectedListItems(getSelectedBlocks(range)).forEach((item) => {
|
|
734
1329
|
const list = item.parentElement;
|
|
@@ -744,15 +1339,40 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
744
1339
|
lists.add(list);
|
|
745
1340
|
});
|
|
746
1341
|
if (lists.size === 0) {
|
|
747
|
-
const
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
1342
|
+
const blocks = range.collapsed
|
|
1343
|
+
? [getCurrentBlock()].filter((block) => Boolean(block))
|
|
1344
|
+
: getSelectedBlocks(range);
|
|
1345
|
+
const convertibleBlocks = blocks.filter((block) => editableRef.current?.contains(block) &&
|
|
1346
|
+
!block.closest("ul,ol") &&
|
|
1347
|
+
Boolean(block.parentElement));
|
|
1348
|
+
if (convertibleBlocks.length > 0) {
|
|
1349
|
+
pushEditorHistory();
|
|
1350
|
+
if (convertSelectedBlocksToList(convertibleBlocks, listTag)) {
|
|
1351
|
+
const createdList = getCurrentBlock()?.closest("ul,ol");
|
|
1352
|
+
if (createdList) {
|
|
1353
|
+
createdList.style.listStyleType = styleType;
|
|
1354
|
+
const mergedList = mergeAdjacentCompatibleLists(createdList);
|
|
1355
|
+
const lastItem = mergedList.lastElementChild;
|
|
1356
|
+
if (lastItem)
|
|
1357
|
+
focusElementEnd(lastItem);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
753
1360
|
handleInput();
|
|
1361
|
+
requestAnimationFrame(updateActiveState);
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
if (!range.collapsed && blocks.length === 0) {
|
|
1365
|
+
toggleList(listTag);
|
|
1366
|
+
const createdList = getCurrentBlock()?.closest("ul,ol");
|
|
1367
|
+
if (createdList) {
|
|
1368
|
+
createdList.style.listStyleType = styleType;
|
|
1369
|
+
handleInput();
|
|
1370
|
+
}
|
|
754
1371
|
return;
|
|
755
1372
|
}
|
|
1373
|
+
const block = getCurrentBlock();
|
|
1374
|
+
if (!block?.closest("ul,ol"))
|
|
1375
|
+
return;
|
|
756
1376
|
const currentList = block.closest("ul,ol");
|
|
757
1377
|
if (currentList)
|
|
758
1378
|
lists.add(currentList);
|
|
@@ -761,6 +1381,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
761
1381
|
return;
|
|
762
1382
|
pushEditorHistory();
|
|
763
1383
|
let lastList = null;
|
|
1384
|
+
const styledLists = [];
|
|
764
1385
|
lists.forEach((list) => {
|
|
765
1386
|
const target = list.tagName.toLowerCase() === listTag
|
|
766
1387
|
? list
|
|
@@ -769,8 +1390,15 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
769
1390
|
target.innerHTML = list.innerHTML;
|
|
770
1391
|
list.parentElement?.replaceChild(target, list);
|
|
771
1392
|
}
|
|
1393
|
+
clearChecklist(target);
|
|
772
1394
|
target.style.listStyleType = styleType;
|
|
773
1395
|
lastList = target;
|
|
1396
|
+
styledLists.push(target);
|
|
1397
|
+
});
|
|
1398
|
+
styledLists.forEach((list) => {
|
|
1399
|
+
if (editableRef.current?.contains(list)) {
|
|
1400
|
+
lastList = mergeAdjacentCompatibleLists(list);
|
|
1401
|
+
}
|
|
774
1402
|
});
|
|
775
1403
|
const lastItem = lastList?.lastElementChild;
|
|
776
1404
|
if (lastItem)
|
|
@@ -778,7 +1406,54 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
778
1406
|
handleInput();
|
|
779
1407
|
requestAnimationFrame(updateActiveState);
|
|
780
1408
|
};
|
|
781
|
-
const
|
|
1409
|
+
const applyChecklist = (strikeCompleted = false, toggleOff = false) => {
|
|
1410
|
+
if (!restoreSavedSelection())
|
|
1411
|
+
safeSelectRange(getSelectionRangeInEditor());
|
|
1412
|
+
const range = getSelectionRangeInEditor();
|
|
1413
|
+
if (!range)
|
|
1414
|
+
return;
|
|
1415
|
+
const selectedItems = getSelectedListItems(getSelectedBlocks(range));
|
|
1416
|
+
const selectedChecklists = selectedItems.filter((item) => item.parentElement?.dataset.srteChecklist === "true");
|
|
1417
|
+
if (toggleOff && selectedItems.length > 0 && selectedChecklists.length === selectedItems.length) {
|
|
1418
|
+
pushEditorHistory();
|
|
1419
|
+
selectedItems.forEach((item) => {
|
|
1420
|
+
item.querySelectorAll(':scope > [data-srte-check]').forEach((control) => control.remove());
|
|
1421
|
+
delete item.dataset.checked;
|
|
1422
|
+
item.style.textDecoration = "";
|
|
1423
|
+
});
|
|
1424
|
+
if (transformSelectedListItems(selectedItems, "ul")) {
|
|
1425
|
+
handleInput();
|
|
1426
|
+
requestAnimationFrame(updateActiveState);
|
|
1427
|
+
}
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
if (selectedItems.length > 0) {
|
|
1431
|
+
pushEditorHistory();
|
|
1432
|
+
const lastItem = restyleSelectedListItems(selectedItems, "ul", "none");
|
|
1433
|
+
const lists = new Set();
|
|
1434
|
+
selectedItems.forEach((item) => {
|
|
1435
|
+
if (item.parentElement)
|
|
1436
|
+
lists.add(item.parentElement);
|
|
1437
|
+
});
|
|
1438
|
+
lists.forEach((list) => decorateChecklist(list, strikeCompleted));
|
|
1439
|
+
if (lastItem)
|
|
1440
|
+
focusElementEnd(lastItem);
|
|
1441
|
+
handleInput();
|
|
1442
|
+
requestAnimationFrame(updateActiveState);
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
applyListStyle("bullet:none");
|
|
1446
|
+
const createdList = getCurrentBlock()?.closest("ul");
|
|
1447
|
+
if (!createdList)
|
|
1448
|
+
return;
|
|
1449
|
+
const merged = decorateChecklist(createdList, strikeCompleted);
|
|
1450
|
+
const lastItem = merged.lastElementChild;
|
|
1451
|
+
if (lastItem)
|
|
1452
|
+
focusElementEnd(lastItem);
|
|
1453
|
+
handleInput();
|
|
1454
|
+
requestAnimationFrame(updateActiveState);
|
|
1455
|
+
};
|
|
1456
|
+
const convertSelectedBlocksToList = (blocks, listTag, styleType) => {
|
|
782
1457
|
const editor = editableRef.current;
|
|
783
1458
|
if (!editor || blocks.length === 0)
|
|
784
1459
|
return false;
|
|
@@ -809,19 +1484,130 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
809
1484
|
return position & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
|
|
810
1485
|
});
|
|
811
1486
|
const list = document.createElement(listTag);
|
|
1487
|
+
if (styleType)
|
|
1488
|
+
list.style.listStyleType = styleType;
|
|
812
1489
|
parent.insertBefore(list, group[0]);
|
|
813
1490
|
group.forEach((block) => {
|
|
814
1491
|
const li = document.createElement("li");
|
|
815
1492
|
li.innerHTML = block.innerHTML || "<br>";
|
|
1493
|
+
li.style.textAlign = block.style.textAlign;
|
|
816
1494
|
list.appendChild(li);
|
|
817
1495
|
block.remove();
|
|
818
1496
|
lastLi = li;
|
|
819
1497
|
});
|
|
1498
|
+
mergeAdjacentCompatibleLists(list);
|
|
820
1499
|
});
|
|
821
1500
|
if (lastLi)
|
|
822
1501
|
focusElementEnd(lastLi);
|
|
823
1502
|
return true;
|
|
824
1503
|
};
|
|
1504
|
+
const convertTableCellSelectionToList = (range, listTag) => {
|
|
1505
|
+
if (range.collapsed)
|
|
1506
|
+
return false;
|
|
1507
|
+
const startCell = getClosestCell(range.startContainer);
|
|
1508
|
+
const endCell = getClosestCell(range.endContainer);
|
|
1509
|
+
if (!startCell || startCell !== endCell)
|
|
1510
|
+
return false;
|
|
1511
|
+
const fragment = range.extractContents();
|
|
1512
|
+
const parts = [document.createDocumentFragment()];
|
|
1513
|
+
Array.from(fragment.childNodes).forEach((node) => {
|
|
1514
|
+
if (node instanceof HTMLBRElement) {
|
|
1515
|
+
parts.push(document.createDocumentFragment());
|
|
1516
|
+
}
|
|
1517
|
+
else {
|
|
1518
|
+
parts[parts.length - 1].appendChild(node);
|
|
1519
|
+
}
|
|
1520
|
+
});
|
|
1521
|
+
const nonEmptyParts = parts.filter((part) => part.textContent?.trim() || part.childNodes.length > 0);
|
|
1522
|
+
if (nonEmptyParts.length === 0) {
|
|
1523
|
+
range.insertNode(fragment);
|
|
1524
|
+
return false;
|
|
1525
|
+
}
|
|
1526
|
+
const list = document.createElement(listTag);
|
|
1527
|
+
nonEmptyParts.forEach((part) => {
|
|
1528
|
+
const item = document.createElement("li");
|
|
1529
|
+
item.appendChild(part);
|
|
1530
|
+
if (!item.innerHTML.trim())
|
|
1531
|
+
item.innerHTML = "<br>";
|
|
1532
|
+
list.appendChild(item);
|
|
1533
|
+
});
|
|
1534
|
+
range.insertNode(list);
|
|
1535
|
+
const lastItem = list.lastElementChild;
|
|
1536
|
+
if (lastItem)
|
|
1537
|
+
focusElementEnd(lastItem);
|
|
1538
|
+
return true;
|
|
1539
|
+
};
|
|
1540
|
+
const convertRootLineSelectionToList = (range, listTag) => {
|
|
1541
|
+
const editor = editableRef.current;
|
|
1542
|
+
if (!editor || range.collapsed)
|
|
1543
|
+
return false;
|
|
1544
|
+
const closestBlock = (node) => {
|
|
1545
|
+
const element = node instanceof HTMLElement ? node : node.parentElement;
|
|
1546
|
+
const block = element?.closest(blockSelector);
|
|
1547
|
+
return block === editor ? null : block;
|
|
1548
|
+
};
|
|
1549
|
+
if (closestBlock(range.startContainer) || closestBlock(range.endContainer))
|
|
1550
|
+
return false;
|
|
1551
|
+
const lineRange = range.cloneRange();
|
|
1552
|
+
const breaks = Array.from(editor.querySelectorAll("br"));
|
|
1553
|
+
let previousBreak = null;
|
|
1554
|
+
let nextBreak = null;
|
|
1555
|
+
breaks.forEach((lineBreak) => {
|
|
1556
|
+
const parent = lineBreak.parentNode;
|
|
1557
|
+
if (!parent)
|
|
1558
|
+
return;
|
|
1559
|
+
const index = Array.prototype.indexOf.call(parent.childNodes, lineBreak);
|
|
1560
|
+
const beforeRelation = range.comparePoint(parent, index);
|
|
1561
|
+
const afterRelation = range.comparePoint(parent, index + 1);
|
|
1562
|
+
if (afterRelation === -1) {
|
|
1563
|
+
previousBreak = lineBreak;
|
|
1564
|
+
}
|
|
1565
|
+
else if (!nextBreak &&
|
|
1566
|
+
(beforeRelation === 1 || (beforeRelation === 0 && !range.intersectsNode(lineBreak)))) {
|
|
1567
|
+
nextBreak = lineBreak;
|
|
1568
|
+
}
|
|
1569
|
+
});
|
|
1570
|
+
if (previousBreak)
|
|
1571
|
+
lineRange.setStartAfter(previousBreak);
|
|
1572
|
+
else
|
|
1573
|
+
lineRange.setStart(editor, 0);
|
|
1574
|
+
if (nextBreak)
|
|
1575
|
+
lineRange.setEndBefore(nextBreak);
|
|
1576
|
+
else
|
|
1577
|
+
lineRange.setEnd(editor, editor.childNodes.length);
|
|
1578
|
+
const fragment = lineRange.extractContents();
|
|
1579
|
+
const parts = [document.createDocumentFragment()];
|
|
1580
|
+
Array.from(fragment.childNodes).forEach((node) => {
|
|
1581
|
+
if (node instanceof HTMLBRElement)
|
|
1582
|
+
parts.push(document.createDocumentFragment());
|
|
1583
|
+
else if (node instanceof HTMLElement && node.matches(blockSelector)) {
|
|
1584
|
+
if (parts[parts.length - 1].childNodes.length > 0)
|
|
1585
|
+
parts.push(document.createDocumentFragment());
|
|
1586
|
+
while (node.firstChild)
|
|
1587
|
+
parts[parts.length - 1].appendChild(node.firstChild);
|
|
1588
|
+
parts.push(document.createDocumentFragment());
|
|
1589
|
+
}
|
|
1590
|
+
else
|
|
1591
|
+
parts[parts.length - 1].appendChild(node);
|
|
1592
|
+
});
|
|
1593
|
+
const nonEmptyParts = parts.filter((part) => part.textContent?.length || part.childNodes.length > 0);
|
|
1594
|
+
if (nonEmptyParts.length === 0) {
|
|
1595
|
+
lineRange.insertNode(fragment);
|
|
1596
|
+
return false;
|
|
1597
|
+
}
|
|
1598
|
+
const list = document.createElement(listTag);
|
|
1599
|
+
nonEmptyParts.forEach((part) => {
|
|
1600
|
+
const item = document.createElement("li");
|
|
1601
|
+
item.appendChild(part);
|
|
1602
|
+
list.appendChild(item);
|
|
1603
|
+
});
|
|
1604
|
+
lineRange.insertNode(list);
|
|
1605
|
+
const mergedList = mergeAdjacentCompatibleLists(list);
|
|
1606
|
+
const lastItem = mergedList.lastElementChild;
|
|
1607
|
+
if (lastItem)
|
|
1608
|
+
focusElementEnd(lastItem);
|
|
1609
|
+
return true;
|
|
1610
|
+
};
|
|
825
1611
|
const transformSelectedListItems = (items, listTag) => {
|
|
826
1612
|
const editor = editableRef.current;
|
|
827
1613
|
if (!editor || items.length === 0)
|
|
@@ -860,6 +1646,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
860
1646
|
flushPendingList();
|
|
861
1647
|
const paragraph = document.createElement("p");
|
|
862
1648
|
paragraph.innerHTML = child.innerHTML || "<br>";
|
|
1649
|
+
paragraph.style.textAlign = child.style.textAlign;
|
|
863
1650
|
parent.insertBefore(paragraph, list);
|
|
864
1651
|
lastTarget = paragraph;
|
|
865
1652
|
child.remove();
|
|
@@ -919,6 +1706,21 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
919
1706
|
requestAnimationFrame(updateActiveState);
|
|
920
1707
|
return;
|
|
921
1708
|
}
|
|
1709
|
+
if (blocks.length === 0) {
|
|
1710
|
+
pushEditorHistory();
|
|
1711
|
+
if (convertRootLineSelectionToList(range, listTag)) {
|
|
1712
|
+
setListActiveState(true);
|
|
1713
|
+
handleInput();
|
|
1714
|
+
requestAnimationFrame(updateActiveState);
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
if (convertTableCellSelectionToList(range, listTag)) {
|
|
1718
|
+
setListActiveState(true);
|
|
1719
|
+
handleInput();
|
|
1720
|
+
requestAnimationFrame(updateActiveState);
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
922
1724
|
}
|
|
923
1725
|
const block = getCurrentBlock();
|
|
924
1726
|
if (!block) {
|
|
@@ -1006,19 +1808,69 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1006
1808
|
const selected = sortInDocumentOrder(blocks).filter((block) => {
|
|
1007
1809
|
if (!editor.contains(block) || block === editor)
|
|
1008
1810
|
return false;
|
|
1009
|
-
if (block.closest("ul,ol"))
|
|
1010
|
-
return false;
|
|
1011
1811
|
if (block.tagName.toLowerCase() === "blockquote")
|
|
1012
1812
|
return false;
|
|
1013
1813
|
return Boolean(block.parentElement);
|
|
1014
1814
|
});
|
|
1015
1815
|
if (selected.length === 0)
|
|
1016
1816
|
return false;
|
|
1817
|
+
const mergeQuote = (quote) => {
|
|
1818
|
+
let merged = quote;
|
|
1819
|
+
const previous = merged.previousElementSibling;
|
|
1820
|
+
if (previous?.tagName === "BLOCKQUOTE") {
|
|
1821
|
+
while (merged.firstChild)
|
|
1822
|
+
previous.appendChild(merged.firstChild);
|
|
1823
|
+
merged.remove();
|
|
1824
|
+
merged = previous;
|
|
1825
|
+
}
|
|
1826
|
+
const next = merged.nextElementSibling;
|
|
1827
|
+
if (next?.tagName === "BLOCKQUOTE") {
|
|
1828
|
+
while (next.firstChild)
|
|
1829
|
+
merged.appendChild(next.firstChild);
|
|
1830
|
+
next.remove();
|
|
1831
|
+
}
|
|
1832
|
+
return merged;
|
|
1833
|
+
};
|
|
1834
|
+
const selectedItems = new Set(getSelectedListItems(selected));
|
|
1835
|
+
const sourceLists = Array.from(new Set(Array.from(selectedItems, (item) => item.parentElement)));
|
|
1017
1836
|
let lastWrapped = null;
|
|
1018
|
-
|
|
1837
|
+
sourceLists.forEach((source) => {
|
|
1838
|
+
const parent = source.parentElement;
|
|
1839
|
+
if (!parent)
|
|
1840
|
+
return;
|
|
1841
|
+
let pending = null;
|
|
1842
|
+
let pendingSelected = null;
|
|
1843
|
+
const outputs = [];
|
|
1844
|
+
Array.from(source.children).forEach((child) => {
|
|
1845
|
+
if (!(child instanceof HTMLElement) || child.tagName !== "LI")
|
|
1846
|
+
return;
|
|
1847
|
+
const isSelected = selectedItems.has(child);
|
|
1848
|
+
if (!pending || pendingSelected !== isSelected) {
|
|
1849
|
+
pending = cloneListShell(source);
|
|
1850
|
+
pendingSelected = isSelected;
|
|
1851
|
+
outputs.push({ list: pending, selected: isSelected });
|
|
1852
|
+
}
|
|
1853
|
+
pending.appendChild(child);
|
|
1854
|
+
});
|
|
1855
|
+
outputs.forEach((output) => {
|
|
1856
|
+
if (output.selected) {
|
|
1857
|
+
const quote = document.createElement("blockquote");
|
|
1858
|
+
quote.appendChild(output.list);
|
|
1859
|
+
parent.insertBefore(quote, source);
|
|
1860
|
+
mergeQuote(quote);
|
|
1861
|
+
lastWrapped = output.list.lastElementChild;
|
|
1862
|
+
}
|
|
1863
|
+
else {
|
|
1864
|
+
parent.insertBefore(output.list, source);
|
|
1865
|
+
}
|
|
1866
|
+
});
|
|
1867
|
+
source.remove();
|
|
1868
|
+
});
|
|
1869
|
+
selected.filter((block) => !block.closest("ul,ol")).forEach((block) => {
|
|
1019
1870
|
const quote = document.createElement("blockquote");
|
|
1020
1871
|
block.parentElement?.insertBefore(quote, block);
|
|
1021
1872
|
quote.appendChild(block);
|
|
1873
|
+
mergeQuote(quote);
|
|
1022
1874
|
lastWrapped = block;
|
|
1023
1875
|
});
|
|
1024
1876
|
if (lastWrapped)
|
|
@@ -1044,93 +1896,243 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1044
1896
|
const block = getCurrentBlock();
|
|
1045
1897
|
if (block) {
|
|
1046
1898
|
pushEditorHistory();
|
|
1899
|
+
const currentList = block.closest("ul,ol");
|
|
1900
|
+
if (currentList && editor.contains(currentList)) {
|
|
1901
|
+
const quote = document.createElement("blockquote");
|
|
1902
|
+
currentList.parentElement?.insertBefore(quote, currentList);
|
|
1903
|
+
quote.appendChild(currentList);
|
|
1904
|
+
focusElementEnd(block);
|
|
1905
|
+
handleInput();
|
|
1906
|
+
requestAnimationFrame(updateActiveState);
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1047
1909
|
if (!wrapBlocks([block]))
|
|
1048
1910
|
return;
|
|
1049
1911
|
handleInput();
|
|
1050
1912
|
requestAnimationFrame(updateActiveState);
|
|
1051
1913
|
return;
|
|
1052
1914
|
}
|
|
1053
|
-
}
|
|
1054
|
-
else {
|
|
1055
|
-
const blocks = getSelectedBlocks(range);
|
|
1056
|
-
if (blocks.length > 0) {
|
|
1057
|
-
pushEditorHistory();
|
|
1058
|
-
if (!wrapBlocks(blocks))
|
|
1059
|
-
return;
|
|
1060
|
-
handleInput();
|
|
1061
|
-
requestAnimationFrame(updateActiveState);
|
|
1915
|
+
}
|
|
1916
|
+
else {
|
|
1917
|
+
const blocks = getSelectedBlocks(range);
|
|
1918
|
+
if (blocks.length > 0) {
|
|
1919
|
+
pushEditorHistory();
|
|
1920
|
+
if (!wrapBlocks(blocks))
|
|
1921
|
+
return;
|
|
1922
|
+
handleInput();
|
|
1923
|
+
requestAnimationFrame(updateActiveState);
|
|
1924
|
+
return;
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
pushEditorHistory();
|
|
1928
|
+
exec("formatBlock", "<blockquote>");
|
|
1929
|
+
}
|
|
1930
|
+
catch { }
|
|
1931
|
+
};
|
|
1932
|
+
const toggleCodeBlock = () => {
|
|
1933
|
+
try {
|
|
1934
|
+
if (!restoreSavedSelection())
|
|
1935
|
+
safeSelectRange(getSelectionRangeInEditor());
|
|
1936
|
+
const editor = editableRef.current;
|
|
1937
|
+
const range = getSelectionRangeInEditor();
|
|
1938
|
+
if (!editor || !range)
|
|
1939
|
+
return;
|
|
1940
|
+
const blocks = (range.collapsed
|
|
1941
|
+
? [getCurrentBlock()].filter((block) => Boolean(block))
|
|
1942
|
+
: getSelectedBlocks(range)).filter((block) => editor.contains(block));
|
|
1943
|
+
if (blocks.length === 0)
|
|
1944
|
+
return;
|
|
1945
|
+
const listItems = getSelectedListItems(blocks);
|
|
1946
|
+
const plainBlocks = blocks.filter((block) => !block.closest("ul,ol") && !block.closest("table"));
|
|
1947
|
+
const getItemCode = (item) => item.querySelector(":scope > pre[data-srte-list-code]");
|
|
1948
|
+
const allTargetsActive = listItems.every((item) => Boolean(getItemCode(item))) &&
|
|
1949
|
+
plainBlocks.every((block) => block.tagName === "PRE") &&
|
|
1950
|
+
listItems.length + plainBlocks.length > 0;
|
|
1951
|
+
pushEditorHistory();
|
|
1952
|
+
let lastTarget = null;
|
|
1953
|
+
listItems.forEach((item) => {
|
|
1954
|
+
const existing = getItemCode(item);
|
|
1955
|
+
if (allTargetsActive && existing) {
|
|
1956
|
+
const code = existing.querySelector(":scope > code");
|
|
1957
|
+
const source = code || existing;
|
|
1958
|
+
while (source.firstChild)
|
|
1959
|
+
item.insertBefore(source.firstChild, existing);
|
|
1960
|
+
existing.remove();
|
|
1961
|
+
lastTarget = item;
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
if (existing) {
|
|
1965
|
+
lastTarget = existing;
|
|
1062
1966
|
return;
|
|
1063
1967
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1968
|
+
const pre = document.createElement("pre");
|
|
1969
|
+
pre.dataset.srteListCode = "true";
|
|
1970
|
+
pre.style.textAlign = "left";
|
|
1971
|
+
const code = document.createElement("code");
|
|
1972
|
+
const nestedList = Array.from(item.children).find((child) => child.matches("ul,ol")) || null;
|
|
1973
|
+
Array.from(item.childNodes).forEach((node) => {
|
|
1974
|
+
if (node === nestedList)
|
|
1975
|
+
return;
|
|
1976
|
+
if (node instanceof HTMLElement && node.dataset.srteCheck === "true")
|
|
1977
|
+
return;
|
|
1978
|
+
code.appendChild(node);
|
|
1979
|
+
});
|
|
1980
|
+
if (!code.childNodes.length)
|
|
1981
|
+
code.innerHTML = "<br>";
|
|
1982
|
+
pre.appendChild(code);
|
|
1983
|
+
item.insertBefore(pre, nestedList);
|
|
1984
|
+
lastTarget = pre;
|
|
1985
|
+
});
|
|
1986
|
+
sortInDocumentOrder(plainBlocks).forEach((block) => {
|
|
1987
|
+
if (allTargetsActive && block.tagName === "PRE") {
|
|
1988
|
+
const paragraph = document.createElement("p");
|
|
1989
|
+
const code = block.querySelector(":scope > code");
|
|
1990
|
+
paragraph.innerHTML = code?.innerHTML || block.innerHTML || "<br>";
|
|
1991
|
+
block.parentElement?.replaceChild(paragraph, block);
|
|
1992
|
+
lastTarget = paragraph;
|
|
1993
|
+
}
|
|
1994
|
+
else if (block.tagName !== "PRE") {
|
|
1995
|
+
const pre = replaceBlockTag(block, "pre");
|
|
1996
|
+
pre.style.textAlign = "left";
|
|
1997
|
+
if (!pre.querySelector(":scope > code")) {
|
|
1998
|
+
const code = document.createElement("code");
|
|
1999
|
+
while (pre.firstChild)
|
|
2000
|
+
code.appendChild(pre.firstChild);
|
|
2001
|
+
pre.appendChild(code);
|
|
2002
|
+
}
|
|
2003
|
+
lastTarget = pre;
|
|
2004
|
+
}
|
|
2005
|
+
});
|
|
2006
|
+
if (lastTarget)
|
|
2007
|
+
focusElementEnd(lastTarget);
|
|
2008
|
+
handleInput();
|
|
2009
|
+
requestAnimationFrame(updateActiveState);
|
|
1067
2010
|
}
|
|
1068
2011
|
catch { }
|
|
1069
2012
|
};
|
|
1070
2013
|
const applyFontSize = (size) => {
|
|
1071
2014
|
try {
|
|
1072
|
-
// Update current font size state
|
|
1073
|
-
setCurrentFontSize(size);
|
|
1074
2015
|
const editor = editableRef.current;
|
|
1075
2016
|
if (!editor)
|
|
1076
2017
|
return;
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
// Use current range if it's within our editor
|
|
1084
|
-
if (editor.contains(currentRange.commonAncestorContainer)) {
|
|
1085
|
-
range = currentRange;
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
// Fallback to saved range if current range is not available
|
|
1089
|
-
if (!range && savedRangeRef.current) {
|
|
1090
|
-
range = savedRangeRef.current.cloneRange();
|
|
1091
|
-
}
|
|
1092
|
-
// If no range at all, just update state for future typing
|
|
2018
|
+
const valuePx = Number(size);
|
|
2019
|
+
if (!Number.isFinite(valuePx) || valuePx <= 0)
|
|
2020
|
+
return;
|
|
2021
|
+
if (!restoreSavedSelection())
|
|
2022
|
+
safeSelectRange(getSelectionRangeInEditor());
|
|
2023
|
+
const range = getSelectionRangeInEditor();
|
|
1093
2024
|
if (!range)
|
|
1094
2025
|
return;
|
|
1095
|
-
|
|
2026
|
+
setCurrentFontSize(String(Math.round(valuePx)));
|
|
1096
2027
|
if (range.collapsed) {
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
const newRange = document.createRange();
|
|
1104
|
-
newRange.setStart(span.firstChild, 1);
|
|
1105
|
-
newRange.collapse(true);
|
|
1106
|
-
if (sel) {
|
|
1107
|
-
sel.removeAllRanges();
|
|
1108
|
-
sel.addRange(newRange);
|
|
1109
|
-
}
|
|
1110
|
-
handleInput();
|
|
2028
|
+
pendingFontSizeRef.current = {
|
|
2029
|
+
valuePx,
|
|
2030
|
+
container: range.startContainer,
|
|
2031
|
+
offset: range.startOffset,
|
|
2032
|
+
};
|
|
2033
|
+
savedRangeRef.current = range.cloneRange();
|
|
1111
2034
|
return;
|
|
1112
2035
|
}
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
2036
|
+
pushEditorHistory();
|
|
2037
|
+
pendingFontSizeRef.current = null;
|
|
2038
|
+
const textNodes = [];
|
|
2039
|
+
const walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT);
|
|
2040
|
+
let candidate = walker.nextNode();
|
|
2041
|
+
while (candidate) {
|
|
2042
|
+
const text = candidate;
|
|
2043
|
+
const owner = text.parentElement;
|
|
2044
|
+
try {
|
|
2045
|
+
if (text.data.length > 0 && range.intersectsNode(text) &&
|
|
2046
|
+
!owner?.closest('[contenteditable="false"],button,[data-srte-editor-only="true"]'))
|
|
2047
|
+
textNodes.push(text);
|
|
2048
|
+
}
|
|
2049
|
+
catch { }
|
|
2050
|
+
candidate = walker.nextNode();
|
|
2051
|
+
}
|
|
2052
|
+
const selectedTexts = [];
|
|
2053
|
+
[...textNodes].reverse().forEach((text) => {
|
|
2054
|
+
const start = text === range.startContainer ? range.startOffset : 0;
|
|
2055
|
+
const end = text === range.endContainer ? range.endOffset : text.data.length;
|
|
2056
|
+
if (end <= start)
|
|
2057
|
+
return;
|
|
2058
|
+
if (end < text.data.length)
|
|
2059
|
+
text.splitText(end);
|
|
2060
|
+
const selected = start > 0 ? text.splitText(start) : text;
|
|
2061
|
+
const parent = selected.parentElement;
|
|
2062
|
+
if (parent?.tagName === "SPAN" &&
|
|
2063
|
+
parent.childNodes.length === 1 &&
|
|
2064
|
+
parent.textContent === selected.data) {
|
|
2065
|
+
parent.style.fontSize = `${valuePx}px`;
|
|
2066
|
+
}
|
|
2067
|
+
else {
|
|
2068
|
+
const span = document.createElement("span");
|
|
2069
|
+
span.style.fontSize = `${valuePx}px`;
|
|
2070
|
+
selected.parentNode?.insertBefore(span, selected);
|
|
2071
|
+
span.appendChild(selected);
|
|
2072
|
+
}
|
|
2073
|
+
selectedTexts.unshift(selected);
|
|
2074
|
+
});
|
|
2075
|
+
if (selectedTexts.length > 0) {
|
|
2076
|
+
normalizeFontSizeSpans(editor);
|
|
2077
|
+
const nextRange = document.createRange();
|
|
2078
|
+
nextRange.setStart(selectedTexts[0], 0);
|
|
2079
|
+
const last = selectedTexts[selectedTexts.length - 1];
|
|
2080
|
+
nextRange.setEnd(last, last.data.length);
|
|
2081
|
+
safeSelectRange(nextRange);
|
|
2082
|
+
savedRangeRef.current = nextRange.cloneRange();
|
|
1126
2083
|
}
|
|
1127
|
-
// Trigger change event
|
|
1128
2084
|
handleInput();
|
|
2085
|
+
requestAnimationFrame(updateActiveState);
|
|
1129
2086
|
}
|
|
1130
2087
|
catch (error) {
|
|
1131
2088
|
console.error('Error applying font size:', error);
|
|
1132
2089
|
}
|
|
1133
2090
|
};
|
|
2091
|
+
useEffect(() => {
|
|
2092
|
+
const editor = editableRef.current;
|
|
2093
|
+
if (!editor)
|
|
2094
|
+
return;
|
|
2095
|
+
const applyPendingFontSize = (event) => {
|
|
2096
|
+
const pending = pendingFontSizeRef.current;
|
|
2097
|
+
if (!pending || event.inputType !== "insertText" || !event.data)
|
|
2098
|
+
return;
|
|
2099
|
+
const range = getSelectionRangeInEditor();
|
|
2100
|
+
if (!range?.collapsed ||
|
|
2101
|
+
range.startContainer !== pending.container ||
|
|
2102
|
+
range.startOffset !== pending.offset)
|
|
2103
|
+
return;
|
|
2104
|
+
event.preventDefault();
|
|
2105
|
+
event.stopPropagation();
|
|
2106
|
+
pushEditorHistory();
|
|
2107
|
+
const text = document.createTextNode(event.data);
|
|
2108
|
+
const sizedAncestor = range.startContainer instanceof HTMLElement
|
|
2109
|
+
? range.startContainer.closest("span")
|
|
2110
|
+
: range.startContainer.parentElement?.closest("span");
|
|
2111
|
+
if (sizedAncestor instanceof HTMLElement &&
|
|
2112
|
+
parseFontSizePx(sizedAncestor.style.fontSize) === pending.valuePx) {
|
|
2113
|
+
range.insertNode(text);
|
|
2114
|
+
}
|
|
2115
|
+
else {
|
|
2116
|
+
const span = document.createElement("span");
|
|
2117
|
+
span.style.fontSize = `${pending.valuePx}px`;
|
|
2118
|
+
span.appendChild(text);
|
|
2119
|
+
range.insertNode(span);
|
|
2120
|
+
}
|
|
2121
|
+
const nextRange = document.createRange();
|
|
2122
|
+
nextRange.setStartAfter(text);
|
|
2123
|
+
nextRange.collapse(true);
|
|
2124
|
+
safeSelectRange(nextRange);
|
|
2125
|
+
pendingFontSizeRef.current = {
|
|
2126
|
+
valuePx: pending.valuePx,
|
|
2127
|
+
container: nextRange.startContainer,
|
|
2128
|
+
offset: nextRange.startOffset,
|
|
2129
|
+
};
|
|
2130
|
+
savedRangeRef.current = nextRange.cloneRange();
|
|
2131
|
+
handleInput();
|
|
2132
|
+
};
|
|
2133
|
+
editor.addEventListener("beforeinput", applyPendingFontSize);
|
|
2134
|
+
return () => editor.removeEventListener("beforeinput", applyPendingFontSize);
|
|
2135
|
+
}, []);
|
|
1134
2136
|
const applyFontFamily = (font) => {
|
|
1135
2137
|
try {
|
|
1136
2138
|
setCurrentFont(font);
|
|
@@ -2218,164 +3220,6 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
2218
3220
|
.replace(/&/g, "&")
|
|
2219
3221
|
.replace(/</g, "<")
|
|
2220
3222
|
.replace(/>/g, ">");
|
|
2221
|
-
const escapeHtmlAttribute = (value) => escapeHtml(value).replace(/"/g, """);
|
|
2222
|
-
const markdownToHtml = (markdown) => {
|
|
2223
|
-
const lines = markdown.replace(/\r\n/g, "\n").split("\n");
|
|
2224
|
-
let html = "";
|
|
2225
|
-
let listType = null;
|
|
2226
|
-
let paragraph = [];
|
|
2227
|
-
let codeFence = null;
|
|
2228
|
-
const closeList = () => {
|
|
2229
|
-
if (listType) {
|
|
2230
|
-
html += `</${listType}>`;
|
|
2231
|
-
listType = null;
|
|
2232
|
-
}
|
|
2233
|
-
};
|
|
2234
|
-
const inline = (text) => {
|
|
2235
|
-
const codeTokens = [];
|
|
2236
|
-
let value = text.replace(/`([^`]+)`/g, (_match, code) => {
|
|
2237
|
-
const token = `@@SRTE_CODE_${codeTokens.length}@@`;
|
|
2238
|
-
codeTokens.push(`<code>${escapeHtml(code)}</code>`);
|
|
2239
|
-
return token;
|
|
2240
|
-
});
|
|
2241
|
-
value = escapeHtml(value)
|
|
2242
|
-
.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^"]+)")?\)/g, (_match, alt, src, title) => {
|
|
2243
|
-
const titleAttr = title ? ` title="${escapeHtmlAttribute(title)}"` : "";
|
|
2244
|
-
return `<img src="${escapeHtmlAttribute(src)}" alt="${escapeHtmlAttribute(alt)}"${titleAttr}>`;
|
|
2245
|
-
})
|
|
2246
|
-
.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"([^"]+)")?\)/g, (_match, label, href, title) => {
|
|
2247
|
-
const titleAttr = title ? ` title="${escapeHtmlAttribute(title)}"` : "";
|
|
2248
|
-
return `<a href="${escapeHtmlAttribute(href)}"${titleAttr}>${label}</a>`;
|
|
2249
|
-
})
|
|
2250
|
-
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
|
|
2251
|
-
.replace(/__([^_]+)__/g, "<strong>$1</strong>")
|
|
2252
|
-
.replace(/~~([^~]+)~~/g, "<s>$1</s>")
|
|
2253
|
-
.replace(/(^|[^*])\*([^*\n]+)\*/g, "$1<em>$2</em>")
|
|
2254
|
-
.replace(/(^|[^_])_([^_\n]+)_/g, "$1<em>$2</em>");
|
|
2255
|
-
codeTokens.forEach((replacement, index) => {
|
|
2256
|
-
value = value.replace(`@@SRTE_CODE_${index}@@`, replacement);
|
|
2257
|
-
});
|
|
2258
|
-
return value;
|
|
2259
|
-
};
|
|
2260
|
-
const closeParagraph = () => {
|
|
2261
|
-
if (!paragraph.length)
|
|
2262
|
-
return;
|
|
2263
|
-
html += `<p>${inline(paragraph.join(" "))}</p>`;
|
|
2264
|
-
paragraph = [];
|
|
2265
|
-
};
|
|
2266
|
-
const isTableSeparator = (line) => /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(line);
|
|
2267
|
-
const parseTableRow = (line) => {
|
|
2268
|
-
let value = line.trim();
|
|
2269
|
-
if (value.startsWith("|"))
|
|
2270
|
-
value = value.slice(1);
|
|
2271
|
-
if (value.endsWith("|"))
|
|
2272
|
-
value = value.slice(0, -1);
|
|
2273
|
-
return value.split("|").map((cell) => cell.trim());
|
|
2274
|
-
};
|
|
2275
|
-
const renderTable = (startIndex) => {
|
|
2276
|
-
const header = parseTableRow(lines[startIndex]);
|
|
2277
|
-
let index = startIndex + 2;
|
|
2278
|
-
const rows = [];
|
|
2279
|
-
while (index < lines.length && lines[index].includes("|") && lines[index].trim()) {
|
|
2280
|
-
rows.push(parseTableRow(lines[index]));
|
|
2281
|
-
index += 1;
|
|
2282
|
-
}
|
|
2283
|
-
const headHtml = `<thead><tr>${header.map((cell) => `<th>${inline(cell)}</th>`).join("")}</tr></thead>`;
|
|
2284
|
-
const bodyHtml = rows.length
|
|
2285
|
-
? `<tbody>${rows.map((row) => `<tr>${header.map((_cell, cellIndex) => `<td>${inline(row[cellIndex] || "")}</td>`).join("")}</tr>`).join("")}</tbody>`
|
|
2286
|
-
: "";
|
|
2287
|
-
html += `<table style="border-collapse: collapse; width: 100%; margin: 12px 0;">${headHtml}${bodyHtml}</table>`;
|
|
2288
|
-
return index;
|
|
2289
|
-
};
|
|
2290
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
2291
|
-
const line = lines[i];
|
|
2292
|
-
const trimmed = line.trim();
|
|
2293
|
-
const fence = /^```([A-Za-z0-9_-]+)?\s*$/.exec(trimmed);
|
|
2294
|
-
if (fence) {
|
|
2295
|
-
closeParagraph();
|
|
2296
|
-
closeList();
|
|
2297
|
-
if (codeFence) {
|
|
2298
|
-
const langClass = codeFence.lang ? ` class="language-${escapeHtmlAttribute(codeFence.lang)}"` : "";
|
|
2299
|
-
html += `<pre><code${langClass}>${escapeHtml(codeFence.lines.join("\n"))}</code></pre>`;
|
|
2300
|
-
codeFence = null;
|
|
2301
|
-
}
|
|
2302
|
-
else {
|
|
2303
|
-
codeFence = { lang: fence[1] || "", lines: [] };
|
|
2304
|
-
}
|
|
2305
|
-
continue;
|
|
2306
|
-
}
|
|
2307
|
-
if (codeFence) {
|
|
2308
|
-
codeFence.lines.push(line);
|
|
2309
|
-
continue;
|
|
2310
|
-
}
|
|
2311
|
-
if (!trimmed) {
|
|
2312
|
-
closeParagraph();
|
|
2313
|
-
closeList();
|
|
2314
|
-
continue;
|
|
2315
|
-
}
|
|
2316
|
-
if (/^(-{3,}|\*{3,}|_{3,})$/.test(trimmed)) {
|
|
2317
|
-
closeParagraph();
|
|
2318
|
-
closeList();
|
|
2319
|
-
html += "<hr>";
|
|
2320
|
-
continue;
|
|
2321
|
-
}
|
|
2322
|
-
if (i + 1 < lines.length && trimmed.includes("|") && isTableSeparator(lines[i + 1])) {
|
|
2323
|
-
closeParagraph();
|
|
2324
|
-
closeList();
|
|
2325
|
-
i = renderTable(i) - 1;
|
|
2326
|
-
continue;
|
|
2327
|
-
}
|
|
2328
|
-
const heading = /^(#{1,6})\s+(.+)$/.exec(trimmed);
|
|
2329
|
-
if (heading) {
|
|
2330
|
-
closeParagraph();
|
|
2331
|
-
closeList();
|
|
2332
|
-
const level = heading[1].length;
|
|
2333
|
-
html += `<h${level}>${inline(heading[2])}</h${level}>`;
|
|
2334
|
-
continue;
|
|
2335
|
-
}
|
|
2336
|
-
const bullet = /^[-*+]\s+(.+)$/.exec(trimmed);
|
|
2337
|
-
if (bullet) {
|
|
2338
|
-
closeParagraph();
|
|
2339
|
-
if (listType !== "ul") {
|
|
2340
|
-
closeList();
|
|
2341
|
-
html += "<ul>";
|
|
2342
|
-
listType = "ul";
|
|
2343
|
-
}
|
|
2344
|
-
html += `<li>${inline(bullet[1])}</li>`;
|
|
2345
|
-
continue;
|
|
2346
|
-
}
|
|
2347
|
-
const numbered = /^\d+[.)]\s+(.+)$/.exec(trimmed);
|
|
2348
|
-
if (numbered) {
|
|
2349
|
-
closeParagraph();
|
|
2350
|
-
if (listType !== "ol") {
|
|
2351
|
-
closeList();
|
|
2352
|
-
html += "<ol>";
|
|
2353
|
-
listType = "ol";
|
|
2354
|
-
}
|
|
2355
|
-
html += `<li>${inline(numbered[1])}</li>`;
|
|
2356
|
-
continue;
|
|
2357
|
-
}
|
|
2358
|
-
const quote = /^>\s?(.*)$/.exec(trimmed);
|
|
2359
|
-
if (quote) {
|
|
2360
|
-
closeParagraph();
|
|
2361
|
-
closeList();
|
|
2362
|
-
html += `<blockquote>${inline(quote[1]) || "<br>"}</blockquote>`;
|
|
2363
|
-
continue;
|
|
2364
|
-
}
|
|
2365
|
-
closeList();
|
|
2366
|
-
paragraph.push(trimmed);
|
|
2367
|
-
}
|
|
2368
|
-
if (codeFence) {
|
|
2369
|
-
const langClass = codeFence.lang ? ` class="language-${escapeHtmlAttribute(codeFence.lang)}"` : "";
|
|
2370
|
-
html += `<pre><code${langClass}>${escapeHtml(codeFence.lines.join("\n"))}</code></pre>`;
|
|
2371
|
-
}
|
|
2372
|
-
closeParagraph();
|
|
2373
|
-
closeList();
|
|
2374
|
-
const root = document.createElement("div");
|
|
2375
|
-
root.innerHTML = html;
|
|
2376
|
-
enhanceImportedTables(root);
|
|
2377
|
-
return root.innerHTML;
|
|
2378
|
-
};
|
|
2379
3223
|
const htmlToMarkdown = (html) => {
|
|
2380
3224
|
const root = document.createElement("div");
|
|
2381
3225
|
root.innerHTML = html;
|
|
@@ -2419,7 +3263,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
2419
3263
|
return;
|
|
2420
3264
|
const file = files[0];
|
|
2421
3265
|
const text = await file.text();
|
|
2422
|
-
const html = type === "html" ? text :
|
|
3266
|
+
const html = type === "html" ? text : markdownToCompatibilityHtml(text);
|
|
2423
3267
|
const el = editableRef.current;
|
|
2424
3268
|
const hasContent = el && el.textContent && el.textContent.trim().length > 0;
|
|
2425
3269
|
insertImportedHtml(html, hasContent ? "append" : "replace", {
|
|
@@ -2885,11 +3729,73 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
2885
3729
|
console.error("Error wrapping tables", e);
|
|
2886
3730
|
}
|
|
2887
3731
|
};
|
|
3732
|
+
const normalizeInvalidTableNesting = (root) => {
|
|
3733
|
+
const getOuterList = (item) => {
|
|
3734
|
+
let list = item.parentElement;
|
|
3735
|
+
while (list?.parentElement?.tagName === "LI") {
|
|
3736
|
+
const parentList = list.parentElement.parentElement;
|
|
3737
|
+
if (!parentList || !["UL", "OL"].includes(parentList.tagName))
|
|
3738
|
+
break;
|
|
3739
|
+
list = parentList;
|
|
3740
|
+
}
|
|
3741
|
+
return list;
|
|
3742
|
+
};
|
|
3743
|
+
root.querySelectorAll("table").forEach((table) => {
|
|
3744
|
+
const tableBlock = (table.closest('[data-table-wrapper="true"]') || table);
|
|
3745
|
+
const codeBlock = tableBlock.closest("pre");
|
|
3746
|
+
if (codeBlock?.parentElement) {
|
|
3747
|
+
codeBlock.parentElement.insertBefore(tableBlock, codeBlock.nextSibling);
|
|
3748
|
+
return;
|
|
3749
|
+
}
|
|
3750
|
+
const listItem = tableBlock.closest("li");
|
|
3751
|
+
if (!listItem)
|
|
3752
|
+
return;
|
|
3753
|
+
const outerList = getOuterList(listItem);
|
|
3754
|
+
if (outerList?.parentElement) {
|
|
3755
|
+
outerList.parentElement.insertBefore(tableBlock, outerList.nextSibling);
|
|
3756
|
+
}
|
|
3757
|
+
});
|
|
3758
|
+
};
|
|
3759
|
+
const normalizeInvalidQuoteNesting = (root) => {
|
|
3760
|
+
root.querySelectorAll("blockquote blockquote").forEach((quote) => {
|
|
3761
|
+
const outerQuote = quote.parentElement?.closest("blockquote");
|
|
3762
|
+
if (outerQuote?.parentElement) {
|
|
3763
|
+
outerQuote.parentElement.insertBefore(quote, outerQuote.nextSibling);
|
|
3764
|
+
}
|
|
3765
|
+
});
|
|
3766
|
+
root.querySelectorAll("p,h1,h2,h3,h4,h5,h6,pre").forEach((container) => {
|
|
3767
|
+
const nestedQuotes = Array.from(container.children).filter((child) => child.tagName === "BLOCKQUOTE");
|
|
3768
|
+
nestedQuotes.forEach((quote) => {
|
|
3769
|
+
container.parentElement?.insertBefore(quote, container.nextSibling);
|
|
3770
|
+
});
|
|
3771
|
+
if (nestedQuotes.length > 0 &&
|
|
3772
|
+
container.tagName === "P" &&
|
|
3773
|
+
!container.textContent?.trim() &&
|
|
3774
|
+
Array.from(container.children).every((child) => child.tagName === "BR")) {
|
|
3775
|
+
container.remove();
|
|
3776
|
+
}
|
|
3777
|
+
});
|
|
3778
|
+
};
|
|
3779
|
+
const normalizeInvalidCodeBlockNesting = (root) => {
|
|
3780
|
+
root.querySelectorAll("pre pre").forEach((codeBlock) => {
|
|
3781
|
+
const outerCodeBlock = codeBlock.parentElement?.closest("pre");
|
|
3782
|
+
if (outerCodeBlock?.parentElement) {
|
|
3783
|
+
outerCodeBlock.parentElement.insertBefore(codeBlock, outerCodeBlock.nextSibling);
|
|
3784
|
+
}
|
|
3785
|
+
});
|
|
3786
|
+
root.querySelectorAll("p,h1,h2,h3,h4,h5,h6,blockquote").forEach((container) => {
|
|
3787
|
+
const nestedCodeBlocks = Array.from(container.children).filter((child) => child.tagName === "PRE");
|
|
3788
|
+
nestedCodeBlocks.forEach((codeBlock) => {
|
|
3789
|
+
container.parentElement?.insertBefore(codeBlock, container.nextSibling);
|
|
3790
|
+
});
|
|
3791
|
+
});
|
|
3792
|
+
};
|
|
2888
3793
|
const isCaretBoundaryBlock = (node) => {
|
|
2889
3794
|
if (!(node instanceof HTMLElement))
|
|
2890
3795
|
return false;
|
|
2891
3796
|
const tag = node.tagName.toLowerCase();
|
|
2892
3797
|
return (tag === "blockquote" ||
|
|
3798
|
+
tag === "pre" ||
|
|
2893
3799
|
tag === "table" ||
|
|
2894
3800
|
node.getAttribute("data-table-wrapper") === "true");
|
|
2895
3801
|
};
|
|
@@ -2922,6 +3828,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
2922
3828
|
return;
|
|
2923
3829
|
// Auto-fix negative margins that might cause visibility issues
|
|
2924
3830
|
fixNegativeMargins(el);
|
|
3831
|
+
// Quotes are document blocks and cannot be nested by drag and drop.
|
|
3832
|
+
normalizeInvalidQuoteNesting(el);
|
|
3833
|
+
// Code blocks are document blocks and cannot be nested by drag and drop.
|
|
3834
|
+
normalizeInvalidCodeBlockNesting(el);
|
|
3835
|
+
// Tables are document-level blocks and must not remain inside code or list items.
|
|
3836
|
+
normalizeInvalidTableNesting(el);
|
|
2925
3837
|
// Ensure tables are wrapped for horizontal scrolling
|
|
2926
3838
|
ensureTableWrappers(el);
|
|
2927
3839
|
// Keep a reachable typing position around isolating blocks at document edges
|
|
@@ -3509,13 +4421,19 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3509
4421
|
if (image && editor.contains(image)) {
|
|
3510
4422
|
return (image.parentElement?.tagName === "A" ? image.parentElement : image);
|
|
3511
4423
|
}
|
|
4424
|
+
const listElement = element.closest("ul,ol");
|
|
4425
|
+
if (listElement && editor.contains(listElement))
|
|
4426
|
+
return listElement;
|
|
3512
4427
|
const tableElement = element.closest("table");
|
|
3513
4428
|
if (tableElement && editor.contains(tableElement)) {
|
|
3514
4429
|
return (tableElement.closest('[data-table-wrapper="true"]') || tableElement);
|
|
3515
4430
|
}
|
|
3516
|
-
const
|
|
3517
|
-
if (
|
|
3518
|
-
return
|
|
4431
|
+
const quoteElement = element.closest("blockquote");
|
|
4432
|
+
if (quoteElement && editor.contains(quoteElement))
|
|
4433
|
+
return quoteElement;
|
|
4434
|
+
const codeBlock = element.closest("pre");
|
|
4435
|
+
if (codeBlock && editor.contains(codeBlock))
|
|
4436
|
+
return codeBlock;
|
|
3519
4437
|
const block = element.closest('[data-table-wrapper="true"],blockquote,pre,p,h1,h2,h3,h4,h5,h6,div');
|
|
3520
4438
|
if (!block || block === editor || !editor.contains(block))
|
|
3521
4439
|
return null;
|
|
@@ -3535,8 +4453,13 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3535
4453
|
}
|
|
3536
4454
|
const targetRect = target.getBoundingClientRect();
|
|
3537
4455
|
const scrollRect = scroller.getBoundingClientRect();
|
|
4456
|
+
const isTableTarget = target.matches("table,[data-table-wrapper='true']") ||
|
|
4457
|
+
Boolean(target.querySelector("table"));
|
|
4458
|
+
const isListInsideCell = target.matches("ul,ol") &&
|
|
4459
|
+
Boolean(target.closest("td,th"));
|
|
3538
4460
|
const next = {
|
|
3539
|
-
|
|
4461
|
+
// Table handles sit on the table's left border so nested tables remain reachable.
|
|
4462
|
+
left: Math.max(4, targetRect.left - scrollRect.left + scroller.scrollLeft - (isTableTarget || isListInsideCell ? 12 : 30)),
|
|
3540
4463
|
top: targetRect.top - scrollRect.top + scroller.scrollTop,
|
|
3541
4464
|
height: Math.max(24, targetRect.height),
|
|
3542
4465
|
target,
|
|
@@ -3559,7 +4482,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3559
4482
|
dragHandleHideTimerRef.current = null;
|
|
3560
4483
|
if (!draggedBlockRef.current)
|
|
3561
4484
|
setDragHandle(null);
|
|
3562
|
-
},
|
|
4485
|
+
}, 350);
|
|
3563
4486
|
};
|
|
3564
4487
|
const getImageFromMovableBlock = (block) => {
|
|
3565
4488
|
if (block.tagName === "IMG")
|
|
@@ -3571,6 +4494,50 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3571
4494
|
if (!editor || !editor.contains(block))
|
|
3572
4495
|
return false;
|
|
3573
4496
|
const under = document.elementFromPoint(x, y);
|
|
4497
|
+
const draggedQuote = block.tagName === "BLOCKQUOTE";
|
|
4498
|
+
const draggedCodeBlock = block.tagName === "PRE";
|
|
4499
|
+
if (draggedQuote) {
|
|
4500
|
+
const underElement = under instanceof HTMLElement ? under : under?.parentElement;
|
|
4501
|
+
const quoteTarget = underElement?.closest("blockquote");
|
|
4502
|
+
if (quoteTarget && quoteTarget !== block) {
|
|
4503
|
+
let rootQuote = quoteTarget;
|
|
4504
|
+
while (rootQuote.parentElement?.closest("blockquote")) {
|
|
4505
|
+
rootQuote = rootQuote.parentElement.closest("blockquote");
|
|
4506
|
+
}
|
|
4507
|
+
const parent = rootQuote.parentElement;
|
|
4508
|
+
if (!parent)
|
|
4509
|
+
return false;
|
|
4510
|
+
const rect = rootQuote.getBoundingClientRect();
|
|
4511
|
+
if (rootQuote.contains(block) || y >= rect.top + rect.height / 2) {
|
|
4512
|
+
parent.insertBefore(block, rootQuote.nextSibling);
|
|
4513
|
+
}
|
|
4514
|
+
else {
|
|
4515
|
+
parent.insertBefore(block, rootQuote);
|
|
4516
|
+
}
|
|
4517
|
+
return true;
|
|
4518
|
+
}
|
|
4519
|
+
}
|
|
4520
|
+
if (draggedCodeBlock) {
|
|
4521
|
+
const underElement = under instanceof HTMLElement ? under : under?.parentElement;
|
|
4522
|
+
const codeTarget = underElement?.closest("pre");
|
|
4523
|
+
if (codeTarget && codeTarget !== block) {
|
|
4524
|
+
let rootCodeBlock = codeTarget;
|
|
4525
|
+
while (rootCodeBlock.parentElement?.closest("pre")) {
|
|
4526
|
+
rootCodeBlock = rootCodeBlock.parentElement.closest("pre");
|
|
4527
|
+
}
|
|
4528
|
+
const parent = rootCodeBlock.parentElement;
|
|
4529
|
+
if (!parent)
|
|
4530
|
+
return false;
|
|
4531
|
+
const rect = rootCodeBlock.getBoundingClientRect();
|
|
4532
|
+
if (rootCodeBlock.contains(block) || y >= rect.top + rect.height / 2) {
|
|
4533
|
+
parent.insertBefore(block, rootCodeBlock.nextSibling);
|
|
4534
|
+
}
|
|
4535
|
+
else {
|
|
4536
|
+
parent.insertBefore(block, rootCodeBlock);
|
|
4537
|
+
}
|
|
4538
|
+
return true;
|
|
4539
|
+
}
|
|
4540
|
+
}
|
|
3574
4541
|
const draggedImage = getImageFromMovableBlock(block);
|
|
3575
4542
|
const targetCell = draggedImage ? getClosestCell(under) : null;
|
|
3576
4543
|
if (draggedImage && targetCell && !block.contains(targetCell)) {
|
|
@@ -3601,6 +4568,78 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3601
4568
|
if (range && editor.contains(range.commonAncestorContainer)) {
|
|
3602
4569
|
if (block.contains(range.commonAncestorContainer))
|
|
3603
4570
|
return false;
|
|
4571
|
+
if (draggedQuote) {
|
|
4572
|
+
const element = range.commonAncestorContainer instanceof HTMLElement
|
|
4573
|
+
? range.commonAncestorContainer
|
|
4574
|
+
: range.commonAncestorContainer.parentElement;
|
|
4575
|
+
const container = element?.closest("p,h1,h2,h3,h4,h5,h6,pre");
|
|
4576
|
+
if (container?.parentElement) {
|
|
4577
|
+
const isEmpty = !container.textContent?.trim() &&
|
|
4578
|
+
Array.from(container.children).every((child) => child.tagName === "BR");
|
|
4579
|
+
if (isEmpty) {
|
|
4580
|
+
container.parentElement.insertBefore(block, container);
|
|
4581
|
+
container.remove();
|
|
4582
|
+
}
|
|
4583
|
+
else {
|
|
4584
|
+
const rect = container.getBoundingClientRect();
|
|
4585
|
+
container.parentElement.insertBefore(block, y < rect.top + rect.height / 2 ? container : container.nextSibling);
|
|
4586
|
+
}
|
|
4587
|
+
return true;
|
|
4588
|
+
}
|
|
4589
|
+
}
|
|
4590
|
+
if (draggedCodeBlock) {
|
|
4591
|
+
const element = range.commonAncestorContainer instanceof HTMLElement
|
|
4592
|
+
? range.commonAncestorContainer
|
|
4593
|
+
: range.commonAncestorContainer.parentElement;
|
|
4594
|
+
const codeTarget = element?.closest("pre");
|
|
4595
|
+
if (codeTarget?.parentElement) {
|
|
4596
|
+
let rootCodeBlock = codeTarget;
|
|
4597
|
+
while (rootCodeBlock.parentElement?.closest("pre")) {
|
|
4598
|
+
rootCodeBlock = rootCodeBlock.parentElement.closest("pre");
|
|
4599
|
+
}
|
|
4600
|
+
rootCodeBlock.parentElement.insertBefore(block, rootCodeBlock.nextSibling);
|
|
4601
|
+
return true;
|
|
4602
|
+
}
|
|
4603
|
+
const container = element?.closest("p,h1,h2,h3,h4,h5,h6,blockquote");
|
|
4604
|
+
if (container?.parentElement) {
|
|
4605
|
+
const isEmpty = !container.textContent?.trim() &&
|
|
4606
|
+
Array.from(container.children).every((child) => child.tagName === "BR");
|
|
4607
|
+
if (isEmpty) {
|
|
4608
|
+
container.parentElement.insertBefore(block, container);
|
|
4609
|
+
container.remove();
|
|
4610
|
+
}
|
|
4611
|
+
else {
|
|
4612
|
+
const rect = container.getBoundingClientRect();
|
|
4613
|
+
container.parentElement.insertBefore(block, y < rect.top + rect.height / 2 ? container : container.nextSibling);
|
|
4614
|
+
}
|
|
4615
|
+
return true;
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
4618
|
+
const isTableBlock = block.matches("table,[data-table-wrapper='true']") || Boolean(block.querySelector("table"));
|
|
4619
|
+
if (isTableBlock) {
|
|
4620
|
+
const element = range.commonAncestorContainer instanceof HTMLElement
|
|
4621
|
+
? range.commonAncestorContainer
|
|
4622
|
+
: range.commonAncestorContainer.parentElement;
|
|
4623
|
+
const codeBlock = element?.closest("pre");
|
|
4624
|
+
if (codeBlock?.parentElement) {
|
|
4625
|
+
codeBlock.parentElement.insertBefore(block, codeBlock.nextSibling);
|
|
4626
|
+
return true;
|
|
4627
|
+
}
|
|
4628
|
+
const listItem = element?.closest("li");
|
|
4629
|
+
if (listItem) {
|
|
4630
|
+
let outerList = listItem.parentElement;
|
|
4631
|
+
while (outerList?.parentElement?.tagName === "LI") {
|
|
4632
|
+
const parentList = outerList.parentElement.parentElement;
|
|
4633
|
+
if (!parentList || !["UL", "OL"].includes(parentList.tagName))
|
|
4634
|
+
break;
|
|
4635
|
+
outerList = parentList;
|
|
4636
|
+
}
|
|
4637
|
+
if (outerList?.parentElement) {
|
|
4638
|
+
outerList.parentElement.insertBefore(block, outerList.nextSibling);
|
|
4639
|
+
return true;
|
|
4640
|
+
}
|
|
4641
|
+
}
|
|
4642
|
+
}
|
|
3604
4643
|
range.insertNode(block);
|
|
3605
4644
|
return true;
|
|
3606
4645
|
}
|
|
@@ -3671,13 +4710,65 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3671
4710
|
};
|
|
3672
4711
|
const elementSibling = (element, direction) => {
|
|
3673
4712
|
let sibling = direction === "previous" ? element.previousSibling : element.nextSibling;
|
|
3674
|
-
while (sibling &&
|
|
4713
|
+
while (sibling &&
|
|
4714
|
+
((sibling.nodeType === Node.TEXT_NODE && !sibling.textContent?.trim()) ||
|
|
4715
|
+
(sibling instanceof HTMLElement && sibling.getAttribute("data-srte-caret-boundary") === "true"))) {
|
|
3675
4716
|
sibling = direction === "previous" ? sibling.previousSibling : sibling.nextSibling;
|
|
3676
4717
|
}
|
|
3677
4718
|
return sibling;
|
|
3678
4719
|
};
|
|
4720
|
+
const moveSelectedBlocks = (direction) => {
|
|
4721
|
+
const editor = editableRef.current;
|
|
4722
|
+
const range = getSelectionRangeInEditor();
|
|
4723
|
+
if (!editor || !range || range.collapsed)
|
|
4724
|
+
return false;
|
|
4725
|
+
const blocks = sortInDocumentOrder(getSelectedBlocks(range)).filter((block) => {
|
|
4726
|
+
if (!editor.contains(block) || !block.parentElement)
|
|
4727
|
+
return false;
|
|
4728
|
+
const parentBlock = block.parentElement.closest(blockSelector);
|
|
4729
|
+
return !parentBlock || parentBlock === editor;
|
|
4730
|
+
});
|
|
4731
|
+
if (blocks.length === 0)
|
|
4732
|
+
return false;
|
|
4733
|
+
const parent = blocks[0].parentElement;
|
|
4734
|
+
if (!parent || blocks.some((block) => block.parentElement !== parent))
|
|
4735
|
+
return false;
|
|
4736
|
+
pushEditorHistory();
|
|
4737
|
+
if (direction === "up") {
|
|
4738
|
+
const previous = elementSibling(blocks[0], "previous");
|
|
4739
|
+
if (previous && !blocks.includes(previous)) {
|
|
4740
|
+
blocks.forEach((block) => parent.insertBefore(block, previous));
|
|
4741
|
+
}
|
|
4742
|
+
}
|
|
4743
|
+
else if (direction === "down") {
|
|
4744
|
+
const next = elementSibling(blocks[blocks.length - 1], "next");
|
|
4745
|
+
if (next && !blocks.includes(next)) {
|
|
4746
|
+
const afterNext = next.nextSibling;
|
|
4747
|
+
blocks.forEach((block) => parent.insertBefore(block, afterNext));
|
|
4748
|
+
}
|
|
4749
|
+
}
|
|
4750
|
+
else {
|
|
4751
|
+
blocks.forEach((block) => {
|
|
4752
|
+
const current = parseInt(block.style.marginLeft || "0", 10) || 0;
|
|
4753
|
+
const nextMargin = direction === "right"
|
|
4754
|
+
? Math.min(current + 24, 240)
|
|
4755
|
+
: Math.max(current - 24, 0);
|
|
4756
|
+
block.style.marginLeft = nextMargin ? `${nextMargin}px` : "";
|
|
4757
|
+
});
|
|
4758
|
+
}
|
|
4759
|
+
const movedRange = document.createRange();
|
|
4760
|
+
movedRange.setStartBefore(blocks[0]);
|
|
4761
|
+
movedRange.setEndAfter(blocks[blocks.length - 1]);
|
|
4762
|
+
safeSelectRange(movedRange);
|
|
4763
|
+
savedRangeRef.current = movedRange.cloneRange();
|
|
4764
|
+
handleInput();
|
|
4765
|
+
requestAnimationFrame(updateActiveState);
|
|
4766
|
+
return true;
|
|
4767
|
+
};
|
|
3679
4768
|
const moveCurrentElement = (direction) => {
|
|
3680
4769
|
const editor = editableRef.current;
|
|
4770
|
+
if (moveSelectedBlocks(direction))
|
|
4771
|
+
return;
|
|
3681
4772
|
let target = getMoveTarget();
|
|
3682
4773
|
if (!editor || !target)
|
|
3683
4774
|
return;
|
|
@@ -3859,7 +4950,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3859
4950
|
} }), _jsx("input", { ref: mdInputRef, type: "file", accept: ".md,.markdown,text/markdown,text/plain", style: { display: "none" }, onChange: (e) => {
|
|
3860
4951
|
importTextFile(e.currentTarget.files, "md");
|
|
3861
4952
|
e.currentTarget.value = "";
|
|
3862
|
-
} }), _jsxs("select", { value: currentBlockType, onMouseDown: preserveEditorSelection, onChange: (e) => {
|
|
4953
|
+
} }), _jsxs("select", { value: currentBlockType, onPointerDown: preserveEditorSelection, onMouseDown: preserveEditorSelection, onChange: (e) => {
|
|
3863
4954
|
const val = e.target.value;
|
|
3864
4955
|
if (val === "p")
|
|
3865
4956
|
applyFormatBlock("<p>");
|
|
@@ -3869,6 +4960,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3869
4960
|
applyFormatBlock("<h2>");
|
|
3870
4961
|
else if (val === "h3")
|
|
3871
4962
|
applyFormatBlock("<h3>");
|
|
4963
|
+
else if (val === "h4")
|
|
4964
|
+
applyFormatBlock("<h4>");
|
|
4965
|
+
else if (val === "h5")
|
|
4966
|
+
applyFormatBlock("<h5>");
|
|
4967
|
+
else if (val === "h6")
|
|
4968
|
+
applyFormatBlock("<h6>");
|
|
3872
4969
|
}, title: "Paragraph/Heading", style: {
|
|
3873
4970
|
height: 32,
|
|
3874
4971
|
padding: "0 8px",
|
|
@@ -3876,24 +4973,23 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3876
4973
|
borderRadius: 6,
|
|
3877
4974
|
background: "var(--srte-input-bg)",
|
|
3878
4975
|
color: "var(--srte-input-text)",
|
|
3879
|
-
}, children: [_jsx("option", { value: "
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
}, onChange: (e) => applyFontSize(e.target.value), title: "Font Size", style: {
|
|
4976
|
+
}, 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" })] }), [
|
|
4977
|
+
["left", "Left", "Align left"],
|
|
4978
|
+
["center", "Center", "Align center"],
|
|
4979
|
+
["right", "Right", "Align right"],
|
|
4980
|
+
["justify", "Justify", "Justify"],
|
|
4981
|
+
].map(([alignment, label, title]) => (_jsx("button", { type: "button", title: title, "aria-label": title, "aria-pressed": currentAlignment === alignment, onPointerDown: preserveEditorSelection, onMouseDown: preserveEditorSelection, onClick: () => applyTextAlignment(alignment), style: activeButtonStyle(currentAlignment === alignment, {
|
|
4982
|
+
minWidth: 32,
|
|
4983
|
+
padding: "0 6px",
|
|
4984
|
+
fontSize: 10,
|
|
4985
|
+
}), children: label }, alignment))), _jsx("button", { title: "Bold", onClick: () => exec("bold"), "aria-pressed": activeState.bold, style: activeButtonStyle(activeState.bold), children: _jsx("span", { style: { fontWeight: 700 }, children: "B" }) }), _jsx("button", { title: "Italic", onClick: () => exec("italic"), "aria-pressed": activeState.italic, style: activeButtonStyle(activeState.italic, { fontStyle: "italic" }), children: "I" }), _jsx("button", { title: "Underline", onClick: () => exec("underline"), "aria-pressed": activeState.underline, style: activeButtonStyle(activeState.underline, { textDecoration: "underline" }), children: "U" }), _jsx("button", { title: "Strikethrough", onClick: () => exec("strikeThrough"), "aria-pressed": activeState.strikeThrough, style: activeButtonStyle(activeState.strikeThrough, { textDecoration: "line-through" }), children: "S" }), showFontSize && (_jsxs("select", { value: currentFontSize, onPointerDown: preserveEditorSelection, onMouseDown: preserveEditorSelection, onChange: (e) => applyFontSize(e.target.value), title: "Font Size", style: {
|
|
3890
4986
|
height: 32,
|
|
3891
4987
|
padding: "0 8px",
|
|
3892
4988
|
border: "1px solid var(--srte-input-border)",
|
|
3893
4989
|
borderRadius: 6,
|
|
3894
4990
|
background: "var(--srte-input-bg)",
|
|
3895
4991
|
color: "var(--srte-input-text)",
|
|
3896
|
-
}, children: [_jsx("option", { value: "", disabled: true, children: "Size" }), _jsx("option", { value: "8", children: "8" }), _jsx("option", { value: "9", children: "9" }), _jsx("option", { value: "10", children: "10" }), _jsx("option", { value: "11", children: "11" }), _jsx("option", { value: "12", children: "12" }), _jsx("option", { value: "14", children: "14" }), _jsx("option", { value: "18", children: "18" }), _jsx("option", { value: "24", children: "24" }), _jsx("option", { value: "30", children: "30" }), _jsx("option", { value: "36", children: "36" }), _jsx("option", { value: "48", children: "48" }), _jsx("option", { value: "60", children: "60" }), _jsx("option", { value: "72", children: "72" }), _jsx("option", { value: "96", children: "96" })] })), preserveFontFamily && (_jsxs("select", { value: currentFont, onMouseDown: () => {
|
|
4992
|
+
}, 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: () => {
|
|
3897
4993
|
const sel = window.getSelection();
|
|
3898
4994
|
if (sel && sel.rangeCount > 0) {
|
|
3899
4995
|
const range = sel.getRangeAt(0);
|
|
@@ -3933,20 +5029,50 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3933
5029
|
borderRadius: 6,
|
|
3934
5030
|
background: "var(--srte-input-bg)",
|
|
3935
5031
|
color: "var(--srte-input-text)",
|
|
3936
|
-
}, children: _jsx("span", { style: { fontWeight: 700, padding: "1px 4px", background: "var(--srte-accent-bg)", borderRadius: 3 }, children: "A" }) }), _jsxs("button", { title: "Subscript", onClick: () => toggleInlineScript("subscript"), "aria-pressed": activeState.subscript, style: activeButtonStyle(activeState.subscript), children: ["X", _jsx("sub", { children: "2" })] }), _jsxs("button", { title: "Superscript", onClick: () => toggleInlineScript("superscript"), "aria-pressed": activeState.superscript, style: activeButtonStyle(activeState.superscript), children: ["X", _jsx("sup", { children: "2" })] }),
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
5032
|
+
}, children: _jsx("span", { style: { fontWeight: 700, padding: "1px 4px", background: "var(--srte-accent-bg)", borderRadius: 3 }, children: "A" }) }), _jsxs("button", { title: "Subscript", onMouseDown: preserveEditorSelection, onClick: () => toggleInlineScript("subscript"), "aria-pressed": activeState.subscript, style: activeButtonStyle(activeState.subscript), children: ["X", _jsx("sub", { children: "2" })] }), _jsxs("button", { title: "Superscript", onMouseDown: preserveEditorSelection, onClick: () => toggleInlineScript("superscript"), "aria-pressed": activeState.superscript, style: activeButtonStyle(activeState.superscript), children: ["X", _jsx("sup", { children: "2" })] }), [
|
|
5033
|
+
{
|
|
5034
|
+
key: "check",
|
|
5035
|
+
label: "☐",
|
|
5036
|
+
title: "Checklist",
|
|
5037
|
+
active: activeState.checklist,
|
|
5038
|
+
action: () => applyChecklist(false, true),
|
|
5039
|
+
options: [["check:plain", "☐ Checklist"], ["check:strike", "☑ Checked + strike"]],
|
|
5040
|
+
},
|
|
5041
|
+
{
|
|
5042
|
+
key: "bullet",
|
|
5043
|
+
label: "•≡",
|
|
5044
|
+
title: "Bulleted list",
|
|
5045
|
+
active: activeState.unorderedList,
|
|
5046
|
+
action: () => applyListStyle("bullet:disc"),
|
|
5047
|
+
options: [["bullet:disc", "• Disc"], ["bullet:circle", "○ Circle"], ["bullet:square", "▪ Square"]],
|
|
5048
|
+
},
|
|
5049
|
+
{
|
|
5050
|
+
key: "ordered",
|
|
5051
|
+
label: "1≡",
|
|
5052
|
+
title: "Numbered list",
|
|
5053
|
+
active: activeState.orderedList,
|
|
5054
|
+
action: () => applyListStyle("ordered:decimal"),
|
|
5055
|
+
options: [["ordered:decimal", "1. 2. 3."], ["ordered:lower-alpha", "a. b. c."], ["ordered:upper-alpha", "A. B. C."], ["ordered:lower-roman", "i. ii. iii."], ["ordered:upper-roman", "I. II. III."]],
|
|
5056
|
+
},
|
|
5057
|
+
].map((control) => (_jsxs("span", { style: { display: "inline-flex", height: 32 }, children: [_jsx("button", { type: "button", title: control.title, onPointerDown: preserveEditorSelection, onClick: control.action, "aria-pressed": control.active, style: activeButtonStyle(control.active, { padding: "0 9px", borderRadius: "6px 0 0 6px" }), children: control.label }), _jsxs("select", { defaultValue: "", "aria-label": `${control.title} styles`, title: `${control.title} styles`, onPointerDown: preserveEditorSelection, onMouseDown: preserveEditorSelection, onChange: (event) => {
|
|
5058
|
+
const selected = event.currentTarget.value;
|
|
5059
|
+
if (selected === "check:plain")
|
|
5060
|
+
applyChecklist(false);
|
|
5061
|
+
else if (selected === "check:strike")
|
|
5062
|
+
applyChecklist(true);
|
|
5063
|
+
else if (selected)
|
|
5064
|
+
applyListStyle(selected);
|
|
5065
|
+
event.currentTarget.value = "";
|
|
5066
|
+
}, style: {
|
|
5067
|
+
width: 28,
|
|
5068
|
+
height: 32,
|
|
5069
|
+
padding: 0,
|
|
5070
|
+
border: "1px solid var(--srte-input-border)",
|
|
5071
|
+
borderLeft: 0,
|
|
5072
|
+
borderRadius: "0 6px 6px 0",
|
|
5073
|
+
background: "var(--srte-input-bg)",
|
|
5074
|
+
color: "var(--srte-input-text)",
|
|
5075
|
+
}, 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", title: "Blockquote", onPointerDown: preserveEditorSelection, onClick: toggleBlockquote, "aria-pressed": activeState.blockquote, style: activeButtonStyle(activeState.blockquote), children: "\u275D" }), _jsx("button", { title: "Special characters", onClick: () => setShowSpecialChars(true), style: {
|
|
3950
5076
|
height: 32,
|
|
3951
5077
|
minWidth: 32,
|
|
3952
5078
|
padding: "0 8px",
|
|
@@ -3954,7 +5080,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3954
5080
|
borderRadius: 6,
|
|
3955
5081
|
background: "var(--srte-input-bg)",
|
|
3956
5082
|
color: "var(--srte-input-text)",
|
|
3957
|
-
}, children: "\u03A9" }), _jsx("button", { title: "Code block",
|
|
5083
|
+
}, children: "\u03A9" }), _jsx("button", { type: "button", title: "Code block", onPointerDown: preserveEditorSelection, onClick: toggleCodeBlock, "aria-pressed": activeState.codeBlock, style: activeButtonStyle(activeState.codeBlock, {
|
|
3958
5084
|
minWidth: 36,
|
|
3959
5085
|
fontFamily: "ui-monospace, SFMono-Regular, Menlo",
|
|
3960
5086
|
}), children: "< />" }), formula && (_jsx("button", { title: "Insert formula", onClick: () => setShowFormulaDialog(true), style: {
|
|
@@ -3965,21 +5091,18 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3965
5091
|
borderRadius: 6,
|
|
3966
5092
|
background: "var(--srte-input-bg)",
|
|
3967
5093
|
color: "var(--srte-input-text)",
|
|
3968
|
-
}, children: "\u2211" })), _jsx("button", { title: "Insert link", onClick:
|
|
3969
|
-
height: 32,
|
|
3970
|
-
padding: "0 10px",
|
|
3971
|
-
border: "1px solid var(--srte-input-border)",
|
|
3972
|
-
borderRadius: 6,
|
|
3973
|
-
background: "var(--srte-input-bg)",
|
|
3974
|
-
color: "var(--srte-input-text)",
|
|
3975
|
-
}, children: "Link" }), _jsx("button", { title: "Remove link", onClick: () => exec("unlink"), style: {
|
|
5094
|
+
}, children: "\u2211" })), _jsx("button", { type: "button", title: "Insert link", "aria-label": "Insert or edit link", "aria-pressed": activeState.link, onPointerDown: preserveEditorSelection, onClick: () => openLinkEditor(), style: activeButtonStyle(activeState.link, { minWidth: 34, fontSize: 17 }), children: _jsx("span", { "aria-hidden": "true", children: "\u2197" }) }), _jsx("button", { type: "button", title: "Remove link", "aria-label": "Remove link", disabled: !activeState.link, onPointerDown: preserveEditorSelection, onClick: () => exec("unlink"), style: {
|
|
3976
5095
|
height: 32,
|
|
3977
|
-
|
|
5096
|
+
minWidth: 34,
|
|
5097
|
+
padding: "0 8px",
|
|
3978
5098
|
border: "1px solid var(--srte-input-border)",
|
|
3979
5099
|
borderRadius: 6,
|
|
3980
5100
|
background: "var(--srte-input-bg)",
|
|
3981
5101
|
color: "var(--srte-input-text)",
|
|
3982
|
-
|
|
5102
|
+
cursor: activeState.link ? "pointer" : "not-allowed",
|
|
5103
|
+
opacity: activeState.link ? 1 : 0.45,
|
|
5104
|
+
fontSize: 16,
|
|
5105
|
+
}, children: _jsx("span", { "aria-hidden": "true", children: "\u2197\u0338" }) }), media && (_jsxs(_Fragment, { children: [_jsx("button", { title: "Insert image", onClick: insertImage, style: {
|
|
3983
5106
|
height: 32,
|
|
3984
5107
|
padding: "0 10px",
|
|
3985
5108
|
border: "1px solid var(--srte-input-border)",
|
|
@@ -4483,8 +5606,14 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
4483
5606
|
}, onMouseMove: (e) => {
|
|
4484
5607
|
if (draggedBlockRef.current)
|
|
4485
5608
|
return;
|
|
4486
|
-
updateDragHandleForTarget(getMovableElementFromNode(e.target));
|
|
4487
|
-
},
|
|
5609
|
+
updateDragHandleForTarget(isNode(e.target) ? getMovableElementFromNode(e.target) : null);
|
|
5610
|
+
}, onMouseOver: (e) => {
|
|
5611
|
+
if (draggedBlockRef.current)
|
|
5612
|
+
return;
|
|
5613
|
+
updateDragHandleForTarget(isNode(e.target) ? getMovableElementFromNode(e.target) : null);
|
|
5614
|
+
}, onMouseLeave: (e) => {
|
|
5615
|
+
if (closestFromTarget(e.relatedTarget, "[data-srte-drag-handle]"))
|
|
5616
|
+
return;
|
|
4488
5617
|
if (!draggedBlockRef.current)
|
|
4489
5618
|
scheduleDragHandleHide();
|
|
4490
5619
|
}, onPaste: (e) => {
|
|
@@ -4580,13 +5709,31 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
4580
5709
|
}
|
|
4581
5710
|
}, onClick: (e) => {
|
|
4582
5711
|
const t = e.target;
|
|
5712
|
+
if (t.dataset.srteCheck === "true") {
|
|
5713
|
+
const item = t.closest("li");
|
|
5714
|
+
const list = item?.closest('[data-srte-checklist="true"]');
|
|
5715
|
+
if (item && list) {
|
|
5716
|
+
pushEditorHistory();
|
|
5717
|
+
const checked = item.dataset.checked !== "true";
|
|
5718
|
+
item.dataset.checked = checked ? "true" : "false";
|
|
5719
|
+
t.textContent = checked ? "☑" : "☐";
|
|
5720
|
+
t.setAttribute("aria-label", checked ? "Mark incomplete" : "Mark complete");
|
|
5721
|
+
item.style.textDecoration =
|
|
5722
|
+
list.dataset.srteChecklistStrike === "true" && checked ? "line-through" : "";
|
|
5723
|
+
handleInput();
|
|
5724
|
+
return;
|
|
5725
|
+
}
|
|
5726
|
+
}
|
|
4583
5727
|
const anchor = t?.closest("a");
|
|
4584
5728
|
if (anchor && editableRef.current?.contains(anchor)) {
|
|
4585
5729
|
e.preventDefault();
|
|
4586
5730
|
e.stopPropagation();
|
|
4587
|
-
|
|
5731
|
+
openLinkEditor(anchor);
|
|
5732
|
+
setTableMenu(null);
|
|
5733
|
+
setImageMenu(null);
|
|
4588
5734
|
return;
|
|
4589
5735
|
}
|
|
5736
|
+
setLinkMenu(null);
|
|
4590
5737
|
if (t && t.tagName === "IMG") {
|
|
4591
5738
|
setSelectedImage(t);
|
|
4592
5739
|
scheduleImageOverlay();
|
|
@@ -4638,6 +5785,11 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
4638
5785
|
}
|
|
4639
5786
|
updateActiveState();
|
|
4640
5787
|
}, onKeyDown: (e) => {
|
|
5788
|
+
if ((e.metaKey || e.ctrlKey) && String(e.key).toLowerCase() === "k") {
|
|
5789
|
+
e.preventDefault();
|
|
5790
|
+
openLinkEditor();
|
|
5791
|
+
return;
|
|
5792
|
+
}
|
|
4641
5793
|
if ((e.metaKey || e.ctrlKey) && String(e.key).toLowerCase() === "z") {
|
|
4642
5794
|
const restored = restoreEditorHistory(e.shiftKey ? "redo" : "undo");
|
|
4643
5795
|
if (restored) {
|
|
@@ -4789,7 +5941,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
4789
5941
|
setTableMenu(null);
|
|
4790
5942
|
setImageMenu(null);
|
|
4791
5943
|
}
|
|
4792
|
-
} }), dragHandle && !readOnly && (_jsx("button", { type: "button", draggable: true, title: "Drag block", "aria-label": "Drag block", onMouseEnter: () => {
|
|
5944
|
+
} }), dragHandle && !readOnly && (_jsx("button", { type: "button", draggable: true, "data-srte-drag-handle": "true", title: "Drag block", "aria-label": "Drag block", onMouseEnter: () => {
|
|
4793
5945
|
if (dragHandleHideTimerRef.current != null) {
|
|
4794
5946
|
window.clearTimeout(dragHandleHideTimerRef.current);
|
|
4795
5947
|
dragHandleHideTimerRef.current = null;
|
|
@@ -4926,7 +6078,10 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
4926
6078
|
borderRadius: 2,
|
|
4927
6079
|
cursor: "ew-resize",
|
|
4928
6080
|
pointerEvents: "auto",
|
|
4929
|
-
} })] }))] }),
|
|
6081
|
+
} })] }))] }), 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 ? () => {
|
|
6082
|
+
openEditorLink(linkMenu.anchor);
|
|
6083
|
+
setLinkMenu(null);
|
|
6084
|
+
} : undefined, onRemove: linkMenu.anchor ? () => removeAnchorLink(linkMenu.anchor) : undefined, onCancel: () => setLinkMenu(null) })), tableMenu && (_jsx("div", { style: {
|
|
4930
6085
|
position: "fixed",
|
|
4931
6086
|
inset: 0,
|
|
4932
6087
|
zIndex: 60,
|