smartrte-react 0.2.8 → 0.3.0
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 +10 -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 +1793 -283
- 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 +76 -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,18 +57,23 @@ 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);
|
|
67
|
+
const inlineScriptCaretOverrideRef = useRef(null);
|
|
58
68
|
const historyRef = useRef({
|
|
59
69
|
undo: [],
|
|
60
70
|
redo: [],
|
|
61
71
|
});
|
|
72
|
+
const inputHistoryGroupRef = useRef(null);
|
|
62
73
|
const [currentFontSize, setCurrentFontSize] = useState("");
|
|
63
74
|
const [currentFont, setCurrentFont] = useState("");
|
|
64
75
|
const [currentBlockType, setCurrentBlockType] = useState("p");
|
|
76
|
+
const [currentAlignment, setCurrentAlignment] = useState("left");
|
|
65
77
|
const [activeState, setActiveState] = useState({
|
|
66
78
|
bold: false,
|
|
67
79
|
italic: false,
|
|
@@ -69,10 +81,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
69
81
|
strikeThrough: false,
|
|
70
82
|
subscript: false,
|
|
71
83
|
superscript: false,
|
|
84
|
+
checklist: false,
|
|
72
85
|
unorderedList: false,
|
|
73
86
|
orderedList: false,
|
|
74
87
|
blockquote: false,
|
|
75
88
|
codeBlock: false,
|
|
89
|
+
link: false,
|
|
76
90
|
});
|
|
77
91
|
useEffect(() => {
|
|
78
92
|
const el = editableRef.current;
|
|
@@ -82,9 +96,17 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
82
96
|
if (typeof value === "string" && value !== el.innerHTML) {
|
|
83
97
|
el.innerHTML = value || "";
|
|
84
98
|
fixNegativeMargins(el);
|
|
99
|
+
normalizeInvalidQuoteNesting(el);
|
|
100
|
+
normalizeInvalidCodeBlockNesting(el);
|
|
101
|
+
normalizeInvalidTableNesting(el);
|
|
85
102
|
ensureTableWrappers(el);
|
|
103
|
+
ensureCaretBoundaryParagraphs(el);
|
|
86
104
|
addTableResizeHandles();
|
|
87
105
|
}
|
|
106
|
+
normalizeInvalidQuoteNesting(el);
|
|
107
|
+
normalizeInvalidCodeBlockNesting(el);
|
|
108
|
+
normalizeInvalidTableNesting(el);
|
|
109
|
+
ensureCaretBoundaryParagraphs(el);
|
|
88
110
|
// Suppress native context menu inside table cells at capture phase
|
|
89
111
|
const onCtx = (evt) => {
|
|
90
112
|
const target = evt.target;
|
|
@@ -98,6 +120,83 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
98
120
|
el.removeEventListener("contextmenu", onCtx, { capture: true });
|
|
99
121
|
};
|
|
100
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
|
+
};
|
|
101
200
|
const updateActiveState = () => {
|
|
102
201
|
const editor = editableRef.current;
|
|
103
202
|
if (!editor)
|
|
@@ -114,18 +213,59 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
114
213
|
const element = node instanceof HTMLElement ? node : null;
|
|
115
214
|
const block = element?.closest("p,h1,h2,h3,h4,h5,h6,li,blockquote,pre,div");
|
|
116
215
|
const tag = block?.tagName.toLowerCase();
|
|
117
|
-
|
|
216
|
+
const queryState = (command) => typeof document.queryCommandState === "function" && document.queryCommandState(command);
|
|
217
|
+
const scriptOverride = inlineScriptCaretOverrideRef.current;
|
|
218
|
+
const overrideApplies = Boolean(scriptOverride &&
|
|
219
|
+
range.collapsed &&
|
|
220
|
+
range.startContainer === scriptOverride.container &&
|
|
221
|
+
range.startOffset === scriptOverride.offset);
|
|
222
|
+
if (scriptOverride && !overrideApplies) {
|
|
223
|
+
inlineScriptCaretOverrideRef.current = null;
|
|
224
|
+
}
|
|
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");
|
|
118
252
|
setActiveState({
|
|
119
|
-
bold:
|
|
120
|
-
italic:
|
|
121
|
-
underline:
|
|
122
|
-
strikeThrough:
|
|
123
|
-
subscript:
|
|
124
|
-
|
|
125
|
-
|
|
253
|
+
bold: queryState("bold"),
|
|
254
|
+
italic: queryState("italic"),
|
|
255
|
+
underline: queryState("underline"),
|
|
256
|
+
strikeThrough: queryState("strikeThrough"),
|
|
257
|
+
subscript: overrideApplies && scriptOverride?.command === "subscript"
|
|
258
|
+
? false
|
|
259
|
+
: subscriptActive,
|
|
260
|
+
superscript: overrideApplies && scriptOverride?.command === "superscript"
|
|
261
|
+
? false
|
|
262
|
+
: superscriptActive,
|
|
263
|
+
checklist: Boolean(element?.closest('[data-srte-checklist="true"]')),
|
|
264
|
+
unorderedList: Boolean(element?.closest("ul:not([data-srte-checklist=\"true\"])")),
|
|
126
265
|
orderedList: Boolean(element?.closest("ol")),
|
|
127
266
|
blockquote: Boolean(element?.closest("blockquote")),
|
|
128
267
|
codeBlock: Boolean(element?.closest("pre")),
|
|
268
|
+
link: Boolean(element?.closest("a")),
|
|
129
269
|
});
|
|
130
270
|
}
|
|
131
271
|
catch { }
|
|
@@ -138,6 +278,10 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
138
278
|
const range = sel.getRangeAt(0);
|
|
139
279
|
const editor = editableRef.current;
|
|
140
280
|
if (editor && editor.contains(range.commonAncestorContainer)) {
|
|
281
|
+
const pending = pendingFontSizeRef.current;
|
|
282
|
+
if (pending &&
|
|
283
|
+
(!range.collapsed || range.startContainer !== pending.container || range.startOffset !== pending.offset))
|
|
284
|
+
pendingFontSizeRef.current = null;
|
|
141
285
|
savedRangeRef.current = range.cloneRange();
|
|
142
286
|
updateActiveState();
|
|
143
287
|
}
|
|
@@ -165,6 +309,53 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
165
309
|
toggleList("ol");
|
|
166
310
|
return;
|
|
167
311
|
}
|
|
312
|
+
pushEditorHistory();
|
|
313
|
+
const editor = editableRef.current;
|
|
314
|
+
const coreMark = { bold: "bold", italic: "italic", underline: "underline", superscript: "superscript", subscript: "subscript" }[command];
|
|
315
|
+
if (coreMark && editor && isCoreInlineMarkEnabled(coreMark)) {
|
|
316
|
+
try {
|
|
317
|
+
const result = getCoreInlineMarkResult(editor, coreMark);
|
|
318
|
+
if (result) {
|
|
319
|
+
editor.innerHTML = result.html;
|
|
320
|
+
ensureTableWrappers(editor);
|
|
321
|
+
addTableResizeHandles();
|
|
322
|
+
if (!restoreSelectionToDom(editor, result.selectionAfter)) {
|
|
323
|
+
const fallback = document.createRange();
|
|
324
|
+
fallback.selectNodeContents(editor);
|
|
325
|
+
fallback.collapse(false);
|
|
326
|
+
safeSelectRange(fallback);
|
|
327
|
+
if (isShadowModeEnabled())
|
|
328
|
+
console.warn(`[Smart RTE] Core ${coreMark} could not restore its exact selection.`);
|
|
329
|
+
}
|
|
330
|
+
handleInput();
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
if (isShadowModeEnabled())
|
|
336
|
+
console.warn(`[Smart RTE] Core ${coreMark} fell back to legacy execution.`, error);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const shadowCommand = {
|
|
340
|
+
bold: toggleBold,
|
|
341
|
+
italic: toggleItalic,
|
|
342
|
+
underline: toggleUnderline,
|
|
343
|
+
superscript: toggleSuperscript,
|
|
344
|
+
subscript: toggleSubscript,
|
|
345
|
+
foreColor: coreApplyTextColor,
|
|
346
|
+
createLink: applyLink,
|
|
347
|
+
unlink: removeLink,
|
|
348
|
+
}[command];
|
|
349
|
+
const shadowInput = command === "createLink" ? { href: valueArg || "" } : valueArg;
|
|
350
|
+
const shadowState = shadowCommand && editor && isShadowModeEnabled()
|
|
351
|
+
? (() => {
|
|
352
|
+
const selection = selectionFromDom(editor, window.getSelection());
|
|
353
|
+
if (!selection)
|
|
354
|
+
return null;
|
|
355
|
+
const { document: coreDocument } = smartDocumentFromEditorRoot(editor);
|
|
356
|
+
return { document: coreDocument, selection };
|
|
357
|
+
})()
|
|
358
|
+
: null;
|
|
168
359
|
const beforeHtml = editableRef.current?.innerHTML || "";
|
|
169
360
|
const ok = document.execCommand(command, false, valueArg);
|
|
170
361
|
const afterHtml = editableRef.current?.innerHTML || "";
|
|
@@ -174,25 +365,103 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
174
365
|
if (command === "formatBlock" && valueArg)
|
|
175
366
|
applyFormatBlockFallback(valueArg);
|
|
176
367
|
}
|
|
368
|
+
if (shadowCommand && shadowState) {
|
|
369
|
+
runShadowCommand({
|
|
370
|
+
command: shadowCommand,
|
|
371
|
+
context: { document: shadowState.document, selection: shadowState.selection },
|
|
372
|
+
input: shadowInput,
|
|
373
|
+
state: shadowState,
|
|
374
|
+
legacyHtml: afterHtml,
|
|
375
|
+
serialize: (state) => serializeSmartDocument(state.document),
|
|
376
|
+
});
|
|
377
|
+
}
|
|
177
378
|
handleInput();
|
|
178
379
|
}
|
|
179
380
|
catch { }
|
|
180
381
|
};
|
|
382
|
+
const getScriptAncestor = (node, tagName) => {
|
|
383
|
+
const editor = editableRef.current;
|
|
384
|
+
let element = node instanceof HTMLElement ? node : node?.parentElement || null;
|
|
385
|
+
while (element && element !== editor) {
|
|
386
|
+
if (element.tagName.toLowerCase() === tagName)
|
|
387
|
+
return element;
|
|
388
|
+
element = element.parentElement;
|
|
389
|
+
}
|
|
390
|
+
return null;
|
|
391
|
+
};
|
|
392
|
+
const toggleInlineScript = (command) => {
|
|
393
|
+
try {
|
|
394
|
+
if (!restoreSavedSelection()) {
|
|
395
|
+
safeSelectRange(getSelectionRangeInEditor());
|
|
396
|
+
}
|
|
397
|
+
const editor = editableRef.current;
|
|
398
|
+
const range = getSelectionRangeInEditor();
|
|
399
|
+
if (!editor || !range)
|
|
400
|
+
return;
|
|
401
|
+
const tagName = command === "subscript" ? "sub" : "sup";
|
|
402
|
+
const script = range.collapsed
|
|
403
|
+
? getScriptAncestor(range.startContainer, tagName)
|
|
404
|
+
: null;
|
|
405
|
+
if (range.collapsed && document.queryCommandState(command)) {
|
|
406
|
+
document.execCommand(command, false);
|
|
407
|
+
const currentRange = getSelectionRangeInEditor();
|
|
408
|
+
savedRangeRef.current = currentRange?.cloneRange() || null;
|
|
409
|
+
inlineScriptCaretOverrideRef.current = currentRange?.collapsed
|
|
410
|
+
? {
|
|
411
|
+
command,
|
|
412
|
+
container: currentRange.startContainer,
|
|
413
|
+
offset: currentRange.startOffset,
|
|
414
|
+
}
|
|
415
|
+
: null;
|
|
416
|
+
setActiveState((current) => ({
|
|
417
|
+
...current,
|
|
418
|
+
subscript: command === "subscript" ? false : current.subscript,
|
|
419
|
+
superscript: command === "superscript" ? false : current.superscript,
|
|
420
|
+
}));
|
|
421
|
+
handleInput();
|
|
422
|
+
requestAnimationFrame(updateActiveState);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (script) {
|
|
426
|
+
const nextRange = document.createRange();
|
|
427
|
+
nextRange.setStartAfter(script);
|
|
428
|
+
nextRange.collapse(true);
|
|
429
|
+
safeSelectRange(nextRange);
|
|
430
|
+
savedRangeRef.current = nextRange.cloneRange();
|
|
431
|
+
inlineScriptCaretOverrideRef.current = {
|
|
432
|
+
command,
|
|
433
|
+
container: nextRange.startContainer,
|
|
434
|
+
offset: nextRange.startOffset,
|
|
435
|
+
};
|
|
436
|
+
setActiveState((current) => ({
|
|
437
|
+
...current,
|
|
438
|
+
subscript: command === "subscript" ? false : current.subscript,
|
|
439
|
+
superscript: command === "superscript" ? false : current.superscript,
|
|
440
|
+
}));
|
|
441
|
+
requestAnimationFrame(updateActiveState);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
inlineScriptCaretOverrideRef.current = null;
|
|
445
|
+
exec(command);
|
|
446
|
+
requestAnimationFrame(updateActiveState);
|
|
447
|
+
}
|
|
448
|
+
catch { }
|
|
449
|
+
};
|
|
181
450
|
const applyFormatBlock = (blockName) => {
|
|
182
451
|
try {
|
|
183
452
|
if (!restoreSavedSelection()) {
|
|
184
453
|
safeSelectRange(getSelectionRangeInEditor());
|
|
185
454
|
}
|
|
455
|
+
pushEditorHistory();
|
|
186
456
|
if (applyFormatBlockFallback(blockName)) {
|
|
187
457
|
const tag = normalizeBlockTag(blockName);
|
|
188
|
-
if (tag === "p" || tag
|
|
458
|
+
if (tag === "p" || /^h[1-6]$/.test(tag || "")) {
|
|
189
459
|
setCurrentBlockType(tag);
|
|
190
460
|
}
|
|
191
461
|
handleInput();
|
|
192
462
|
requestAnimationFrame(updateActiveState);
|
|
193
463
|
return;
|
|
194
464
|
}
|
|
195
|
-
exec("formatBlock", blockName);
|
|
196
465
|
}
|
|
197
466
|
catch { }
|
|
198
467
|
};
|
|
@@ -206,10 +475,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
206
475
|
onChange(html);
|
|
207
476
|
}
|
|
208
477
|
};
|
|
209
|
-
const pushEditorHistory = () => {
|
|
478
|
+
const pushEditorHistory = (preserveInputGroup = false) => {
|
|
210
479
|
const editor = editableRef.current;
|
|
211
480
|
if (!editor)
|
|
212
481
|
return;
|
|
482
|
+
if (!preserveInputGroup)
|
|
483
|
+
inputHistoryGroupRef.current = null;
|
|
213
484
|
const selectionCells = selectionRef.current
|
|
214
485
|
? getCellsInGridRect(selectionRef.current.tbody, selectionRef.current.sr, selectionRef.current.sc, selectionRef.current.er, selectionRef.current.ec)
|
|
215
486
|
: [];
|
|
@@ -247,22 +518,49 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
247
518
|
const history = historyRef.current;
|
|
248
519
|
const from = dir === "undo" ? history.undo : history.redo;
|
|
249
520
|
const to = dir === "undo" ? history.redo : history.undo;
|
|
250
|
-
const
|
|
521
|
+
const currentHtml = editor.innerHTML;
|
|
522
|
+
let html;
|
|
523
|
+
while (from.length > 0) {
|
|
524
|
+
const candidate = from.pop();
|
|
525
|
+
if (candidate !== currentHtml) {
|
|
526
|
+
html = candidate;
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
251
530
|
if (html == null)
|
|
252
531
|
return false;
|
|
253
|
-
to.
|
|
532
|
+
if (to[to.length - 1] !== currentHtml)
|
|
533
|
+
to.push(currentHtml);
|
|
254
534
|
editor.innerHTML = html;
|
|
255
535
|
fixNegativeMargins(editor);
|
|
536
|
+
normalizeInvalidCodeBlockNesting(editor);
|
|
256
537
|
ensureTableWrappers(editor);
|
|
538
|
+
ensureCaretBoundaryParagraphs(editor);
|
|
257
539
|
addTableResizeHandles();
|
|
258
540
|
clearSelectionDecor();
|
|
259
541
|
setTableMenu(null);
|
|
542
|
+
inputHistoryGroupRef.current = null;
|
|
543
|
+
focusElementEnd(editor);
|
|
544
|
+
requestAnimationFrame(updateActiveState);
|
|
260
545
|
if (html !== lastEmittedRef.current) {
|
|
261
546
|
lastEmittedRef.current = html;
|
|
262
547
|
onChange?.(html);
|
|
263
548
|
}
|
|
264
549
|
return true;
|
|
265
550
|
};
|
|
551
|
+
const captureInputHistory = (inputType) => {
|
|
552
|
+
const now = Date.now();
|
|
553
|
+
const previous = inputHistoryGroupRef.current;
|
|
554
|
+
const groupable = inputType === "insertText" ||
|
|
555
|
+
inputType === "deleteContentBackward" ||
|
|
556
|
+
inputType === "deleteContentForward";
|
|
557
|
+
const continuesGroup = groupable &&
|
|
558
|
+
previous?.inputType === inputType &&
|
|
559
|
+
now - previous.timestamp < 1000;
|
|
560
|
+
if (!continuesGroup)
|
|
561
|
+
pushEditorHistory(true);
|
|
562
|
+
inputHistoryGroupRef.current = { inputType, timestamp: now };
|
|
563
|
+
};
|
|
266
564
|
const restoreSavedSelection = () => {
|
|
267
565
|
const editor = editableRef.current;
|
|
268
566
|
if (!editor)
|
|
@@ -286,11 +584,119 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
286
584
|
savedRangeRef.current = range.cloneRange();
|
|
287
585
|
}
|
|
288
586
|
};
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
587
|
+
const escapeAttribute = (value) => value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
588
|
+
const getSelectedAnchor = () => {
|
|
589
|
+
const editor = editableRef.current;
|
|
590
|
+
const range = getSelectionRangeInEditor();
|
|
591
|
+
if (!editor || !range)
|
|
592
|
+
return null;
|
|
593
|
+
const node = range.startContainer.nodeType === Node.TEXT_NODE ? range.startContainer.parentElement : range.startContainer;
|
|
594
|
+
const anchor = node?.closest?.("a");
|
|
595
|
+
return anchor && editor.contains(anchor) ? anchor : null;
|
|
596
|
+
};
|
|
597
|
+
const selectAnchorContents = (anchor) => {
|
|
598
|
+
const range = document.createRange();
|
|
599
|
+
range.selectNodeContents(anchor);
|
|
600
|
+
safeSelectRange(range);
|
|
601
|
+
savedRangeRef.current = range.cloneRange();
|
|
602
|
+
};
|
|
603
|
+
const getRangeRect = (range) => {
|
|
604
|
+
if (!range || typeof range.getBoundingClientRect !== "function")
|
|
605
|
+
return null;
|
|
606
|
+
const rect = range.getBoundingClientRect();
|
|
607
|
+
return rect.width || rect.height ? rect : null;
|
|
608
|
+
};
|
|
609
|
+
const openLinkEditor = (existingAnchor) => {
|
|
610
|
+
const editor = editableRef.current;
|
|
611
|
+
if (!editor)
|
|
292
612
|
return;
|
|
293
|
-
|
|
613
|
+
const anchor = existingAnchor || getSelectedAnchor();
|
|
614
|
+
const range = anchor
|
|
615
|
+
? (() => {
|
|
616
|
+
const anchorRange = document.createRange();
|
|
617
|
+
anchorRange.selectNodeContents(anchor);
|
|
618
|
+
return anchorRange;
|
|
619
|
+
})()
|
|
620
|
+
: getSelectionRangeInEditor();
|
|
621
|
+
const rect = anchor?.getBoundingClientRect() || getRangeRect(range) || editor.getBoundingClientRect();
|
|
622
|
+
setLinkMenu({
|
|
623
|
+
x: Math.max(8, Math.min(rect.left, window.innerWidth - 320)),
|
|
624
|
+
y: Math.max(8, Math.min(rect.bottom + 8, window.innerHeight - 180)),
|
|
625
|
+
anchor: anchor || undefined,
|
|
626
|
+
range: range ? range.cloneRange() : null,
|
|
627
|
+
initialHref: anchor?.getAttribute("href") || "",
|
|
628
|
+
initialText: anchor?.textContent || (range && !range.collapsed ? range.toString() : ""),
|
|
629
|
+
showTextInput: Boolean(anchor || !range || range.collapsed),
|
|
630
|
+
});
|
|
631
|
+
};
|
|
632
|
+
const applyLinkEditorValue = (value) => {
|
|
633
|
+
const state = linkMenu;
|
|
634
|
+
if (!state)
|
|
635
|
+
return;
|
|
636
|
+
setLinkMenu(null);
|
|
637
|
+
if (state.anchor) {
|
|
638
|
+
pushEditorHistory();
|
|
639
|
+
state.anchor.setAttribute("href", value.href);
|
|
640
|
+
updateAnchorTarget(state.anchor, value.openInNewTab);
|
|
641
|
+
if (value.text != null && value.text !== state.anchor.textContent) {
|
|
642
|
+
state.anchor.textContent = value.text;
|
|
643
|
+
}
|
|
644
|
+
selectAnchorContents(state.anchor);
|
|
645
|
+
handleInput();
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
if (state.range)
|
|
649
|
+
safeSelectRange(state.range.cloneRange());
|
|
650
|
+
if (state.range && !state.range.collapsed) {
|
|
651
|
+
exec("createLink", value.href);
|
|
652
|
+
const createdAnchor = getSelectedAnchor();
|
|
653
|
+
if (createdAnchor) {
|
|
654
|
+
updateAnchorTarget(createdAnchor, value.openInNewTab);
|
|
655
|
+
handleInput();
|
|
656
|
+
}
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
if (!value.text)
|
|
660
|
+
return;
|
|
661
|
+
pushEditorHistory();
|
|
662
|
+
const targetAttributes = value.openInNewTab ? ' target="_blank" rel="noopener noreferrer"' : "";
|
|
663
|
+
document.execCommand("insertHTML", false, `<a href="${escapeAttribute(value.href)}"${targetAttributes}>${escapeAttribute(value.text)}</a>`);
|
|
664
|
+
handleInput();
|
|
665
|
+
};
|
|
666
|
+
const updateAnchorTarget = (anchor, openInNewTab) => {
|
|
667
|
+
const otherRelValues = (anchor.getAttribute("rel") || "")
|
|
668
|
+
.split(/\s+/)
|
|
669
|
+
.filter((value) => value && value !== "noopener" && value !== "noreferrer");
|
|
670
|
+
if (openInNewTab) {
|
|
671
|
+
anchor.target = "_blank";
|
|
672
|
+
anchor.rel = [...otherRelValues, "noopener", "noreferrer"].join(" ");
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
anchor.removeAttribute("target");
|
|
676
|
+
if (otherRelValues.length)
|
|
677
|
+
anchor.rel = otherRelValues.join(" ");
|
|
678
|
+
else
|
|
679
|
+
anchor.removeAttribute("rel");
|
|
680
|
+
};
|
|
681
|
+
const removeAnchorLink = (anchor) => {
|
|
682
|
+
selectAnchorContents(anchor);
|
|
683
|
+
exec("unlink");
|
|
684
|
+
setLinkMenu(null);
|
|
685
|
+
};
|
|
686
|
+
const openEditorLink = (anchor) => {
|
|
687
|
+
const safeHref = sanitizeLinkHref(anchor.getAttribute("href"));
|
|
688
|
+
if (!safeHref)
|
|
689
|
+
return;
|
|
690
|
+
try {
|
|
691
|
+
const url = new URL(safeHref, window.location.href);
|
|
692
|
+
if (!["http:", "https:", "mailto:", "tel:"].includes(url.protocol))
|
|
693
|
+
return;
|
|
694
|
+
const target = anchor.getAttribute("target") || "_blank";
|
|
695
|
+
const opened = window.open(url.href, target, target === "_blank" ? "noopener,noreferrer" : undefined);
|
|
696
|
+
if (opened && target === "_blank")
|
|
697
|
+
opened.opener = null;
|
|
698
|
+
}
|
|
699
|
+
catch { }
|
|
294
700
|
};
|
|
295
701
|
const getSelectionRangeInEditor = () => {
|
|
296
702
|
const editor = editableRef.current;
|
|
@@ -315,12 +721,22 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
315
721
|
const range = getSelectionRangeInEditor();
|
|
316
722
|
if (!editor || !range)
|
|
317
723
|
return null;
|
|
318
|
-
let node = range.
|
|
724
|
+
let node = range.startContainer;
|
|
319
725
|
if (node.nodeType === Node.TEXT_NODE)
|
|
320
726
|
node = node.parentNode;
|
|
321
727
|
const element = node instanceof HTMLElement ? node : null;
|
|
728
|
+
const cell = element?.closest("td,th");
|
|
729
|
+
if (cell && editor.contains(cell)) {
|
|
730
|
+
const cellBlock = element?.closest("p,h1,h2,h3,h4,h5,h6,blockquote,pre");
|
|
731
|
+
if (cellBlock && cell.contains(cellBlock))
|
|
732
|
+
return cellBlock;
|
|
733
|
+
const directBlock = Array.from(cell.children).find((child) => child.matches("p,h1,h2,h3,h4,h5,h6,blockquote,pre"));
|
|
734
|
+
if (directBlock instanceof HTMLElement)
|
|
735
|
+
return directBlock;
|
|
736
|
+
return cell;
|
|
737
|
+
}
|
|
322
738
|
const block = element?.closest("p,h1,h2,h3,h4,h5,h6,li,blockquote,pre,div");
|
|
323
|
-
if (!block || block === editor || !editor.contains(block))
|
|
739
|
+
if (!block || block === editor || block.getAttribute("data-table-wrapper") === "true" || !editor.contains(block))
|
|
324
740
|
return null;
|
|
325
741
|
return block;
|
|
326
742
|
};
|
|
@@ -329,22 +745,43 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
329
745
|
const editor = editableRef.current;
|
|
330
746
|
if (!editor)
|
|
331
747
|
return [];
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
748
|
+
if (range.collapsed) {
|
|
749
|
+
const current = getCurrentBlock();
|
|
750
|
+
return current ? [current] : [];
|
|
751
|
+
}
|
|
752
|
+
const startElement = range.startContainer.nodeType === Node.TEXT_NODE ? range.startContainer.parentElement : range.startContainer;
|
|
753
|
+
const endElement = range.endContainer.nodeType === Node.TEXT_NODE ? range.endContainer.parentElement : range.endContainer;
|
|
754
|
+
const startCell = startElement?.closest?.("td,th");
|
|
755
|
+
const endCell = endElement?.closest?.("td,th");
|
|
756
|
+
const scope = startCell && startCell === endCell && editor.contains(startCell) ? startCell : editor;
|
|
757
|
+
const blocks = Array.from(scope.querySelectorAll(blockSelector)).filter((block) => {
|
|
758
|
+
if (block === editor || block.getAttribute("data-table-wrapper") === "true")
|
|
335
759
|
return false;
|
|
336
760
|
const parentBlock = block.parentElement?.closest(blockSelector);
|
|
337
|
-
if (parentBlock && parentBlock
|
|
761
|
+
if (parentBlock && scope.contains(parentBlock) && parentBlock !== scope)
|
|
338
762
|
return false;
|
|
339
763
|
try {
|
|
340
|
-
|
|
764
|
+
if (!range.intersectsNode(block))
|
|
765
|
+
return false;
|
|
766
|
+
const parent = block.parentNode;
|
|
767
|
+
if (parent === range.endContainer) {
|
|
768
|
+
const index = Array.prototype.indexOf.call(parent.childNodes, block);
|
|
769
|
+
if (range.endOffset <= index)
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
if (parent === range.startContainer) {
|
|
773
|
+
const index = Array.prototype.indexOf.call(parent.childNodes, block);
|
|
774
|
+
if (range.startOffset > index)
|
|
775
|
+
return false;
|
|
776
|
+
}
|
|
777
|
+
return true;
|
|
341
778
|
}
|
|
342
779
|
catch {
|
|
343
780
|
return false;
|
|
344
781
|
}
|
|
345
782
|
});
|
|
346
783
|
if (blocks.length > 0)
|
|
347
|
-
return blocks;
|
|
784
|
+
return sortInDocumentOrder(blocks);
|
|
348
785
|
const current = getCurrentBlock();
|
|
349
786
|
return current ? [current] : [];
|
|
350
787
|
};
|
|
@@ -362,6 +799,67 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
362
799
|
});
|
|
363
800
|
return items;
|
|
364
801
|
};
|
|
802
|
+
const getAlignmentTarget = (block) => {
|
|
803
|
+
const item = block.tagName === "LI" ? block : block.closest("li");
|
|
804
|
+
return item instanceof HTMLElement ? item : block;
|
|
805
|
+
};
|
|
806
|
+
const getAlignmentTargets = (range) => {
|
|
807
|
+
const editor = editableRef.current;
|
|
808
|
+
if (!editor)
|
|
809
|
+
return [];
|
|
810
|
+
const tableSelection = selectionRef.current;
|
|
811
|
+
const rangeElement = range.startContainer instanceof HTMLElement ? range.startContainer : range.startContainer.parentElement;
|
|
812
|
+
const rangeCell = rangeElement?.closest("td,th");
|
|
813
|
+
const selectedCells = tableSelection && rangeCell && isCellInsideSelection(rangeCell)
|
|
814
|
+
? getCellsInGridRect(tableSelection.tbody, tableSelection.sr, tableSelection.sc, tableSelection.er, tableSelection.ec)
|
|
815
|
+
: [];
|
|
816
|
+
const candidates = selectedCells.length > 0
|
|
817
|
+
? selectedCells.flatMap((cell) => {
|
|
818
|
+
const blocks = Array.from(cell.children).filter((child) => child instanceof HTMLElement && child.matches("p,h1,h2,h3,h4,h5,h6,blockquote,pre"));
|
|
819
|
+
return blocks.length > 0 ? blocks : [cell];
|
|
820
|
+
})
|
|
821
|
+
: getSelectedBlocks(range).map(getAlignmentTarget);
|
|
822
|
+
const seen = new Set();
|
|
823
|
+
return candidates.filter((target) => {
|
|
824
|
+
if (!editor.contains(target) || seen.has(target))
|
|
825
|
+
return false;
|
|
826
|
+
seen.add(target);
|
|
827
|
+
return true;
|
|
828
|
+
});
|
|
829
|
+
};
|
|
830
|
+
const readTextAlignment = (target) => {
|
|
831
|
+
const explicit = target.style.textAlign;
|
|
832
|
+
if (explicit === "center" || explicit === "right" || explicit === "justify")
|
|
833
|
+
return explicit;
|
|
834
|
+
const inherited = target.closest("blockquote[style*='text-align']");
|
|
835
|
+
const inheritedValue = inherited?.style.textAlign;
|
|
836
|
+
return inheritedValue === "center" || inheritedValue === "right" || inheritedValue === "justify"
|
|
837
|
+
? inheritedValue
|
|
838
|
+
: "left";
|
|
839
|
+
};
|
|
840
|
+
const applyTextAlignment = (alignment) => {
|
|
841
|
+
try {
|
|
842
|
+
if (!restoreSavedSelection())
|
|
843
|
+
safeSelectRange(getSelectionRangeInEditor());
|
|
844
|
+
const range = getSelectionRangeInEditor();
|
|
845
|
+
if (!range)
|
|
846
|
+
return;
|
|
847
|
+
const targets = getAlignmentTargets(range);
|
|
848
|
+
if (targets.length === 0)
|
|
849
|
+
return;
|
|
850
|
+
pushEditorHistory();
|
|
851
|
+
targets.forEach((target) => {
|
|
852
|
+
target.style.textAlign = alignment === "left" ? "" : alignment;
|
|
853
|
+
if (!target.getAttribute("style"))
|
|
854
|
+
target.removeAttribute("style");
|
|
855
|
+
});
|
|
856
|
+
safeSelectRange(range);
|
|
857
|
+
savedRangeRef.current = range.cloneRange();
|
|
858
|
+
handleInput();
|
|
859
|
+
requestAnimationFrame(updateActiveState);
|
|
860
|
+
}
|
|
861
|
+
catch { }
|
|
862
|
+
};
|
|
365
863
|
const copyCellOrBlockStyles = (from, to) => {
|
|
366
864
|
to.innerHTML = from.innerHTML || "<br>";
|
|
367
865
|
const style = from.getAttribute("style");
|
|
@@ -428,6 +926,39 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
428
926
|
block.parentElement?.replaceChild(replacement, block);
|
|
429
927
|
return replacement;
|
|
430
928
|
};
|
|
929
|
+
const clearExplicitFontSizes = (block) => {
|
|
930
|
+
Array.from(block.querySelectorAll('[style*="font-size"]')).forEach((element) => {
|
|
931
|
+
element.style.fontSize = "";
|
|
932
|
+
if (!element.style.cssText)
|
|
933
|
+
element.removeAttribute("style");
|
|
934
|
+
if (element.tagName === "SPAN" && !element.getAttribute("style") && element.attributes.length === 0) {
|
|
935
|
+
const parent = element.parentNode;
|
|
936
|
+
if (!parent)
|
|
937
|
+
return;
|
|
938
|
+
while (element.firstChild)
|
|
939
|
+
parent.insertBefore(element.firstChild, element);
|
|
940
|
+
element.remove();
|
|
941
|
+
}
|
|
942
|
+
});
|
|
943
|
+
};
|
|
944
|
+
const replaceListItemContentTag = (item, tag) => {
|
|
945
|
+
const directBlock = Array.from(item.children).find((child) => child.matches("p,h1,h2,h3,h4,h5,h6"));
|
|
946
|
+
if (directBlock)
|
|
947
|
+
return replaceBlockTag(directBlock, tag);
|
|
948
|
+
const block = document.createElement(tag);
|
|
949
|
+
const boundary = Array.from(item.children).find((child) => child.matches("ul,ol,blockquote,pre")) || null;
|
|
950
|
+
Array.from(item.childNodes).forEach((node) => {
|
|
951
|
+
if (node === boundary)
|
|
952
|
+
return;
|
|
953
|
+
if (node instanceof HTMLElement && node.dataset.srteCheck === "true")
|
|
954
|
+
return;
|
|
955
|
+
block.appendChild(node);
|
|
956
|
+
});
|
|
957
|
+
if (!block.childNodes.length)
|
|
958
|
+
block.innerHTML = "<br>";
|
|
959
|
+
item.insertBefore(block, boundary);
|
|
960
|
+
return block;
|
|
961
|
+
};
|
|
431
962
|
const applyFormatBlockFallback = (blockName) => {
|
|
432
963
|
const editor = editableRef.current;
|
|
433
964
|
const range = getSelectionRangeInEditor();
|
|
@@ -443,23 +974,36 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
443
974
|
}
|
|
444
975
|
if (range.collapsed) {
|
|
445
976
|
const block = getCurrentBlock();
|
|
446
|
-
if (!block || block === editor ||
|
|
977
|
+
if (!block || block === editor || !block.parentElement)
|
|
447
978
|
return false;
|
|
448
|
-
const
|
|
979
|
+
const item = block.closest("li");
|
|
980
|
+
const replacement = item ? replaceListItemContentTag(item, tag) : replaceBlockTag(block, tag);
|
|
981
|
+
if (/^h[1-6]$/.test(tag))
|
|
982
|
+
clearExplicitFontSizes(replacement);
|
|
449
983
|
focusElementEnd(replacement);
|
|
450
984
|
return true;
|
|
451
985
|
}
|
|
452
986
|
const selectedBlocks = sortInDocumentOrder(getSelectedBlocks(range)).filter((block) => {
|
|
453
987
|
if (!editor.contains(block) || block === editor)
|
|
454
988
|
return false;
|
|
455
|
-
if (block.closest("ul,ol"))
|
|
456
|
-
return false;
|
|
457
989
|
return Boolean(block.parentElement);
|
|
458
990
|
});
|
|
459
991
|
if (selectedBlocks.length > 0) {
|
|
460
992
|
let lastReplacement = null;
|
|
993
|
+
const handledItems = new Set();
|
|
461
994
|
selectedBlocks.forEach((block) => {
|
|
462
|
-
|
|
995
|
+
const item = block.closest("li");
|
|
996
|
+
if (item) {
|
|
997
|
+
if (handledItems.has(item))
|
|
998
|
+
return;
|
|
999
|
+
handledItems.add(item);
|
|
1000
|
+
lastReplacement = replaceListItemContentTag(item, tag);
|
|
1001
|
+
}
|
|
1002
|
+
else {
|
|
1003
|
+
lastReplacement = replaceBlockTag(block, tag);
|
|
1004
|
+
}
|
|
1005
|
+
if (lastReplacement && /^h[1-6]$/.test(tag))
|
|
1006
|
+
clearExplicitFontSizes(lastReplacement);
|
|
463
1007
|
});
|
|
464
1008
|
if (lastReplacement)
|
|
465
1009
|
focusElementEnd(lastReplacement);
|
|
@@ -479,6 +1023,64 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
479
1023
|
Array.from(list.attributes).forEach((attr) => clone.setAttribute(attr.name, attr.value));
|
|
480
1024
|
return clone;
|
|
481
1025
|
};
|
|
1026
|
+
const mergeAdjacentCompatibleLists = (list) => {
|
|
1027
|
+
const isCompatible = (candidate) => candidate instanceof HTMLElement &&
|
|
1028
|
+
candidate.tagName === list.tagName &&
|
|
1029
|
+
candidate.style.listStyleType === list.style.listStyleType &&
|
|
1030
|
+
candidate.dataset.srteChecklist === list.dataset.srteChecklist &&
|
|
1031
|
+
candidate.dataset.srteChecklistStrike === list.dataset.srteChecklistStrike;
|
|
1032
|
+
let merged = list;
|
|
1033
|
+
const previous = merged.previousElementSibling;
|
|
1034
|
+
if (isCompatible(previous)) {
|
|
1035
|
+
while (merged.firstChild)
|
|
1036
|
+
previous.appendChild(merged.firstChild);
|
|
1037
|
+
merged.remove();
|
|
1038
|
+
merged = previous;
|
|
1039
|
+
}
|
|
1040
|
+
const next = merged.nextElementSibling;
|
|
1041
|
+
if (isCompatible(next)) {
|
|
1042
|
+
while (next.firstChild)
|
|
1043
|
+
merged.appendChild(next.firstChild);
|
|
1044
|
+
next.remove();
|
|
1045
|
+
}
|
|
1046
|
+
return merged;
|
|
1047
|
+
};
|
|
1048
|
+
const clearChecklist = (list) => {
|
|
1049
|
+
delete list.dataset.srteChecklist;
|
|
1050
|
+
delete list.dataset.srteChecklistStrike;
|
|
1051
|
+
list.querySelectorAll(':scope > li > [data-srte-check]').forEach((control) => control.remove());
|
|
1052
|
+
Array.from(list.children).forEach((item) => {
|
|
1053
|
+
if (item instanceof HTMLElement) {
|
|
1054
|
+
delete item.dataset.checked;
|
|
1055
|
+
item.style.textDecoration = "";
|
|
1056
|
+
}
|
|
1057
|
+
});
|
|
1058
|
+
};
|
|
1059
|
+
const decorateChecklist = (list, strikeCompleted) => {
|
|
1060
|
+
list.dataset.srteChecklist = "true";
|
|
1061
|
+
list.dataset.srteChecklistStrike = strikeCompleted ? "true" : "false";
|
|
1062
|
+
list.style.listStyleType = "none";
|
|
1063
|
+
list.style.paddingInlineStart = "1.5em";
|
|
1064
|
+
Array.from(list.children).forEach((item) => {
|
|
1065
|
+
if (!(item instanceof HTMLElement) || item.tagName !== "LI")
|
|
1066
|
+
return;
|
|
1067
|
+
const legacyCheckbox = item.querySelector(':scope > input[data-srte-check]');
|
|
1068
|
+
const checked = item.dataset.checked === "true" || Boolean(legacyCheckbox?.checked);
|
|
1069
|
+
item.querySelectorAll(':scope > [data-srte-check]').forEach((control) => control.remove());
|
|
1070
|
+
item.dataset.checked = checked ? "true" : "false";
|
|
1071
|
+
const control = document.createElement("button");
|
|
1072
|
+
control.type = "button";
|
|
1073
|
+
control.dataset.srteCheck = "true";
|
|
1074
|
+
control.contentEditable = "false";
|
|
1075
|
+
control.tabIndex = -1;
|
|
1076
|
+
control.setAttribute("aria-label", checked ? "Mark incomplete" : "Mark complete");
|
|
1077
|
+
control.textContent = checked ? "☑" : "☐";
|
|
1078
|
+
control.style.cssText = "margin-inline:-1.45em .45em;border:0;padding:0;background:transparent;color:inherit;font:inherit;cursor:pointer";
|
|
1079
|
+
item.prepend(control);
|
|
1080
|
+
item.style.textDecoration = strikeCompleted && checked ? "line-through" : "";
|
|
1081
|
+
});
|
|
1082
|
+
return mergeAdjacentCompatibleLists(list);
|
|
1083
|
+
};
|
|
482
1084
|
const focusElementEnd = (element) => {
|
|
483
1085
|
const range = document.createRange();
|
|
484
1086
|
range.selectNodeContents(element);
|
|
@@ -505,6 +1107,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
505
1107
|
return;
|
|
506
1108
|
const paragraph = document.createElement("p");
|
|
507
1109
|
paragraph.innerHTML = li.innerHTML || "<br>";
|
|
1110
|
+
paragraph.style.textAlign = li.style.textAlign;
|
|
508
1111
|
const beforeList = cloneListShell(list);
|
|
509
1112
|
const afterList = cloneListShell(list);
|
|
510
1113
|
while (list.firstChild && list.firstChild !== li) {
|
|
@@ -568,7 +1171,227 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
568
1171
|
}
|
|
569
1172
|
return changed;
|
|
570
1173
|
};
|
|
571
|
-
const
|
|
1174
|
+
const nestListSelection = () => {
|
|
1175
|
+
const editor = editableRef.current;
|
|
1176
|
+
if (!editor)
|
|
1177
|
+
return;
|
|
1178
|
+
if (!restoreSavedSelection())
|
|
1179
|
+
safeSelectRange(getSelectionRangeInEditor());
|
|
1180
|
+
const range = getSelectionRangeInEditor();
|
|
1181
|
+
if (!range || range.collapsed)
|
|
1182
|
+
return;
|
|
1183
|
+
const items = sortInDocumentOrder(getSelectedListItems(getSelectedBlocks(range)));
|
|
1184
|
+
const firstList = items[0]?.parentElement;
|
|
1185
|
+
if (items.length === 0 ||
|
|
1186
|
+
!firstList ||
|
|
1187
|
+
!["ul", "ol"].includes(firstList.tagName.toLowerCase())) {
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
const listTag = firstList.tagName.toLowerCase();
|
|
1191
|
+
const directItems = items.filter((item) => item.parentElement === firstList);
|
|
1192
|
+
if (directItems.length === 0 || !directItems[0].previousElementSibling)
|
|
1193
|
+
return;
|
|
1194
|
+
pushEditorHistory();
|
|
1195
|
+
if (nestSelectedListItems(directItems, listTag)) {
|
|
1196
|
+
handleInput();
|
|
1197
|
+
requestAnimationFrame(updateActiveState);
|
|
1198
|
+
}
|
|
1199
|
+
};
|
|
1200
|
+
const restyleSelectedListItems = (items, listTag, styleType) => {
|
|
1201
|
+
const editor = editableRef.current;
|
|
1202
|
+
if (!editor || items.length === 0)
|
|
1203
|
+
return null;
|
|
1204
|
+
const selected = new Set(items);
|
|
1205
|
+
const sourceLists = Array.from(new Set(items.map((item) => item.parentElement)));
|
|
1206
|
+
let lastSelected = null;
|
|
1207
|
+
sourceLists.forEach((source) => {
|
|
1208
|
+
const parent = source.parentElement;
|
|
1209
|
+
if (!parent || !editor.contains(source))
|
|
1210
|
+
return;
|
|
1211
|
+
const outputs = [];
|
|
1212
|
+
let pending = null;
|
|
1213
|
+
let pendingSelected = null;
|
|
1214
|
+
Array.from(source.children).forEach((child) => {
|
|
1215
|
+
if (!(child instanceof HTMLElement) || child.tagName !== "LI")
|
|
1216
|
+
return;
|
|
1217
|
+
const isSelected = selected.has(child);
|
|
1218
|
+
if (!pending || pendingSelected !== isSelected) {
|
|
1219
|
+
pending = isSelected ? document.createElement(listTag) : cloneListShell(source);
|
|
1220
|
+
if (isSelected)
|
|
1221
|
+
pending.style.listStyleType = styleType;
|
|
1222
|
+
outputs.push(pending);
|
|
1223
|
+
pendingSelected = isSelected;
|
|
1224
|
+
}
|
|
1225
|
+
if (isSelected) {
|
|
1226
|
+
child.querySelectorAll(':scope > [data-srte-check]').forEach((control) => control.remove());
|
|
1227
|
+
delete child.dataset.checked;
|
|
1228
|
+
child.style.textDecoration = "";
|
|
1229
|
+
lastSelected = child;
|
|
1230
|
+
}
|
|
1231
|
+
pending.appendChild(child);
|
|
1232
|
+
});
|
|
1233
|
+
outputs.forEach((output) => parent.insertBefore(output, source));
|
|
1234
|
+
source.remove();
|
|
1235
|
+
outputs.forEach((output) => {
|
|
1236
|
+
if (output.tagName.toLowerCase() === listTag && output.style.listStyleType === styleType) {
|
|
1237
|
+
clearChecklist(output);
|
|
1238
|
+
}
|
|
1239
|
+
mergeAdjacentCompatibleLists(output);
|
|
1240
|
+
});
|
|
1241
|
+
});
|
|
1242
|
+
return lastSelected;
|
|
1243
|
+
};
|
|
1244
|
+
const applyListStyle = (value) => {
|
|
1245
|
+
const listTag = value.startsWith("ordered:") ? "ol" : "ul";
|
|
1246
|
+
const styleType = value.replace(/^(ordered|bullet):/, "");
|
|
1247
|
+
if (!restoreSavedSelection())
|
|
1248
|
+
safeSelectRange(getSelectionRangeInEditor());
|
|
1249
|
+
const range = getSelectionRangeInEditor();
|
|
1250
|
+
if (!range)
|
|
1251
|
+
return;
|
|
1252
|
+
const selectedBlocks = getSelectedBlocks(range);
|
|
1253
|
+
const selectedItems = getSelectedListItems(selectedBlocks);
|
|
1254
|
+
const selectedPlainBlocks = selectedBlocks.filter((block) => !block.closest("ul,ol"));
|
|
1255
|
+
if (selectedItems.length > 0) {
|
|
1256
|
+
pushEditorHistory();
|
|
1257
|
+
const lastItem = restyleSelectedListItems(selectedItems, listTag, styleType);
|
|
1258
|
+
const convertedPlainBlocks = convertSelectedBlocksToList(selectedPlainBlocks, listTag, styleType);
|
|
1259
|
+
if (!convertedPlainBlocks && lastItem)
|
|
1260
|
+
focusElementEnd(lastItem);
|
|
1261
|
+
handleInput();
|
|
1262
|
+
requestAnimationFrame(updateActiveState);
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
const lists = new Set();
|
|
1266
|
+
getSelectedListItems(getSelectedBlocks(range)).forEach((item) => {
|
|
1267
|
+
const list = item.parentElement;
|
|
1268
|
+
if (list &&
|
|
1269
|
+
["ul", "ol"].includes(list.tagName.toLowerCase())) {
|
|
1270
|
+
lists.add(list);
|
|
1271
|
+
}
|
|
1272
|
+
});
|
|
1273
|
+
[range.startContainer, range.endContainer].forEach((node) => {
|
|
1274
|
+
const element = node instanceof HTMLElement ? node : node.parentElement;
|
|
1275
|
+
const list = element?.closest("ul,ol");
|
|
1276
|
+
if (list && editableRef.current?.contains(list))
|
|
1277
|
+
lists.add(list);
|
|
1278
|
+
});
|
|
1279
|
+
if (lists.size === 0) {
|
|
1280
|
+
const blocks = range.collapsed
|
|
1281
|
+
? [getCurrentBlock()].filter((block) => Boolean(block))
|
|
1282
|
+
: getSelectedBlocks(range);
|
|
1283
|
+
const convertibleBlocks = blocks.filter((block) => editableRef.current?.contains(block) &&
|
|
1284
|
+
!block.closest("ul,ol") &&
|
|
1285
|
+
Boolean(block.parentElement));
|
|
1286
|
+
if (convertibleBlocks.length > 0) {
|
|
1287
|
+
pushEditorHistory();
|
|
1288
|
+
if (convertSelectedBlocksToList(convertibleBlocks, listTag)) {
|
|
1289
|
+
const createdList = getCurrentBlock()?.closest("ul,ol");
|
|
1290
|
+
if (createdList) {
|
|
1291
|
+
createdList.style.listStyleType = styleType;
|
|
1292
|
+
const mergedList = mergeAdjacentCompatibleLists(createdList);
|
|
1293
|
+
const lastItem = mergedList.lastElementChild;
|
|
1294
|
+
if (lastItem)
|
|
1295
|
+
focusElementEnd(lastItem);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
handleInput();
|
|
1299
|
+
requestAnimationFrame(updateActiveState);
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
if (!range.collapsed && blocks.length === 0) {
|
|
1303
|
+
toggleList(listTag);
|
|
1304
|
+
const createdList = getCurrentBlock()?.closest("ul,ol");
|
|
1305
|
+
if (createdList) {
|
|
1306
|
+
createdList.style.listStyleType = styleType;
|
|
1307
|
+
handleInput();
|
|
1308
|
+
}
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
const block = getCurrentBlock();
|
|
1312
|
+
if (!block?.closest("ul,ol"))
|
|
1313
|
+
return;
|
|
1314
|
+
const currentList = block.closest("ul,ol");
|
|
1315
|
+
if (currentList)
|
|
1316
|
+
lists.add(currentList);
|
|
1317
|
+
}
|
|
1318
|
+
if (lists.size === 0)
|
|
1319
|
+
return;
|
|
1320
|
+
pushEditorHistory();
|
|
1321
|
+
let lastList = null;
|
|
1322
|
+
const styledLists = [];
|
|
1323
|
+
lists.forEach((list) => {
|
|
1324
|
+
const target = list.tagName.toLowerCase() === listTag
|
|
1325
|
+
? list
|
|
1326
|
+
: cloneListShell(list, listTag);
|
|
1327
|
+
if (target !== list) {
|
|
1328
|
+
target.innerHTML = list.innerHTML;
|
|
1329
|
+
list.parentElement?.replaceChild(target, list);
|
|
1330
|
+
}
|
|
1331
|
+
clearChecklist(target);
|
|
1332
|
+
target.style.listStyleType = styleType;
|
|
1333
|
+
lastList = target;
|
|
1334
|
+
styledLists.push(target);
|
|
1335
|
+
});
|
|
1336
|
+
styledLists.forEach((list) => {
|
|
1337
|
+
if (editableRef.current?.contains(list)) {
|
|
1338
|
+
lastList = mergeAdjacentCompatibleLists(list);
|
|
1339
|
+
}
|
|
1340
|
+
});
|
|
1341
|
+
const lastItem = lastList?.lastElementChild;
|
|
1342
|
+
if (lastItem)
|
|
1343
|
+
focusElementEnd(lastItem);
|
|
1344
|
+
handleInput();
|
|
1345
|
+
requestAnimationFrame(updateActiveState);
|
|
1346
|
+
};
|
|
1347
|
+
const applyChecklist = (strikeCompleted = false, toggleOff = false) => {
|
|
1348
|
+
if (!restoreSavedSelection())
|
|
1349
|
+
safeSelectRange(getSelectionRangeInEditor());
|
|
1350
|
+
const range = getSelectionRangeInEditor();
|
|
1351
|
+
if (!range)
|
|
1352
|
+
return;
|
|
1353
|
+
const selectedItems = getSelectedListItems(getSelectedBlocks(range));
|
|
1354
|
+
const selectedChecklists = selectedItems.filter((item) => item.parentElement?.dataset.srteChecklist === "true");
|
|
1355
|
+
if (toggleOff && selectedItems.length > 0 && selectedChecklists.length === selectedItems.length) {
|
|
1356
|
+
pushEditorHistory();
|
|
1357
|
+
selectedItems.forEach((item) => {
|
|
1358
|
+
item.querySelectorAll(':scope > [data-srte-check]').forEach((control) => control.remove());
|
|
1359
|
+
delete item.dataset.checked;
|
|
1360
|
+
item.style.textDecoration = "";
|
|
1361
|
+
});
|
|
1362
|
+
if (transformSelectedListItems(selectedItems, "ul")) {
|
|
1363
|
+
handleInput();
|
|
1364
|
+
requestAnimationFrame(updateActiveState);
|
|
1365
|
+
}
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
if (selectedItems.length > 0) {
|
|
1369
|
+
pushEditorHistory();
|
|
1370
|
+
const lastItem = restyleSelectedListItems(selectedItems, "ul", "none");
|
|
1371
|
+
const lists = new Set();
|
|
1372
|
+
selectedItems.forEach((item) => {
|
|
1373
|
+
if (item.parentElement)
|
|
1374
|
+
lists.add(item.parentElement);
|
|
1375
|
+
});
|
|
1376
|
+
lists.forEach((list) => decorateChecklist(list, strikeCompleted));
|
|
1377
|
+
if (lastItem)
|
|
1378
|
+
focusElementEnd(lastItem);
|
|
1379
|
+
handleInput();
|
|
1380
|
+
requestAnimationFrame(updateActiveState);
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
applyListStyle("bullet:none");
|
|
1384
|
+
const createdList = getCurrentBlock()?.closest("ul");
|
|
1385
|
+
if (!createdList)
|
|
1386
|
+
return;
|
|
1387
|
+
const merged = decorateChecklist(createdList, strikeCompleted);
|
|
1388
|
+
const lastItem = merged.lastElementChild;
|
|
1389
|
+
if (lastItem)
|
|
1390
|
+
focusElementEnd(lastItem);
|
|
1391
|
+
handleInput();
|
|
1392
|
+
requestAnimationFrame(updateActiveState);
|
|
1393
|
+
};
|
|
1394
|
+
const convertSelectedBlocksToList = (blocks, listTag, styleType) => {
|
|
572
1395
|
const editor = editableRef.current;
|
|
573
1396
|
if (!editor || blocks.length === 0)
|
|
574
1397
|
return false;
|
|
@@ -599,64 +1422,279 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
599
1422
|
return position & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
|
|
600
1423
|
});
|
|
601
1424
|
const list = document.createElement(listTag);
|
|
1425
|
+
if (styleType)
|
|
1426
|
+
list.style.listStyleType = styleType;
|
|
602
1427
|
parent.insertBefore(list, group[0]);
|
|
603
1428
|
group.forEach((block) => {
|
|
604
1429
|
const li = document.createElement("li");
|
|
605
1430
|
li.innerHTML = block.innerHTML || "<br>";
|
|
1431
|
+
li.style.textAlign = block.style.textAlign;
|
|
606
1432
|
list.appendChild(li);
|
|
607
1433
|
block.remove();
|
|
608
1434
|
lastLi = li;
|
|
609
1435
|
});
|
|
1436
|
+
mergeAdjacentCompatibleLists(list);
|
|
610
1437
|
});
|
|
611
1438
|
if (lastLi)
|
|
612
1439
|
focusElementEnd(lastLi);
|
|
613
1440
|
return true;
|
|
614
1441
|
};
|
|
1442
|
+
const convertTableCellSelectionToList = (range, listTag) => {
|
|
1443
|
+
if (range.collapsed)
|
|
1444
|
+
return false;
|
|
1445
|
+
const startCell = getClosestCell(range.startContainer);
|
|
1446
|
+
const endCell = getClosestCell(range.endContainer);
|
|
1447
|
+
if (!startCell || startCell !== endCell)
|
|
1448
|
+
return false;
|
|
1449
|
+
const fragment = range.extractContents();
|
|
1450
|
+
const parts = [document.createDocumentFragment()];
|
|
1451
|
+
Array.from(fragment.childNodes).forEach((node) => {
|
|
1452
|
+
if (node instanceof HTMLBRElement) {
|
|
1453
|
+
parts.push(document.createDocumentFragment());
|
|
1454
|
+
}
|
|
1455
|
+
else {
|
|
1456
|
+
parts[parts.length - 1].appendChild(node);
|
|
1457
|
+
}
|
|
1458
|
+
});
|
|
1459
|
+
const nonEmptyParts = parts.filter((part) => part.textContent?.trim() || part.childNodes.length > 0);
|
|
1460
|
+
if (nonEmptyParts.length === 0) {
|
|
1461
|
+
range.insertNode(fragment);
|
|
1462
|
+
return false;
|
|
1463
|
+
}
|
|
1464
|
+
const list = document.createElement(listTag);
|
|
1465
|
+
nonEmptyParts.forEach((part) => {
|
|
1466
|
+
const item = document.createElement("li");
|
|
1467
|
+
item.appendChild(part);
|
|
1468
|
+
if (!item.innerHTML.trim())
|
|
1469
|
+
item.innerHTML = "<br>";
|
|
1470
|
+
list.appendChild(item);
|
|
1471
|
+
});
|
|
1472
|
+
range.insertNode(list);
|
|
1473
|
+
const lastItem = list.lastElementChild;
|
|
1474
|
+
if (lastItem)
|
|
1475
|
+
focusElementEnd(lastItem);
|
|
1476
|
+
return true;
|
|
1477
|
+
};
|
|
1478
|
+
const convertRootLineSelectionToList = (range, listTag) => {
|
|
1479
|
+
const editor = editableRef.current;
|
|
1480
|
+
if (!editor || range.collapsed)
|
|
1481
|
+
return false;
|
|
1482
|
+
const closestBlock = (node) => {
|
|
1483
|
+
const element = node instanceof HTMLElement ? node : node.parentElement;
|
|
1484
|
+
const block = element?.closest(blockSelector);
|
|
1485
|
+
return block === editor ? null : block;
|
|
1486
|
+
};
|
|
1487
|
+
if (closestBlock(range.startContainer) || closestBlock(range.endContainer))
|
|
1488
|
+
return false;
|
|
1489
|
+
const lineRange = range.cloneRange();
|
|
1490
|
+
const breaks = Array.from(editor.querySelectorAll("br"));
|
|
1491
|
+
let previousBreak = null;
|
|
1492
|
+
let nextBreak = null;
|
|
1493
|
+
breaks.forEach((lineBreak) => {
|
|
1494
|
+
const parent = lineBreak.parentNode;
|
|
1495
|
+
if (!parent)
|
|
1496
|
+
return;
|
|
1497
|
+
const index = Array.prototype.indexOf.call(parent.childNodes, lineBreak);
|
|
1498
|
+
const beforeRelation = range.comparePoint(parent, index);
|
|
1499
|
+
const afterRelation = range.comparePoint(parent, index + 1);
|
|
1500
|
+
if (afterRelation === -1) {
|
|
1501
|
+
previousBreak = lineBreak;
|
|
1502
|
+
}
|
|
1503
|
+
else if (!nextBreak &&
|
|
1504
|
+
(beforeRelation === 1 || (beforeRelation === 0 && !range.intersectsNode(lineBreak)))) {
|
|
1505
|
+
nextBreak = lineBreak;
|
|
1506
|
+
}
|
|
1507
|
+
});
|
|
1508
|
+
if (previousBreak)
|
|
1509
|
+
lineRange.setStartAfter(previousBreak);
|
|
1510
|
+
else
|
|
1511
|
+
lineRange.setStart(editor, 0);
|
|
1512
|
+
if (nextBreak)
|
|
1513
|
+
lineRange.setEndBefore(nextBreak);
|
|
1514
|
+
else
|
|
1515
|
+
lineRange.setEnd(editor, editor.childNodes.length);
|
|
1516
|
+
const fragment = lineRange.extractContents();
|
|
1517
|
+
const parts = [document.createDocumentFragment()];
|
|
1518
|
+
Array.from(fragment.childNodes).forEach((node) => {
|
|
1519
|
+
if (node instanceof HTMLBRElement)
|
|
1520
|
+
parts.push(document.createDocumentFragment());
|
|
1521
|
+
else if (node instanceof HTMLElement && node.matches(blockSelector)) {
|
|
1522
|
+
if (parts[parts.length - 1].childNodes.length > 0)
|
|
1523
|
+
parts.push(document.createDocumentFragment());
|
|
1524
|
+
while (node.firstChild)
|
|
1525
|
+
parts[parts.length - 1].appendChild(node.firstChild);
|
|
1526
|
+
parts.push(document.createDocumentFragment());
|
|
1527
|
+
}
|
|
1528
|
+
else
|
|
1529
|
+
parts[parts.length - 1].appendChild(node);
|
|
1530
|
+
});
|
|
1531
|
+
const nonEmptyParts = parts.filter((part) => part.textContent?.length || part.childNodes.length > 0);
|
|
1532
|
+
if (nonEmptyParts.length === 0) {
|
|
1533
|
+
lineRange.insertNode(fragment);
|
|
1534
|
+
return false;
|
|
1535
|
+
}
|
|
1536
|
+
const list = document.createElement(listTag);
|
|
1537
|
+
nonEmptyParts.forEach((part) => {
|
|
1538
|
+
const item = document.createElement("li");
|
|
1539
|
+
item.appendChild(part);
|
|
1540
|
+
list.appendChild(item);
|
|
1541
|
+
});
|
|
1542
|
+
lineRange.insertNode(list);
|
|
1543
|
+
const mergedList = mergeAdjacentCompatibleLists(list);
|
|
1544
|
+
const lastItem = mergedList.lastElementChild;
|
|
1545
|
+
if (lastItem)
|
|
1546
|
+
focusElementEnd(lastItem);
|
|
1547
|
+
return true;
|
|
1548
|
+
};
|
|
1549
|
+
const transformSelectedListItems = (items, listTag) => {
|
|
1550
|
+
const editor = editableRef.current;
|
|
1551
|
+
if (!editor || items.length === 0)
|
|
1552
|
+
return false;
|
|
1553
|
+
const selected = new Set(items.filter((item) => {
|
|
1554
|
+
const parentItem = item.parentElement?.closest("li");
|
|
1555
|
+
return !parentItem || !items.includes(parentItem);
|
|
1556
|
+
}));
|
|
1557
|
+
const lists = new Set();
|
|
1558
|
+
selected.forEach((item) => {
|
|
1559
|
+
const list = item.parentElement;
|
|
1560
|
+
if (list &&
|
|
1561
|
+
(list.tagName.toLowerCase() === "ul" || list.tagName.toLowerCase() === "ol")) {
|
|
1562
|
+
lists.add(list);
|
|
1563
|
+
}
|
|
1564
|
+
});
|
|
1565
|
+
let changed = false;
|
|
1566
|
+
let lastTarget = null;
|
|
1567
|
+
lists.forEach((list) => {
|
|
1568
|
+
const parent = list.parentElement;
|
|
1569
|
+
if (!parent || !editor.contains(list))
|
|
1570
|
+
return;
|
|
1571
|
+
const toggleOff = list.tagName.toLowerCase() === listTag;
|
|
1572
|
+
let pendingList = null;
|
|
1573
|
+
const flushPendingList = () => {
|
|
1574
|
+
if (!pendingList?.childNodes.length)
|
|
1575
|
+
return;
|
|
1576
|
+
parent.insertBefore(pendingList, list);
|
|
1577
|
+
pendingList = null;
|
|
1578
|
+
};
|
|
1579
|
+
Array.from(list.children).forEach((child) => {
|
|
1580
|
+
if (!(child instanceof HTMLElement) || child.tagName.toLowerCase() !== "li")
|
|
1581
|
+
return;
|
|
1582
|
+
const isSelected = selected.has(child);
|
|
1583
|
+
if (isSelected && toggleOff) {
|
|
1584
|
+
flushPendingList();
|
|
1585
|
+
const paragraph = document.createElement("p");
|
|
1586
|
+
paragraph.innerHTML = child.innerHTML || "<br>";
|
|
1587
|
+
paragraph.style.textAlign = child.style.textAlign;
|
|
1588
|
+
parent.insertBefore(paragraph, list);
|
|
1589
|
+
lastTarget = paragraph;
|
|
1590
|
+
child.remove();
|
|
1591
|
+
changed = true;
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
const outputTag = isSelected ? listTag : list.tagName.toLowerCase();
|
|
1595
|
+
if (!pendingList || pendingList.tagName.toLowerCase() !== outputTag) {
|
|
1596
|
+
flushPendingList();
|
|
1597
|
+
pendingList = cloneListShell(list, outputTag);
|
|
1598
|
+
}
|
|
1599
|
+
pendingList.appendChild(child);
|
|
1600
|
+
if (isSelected) {
|
|
1601
|
+
lastTarget = child;
|
|
1602
|
+
changed = true;
|
|
1603
|
+
}
|
|
1604
|
+
});
|
|
1605
|
+
flushPendingList();
|
|
1606
|
+
list.remove();
|
|
1607
|
+
});
|
|
1608
|
+
if (changed && lastTarget)
|
|
1609
|
+
focusElementEnd(lastTarget);
|
|
1610
|
+
return changed;
|
|
1611
|
+
};
|
|
615
1612
|
const toggleList = (listTag) => {
|
|
616
1613
|
const editor = editableRef.current;
|
|
617
1614
|
if (!editor)
|
|
618
1615
|
return;
|
|
619
1616
|
if (!restoreSavedSelection())
|
|
620
1617
|
safeSelectRange(getSelectionRangeInEditor());
|
|
1618
|
+
const setListActiveState = (active) => {
|
|
1619
|
+
setActiveState((current) => ({
|
|
1620
|
+
...current,
|
|
1621
|
+
unorderedList: listTag === "ul" ? active : false,
|
|
1622
|
+
orderedList: listTag === "ol" ? active : false,
|
|
1623
|
+
}));
|
|
1624
|
+
};
|
|
621
1625
|
const range = getSelectionRangeInEditor();
|
|
622
1626
|
if (range && !range.collapsed) {
|
|
623
1627
|
const blocks = getSelectedBlocks(range);
|
|
624
1628
|
const selectedListItems = getSelectedListItems(blocks);
|
|
1629
|
+
if (selectedListItems.length > 0) {
|
|
1630
|
+
pushEditorHistory();
|
|
1631
|
+
if (transformSelectedListItems(selectedListItems, listTag)) {
|
|
1632
|
+
const active = Boolean(getCurrentBlock()?.closest(listTag));
|
|
1633
|
+
setListActiveState(active);
|
|
1634
|
+
handleInput();
|
|
1635
|
+
requestAnimationFrame(updateActiveState);
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
pushEditorHistory();
|
|
625
1640
|
const convertedBlocks = convertSelectedBlocksToList(blocks, listTag);
|
|
626
|
-
|
|
627
|
-
|
|
1641
|
+
if (convertedBlocks) {
|
|
1642
|
+
setListActiveState(true);
|
|
628
1643
|
handleInput();
|
|
629
1644
|
requestAnimationFrame(updateActiveState);
|
|
630
1645
|
return;
|
|
631
1646
|
}
|
|
1647
|
+
if (blocks.length === 0) {
|
|
1648
|
+
pushEditorHistory();
|
|
1649
|
+
if (convertRootLineSelectionToList(range, listTag)) {
|
|
1650
|
+
setListActiveState(true);
|
|
1651
|
+
handleInput();
|
|
1652
|
+
requestAnimationFrame(updateActiveState);
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
if (convertTableCellSelectionToList(range, listTag)) {
|
|
1656
|
+
setListActiveState(true);
|
|
1657
|
+
handleInput();
|
|
1658
|
+
requestAnimationFrame(updateActiveState);
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
632
1662
|
}
|
|
633
1663
|
const block = getCurrentBlock();
|
|
634
1664
|
if (!block) {
|
|
1665
|
+
pushEditorHistory();
|
|
635
1666
|
insertEmptyListAtSelection(listTag);
|
|
1667
|
+
setListActiveState(true);
|
|
636
1668
|
handleInput();
|
|
637
1669
|
requestAnimationFrame(updateActiveState);
|
|
638
1670
|
return;
|
|
639
1671
|
}
|
|
640
1672
|
const currentList = block.closest("ul,ol");
|
|
641
1673
|
if (currentList && editor.contains(currentList)) {
|
|
1674
|
+
pushEditorHistory();
|
|
642
1675
|
if (currentList.tagName.toLowerCase() === listTag) {
|
|
643
1676
|
const li = block.closest("li");
|
|
644
|
-
if (li)
|
|
1677
|
+
if (li) {
|
|
645
1678
|
unwrapListItem(li, currentList);
|
|
1679
|
+
setListActiveState(false);
|
|
1680
|
+
}
|
|
646
1681
|
}
|
|
647
1682
|
else {
|
|
648
1683
|
convertListTag(currentList, listTag);
|
|
1684
|
+
setListActiveState(true);
|
|
649
1685
|
}
|
|
650
1686
|
handleInput();
|
|
651
1687
|
requestAnimationFrame(updateActiveState);
|
|
652
1688
|
return;
|
|
653
1689
|
}
|
|
1690
|
+
pushEditorHistory();
|
|
654
1691
|
const list = document.createElement(listTag);
|
|
655
1692
|
const li = document.createElement("li");
|
|
656
1693
|
li.innerHTML = block.innerHTML || "<br>";
|
|
657
1694
|
list.appendChild(li);
|
|
658
1695
|
block.parentElement?.replaceChild(list, block);
|
|
659
1696
|
focusElementEnd(li);
|
|
1697
|
+
setListActiveState(true);
|
|
660
1698
|
handleInput();
|
|
661
1699
|
requestAnimationFrame(updateActiveState);
|
|
662
1700
|
};
|
|
@@ -708,19 +1746,69 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
708
1746
|
const selected = sortInDocumentOrder(blocks).filter((block) => {
|
|
709
1747
|
if (!editor.contains(block) || block === editor)
|
|
710
1748
|
return false;
|
|
711
|
-
if (block.closest("ul,ol"))
|
|
712
|
-
return false;
|
|
713
1749
|
if (block.tagName.toLowerCase() === "blockquote")
|
|
714
1750
|
return false;
|
|
715
1751
|
return Boolean(block.parentElement);
|
|
716
1752
|
});
|
|
717
1753
|
if (selected.length === 0)
|
|
718
1754
|
return false;
|
|
1755
|
+
const mergeQuote = (quote) => {
|
|
1756
|
+
let merged = quote;
|
|
1757
|
+
const previous = merged.previousElementSibling;
|
|
1758
|
+
if (previous?.tagName === "BLOCKQUOTE") {
|
|
1759
|
+
while (merged.firstChild)
|
|
1760
|
+
previous.appendChild(merged.firstChild);
|
|
1761
|
+
merged.remove();
|
|
1762
|
+
merged = previous;
|
|
1763
|
+
}
|
|
1764
|
+
const next = merged.nextElementSibling;
|
|
1765
|
+
if (next?.tagName === "BLOCKQUOTE") {
|
|
1766
|
+
while (next.firstChild)
|
|
1767
|
+
merged.appendChild(next.firstChild);
|
|
1768
|
+
next.remove();
|
|
1769
|
+
}
|
|
1770
|
+
return merged;
|
|
1771
|
+
};
|
|
1772
|
+
const selectedItems = new Set(getSelectedListItems(selected));
|
|
1773
|
+
const sourceLists = Array.from(new Set(Array.from(selectedItems, (item) => item.parentElement)));
|
|
719
1774
|
let lastWrapped = null;
|
|
720
|
-
|
|
1775
|
+
sourceLists.forEach((source) => {
|
|
1776
|
+
const parent = source.parentElement;
|
|
1777
|
+
if (!parent)
|
|
1778
|
+
return;
|
|
1779
|
+
let pending = null;
|
|
1780
|
+
let pendingSelected = null;
|
|
1781
|
+
const outputs = [];
|
|
1782
|
+
Array.from(source.children).forEach((child) => {
|
|
1783
|
+
if (!(child instanceof HTMLElement) || child.tagName !== "LI")
|
|
1784
|
+
return;
|
|
1785
|
+
const isSelected = selectedItems.has(child);
|
|
1786
|
+
if (!pending || pendingSelected !== isSelected) {
|
|
1787
|
+
pending = cloneListShell(source);
|
|
1788
|
+
pendingSelected = isSelected;
|
|
1789
|
+
outputs.push({ list: pending, selected: isSelected });
|
|
1790
|
+
}
|
|
1791
|
+
pending.appendChild(child);
|
|
1792
|
+
});
|
|
1793
|
+
outputs.forEach((output) => {
|
|
1794
|
+
if (output.selected) {
|
|
1795
|
+
const quote = document.createElement("blockquote");
|
|
1796
|
+
quote.appendChild(output.list);
|
|
1797
|
+
parent.insertBefore(quote, source);
|
|
1798
|
+
mergeQuote(quote);
|
|
1799
|
+
lastWrapped = output.list.lastElementChild;
|
|
1800
|
+
}
|
|
1801
|
+
else {
|
|
1802
|
+
parent.insertBefore(output.list, source);
|
|
1803
|
+
}
|
|
1804
|
+
});
|
|
1805
|
+
source.remove();
|
|
1806
|
+
});
|
|
1807
|
+
selected.filter((block) => !block.closest("ul,ol")).forEach((block) => {
|
|
721
1808
|
const quote = document.createElement("blockquote");
|
|
722
1809
|
block.parentElement?.insertBefore(quote, block);
|
|
723
1810
|
quote.appendChild(block);
|
|
1811
|
+
mergeQuote(quote);
|
|
724
1812
|
lastWrapped = block;
|
|
725
1813
|
});
|
|
726
1814
|
if (lastWrapped)
|
|
@@ -746,6 +1834,16 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
746
1834
|
const block = getCurrentBlock();
|
|
747
1835
|
if (block) {
|
|
748
1836
|
pushEditorHistory();
|
|
1837
|
+
const currentList = block.closest("ul,ol");
|
|
1838
|
+
if (currentList && editor.contains(currentList)) {
|
|
1839
|
+
const quote = document.createElement("blockquote");
|
|
1840
|
+
currentList.parentElement?.insertBefore(quote, currentList);
|
|
1841
|
+
quote.appendChild(currentList);
|
|
1842
|
+
focusElementEnd(block);
|
|
1843
|
+
handleInput();
|
|
1844
|
+
requestAnimationFrame(updateActiveState);
|
|
1845
|
+
return;
|
|
1846
|
+
}
|
|
749
1847
|
if (!wrapBlocks([block]))
|
|
750
1848
|
return;
|
|
751
1849
|
handleInput();
|
|
@@ -769,70 +1867,210 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
769
1867
|
}
|
|
770
1868
|
catch { }
|
|
771
1869
|
};
|
|
1870
|
+
const toggleCodeBlock = () => {
|
|
1871
|
+
try {
|
|
1872
|
+
if (!restoreSavedSelection())
|
|
1873
|
+
safeSelectRange(getSelectionRangeInEditor());
|
|
1874
|
+
const editor = editableRef.current;
|
|
1875
|
+
const range = getSelectionRangeInEditor();
|
|
1876
|
+
if (!editor || !range)
|
|
1877
|
+
return;
|
|
1878
|
+
const blocks = (range.collapsed
|
|
1879
|
+
? [getCurrentBlock()].filter((block) => Boolean(block))
|
|
1880
|
+
: getSelectedBlocks(range)).filter((block) => editor.contains(block));
|
|
1881
|
+
if (blocks.length === 0)
|
|
1882
|
+
return;
|
|
1883
|
+
const listItems = getSelectedListItems(blocks);
|
|
1884
|
+
const plainBlocks = blocks.filter((block) => !block.closest("ul,ol") && !block.closest("table"));
|
|
1885
|
+
const getItemCode = (item) => item.querySelector(":scope > pre[data-srte-list-code]");
|
|
1886
|
+
const allTargetsActive = listItems.every((item) => Boolean(getItemCode(item))) &&
|
|
1887
|
+
plainBlocks.every((block) => block.tagName === "PRE") &&
|
|
1888
|
+
listItems.length + plainBlocks.length > 0;
|
|
1889
|
+
pushEditorHistory();
|
|
1890
|
+
let lastTarget = null;
|
|
1891
|
+
listItems.forEach((item) => {
|
|
1892
|
+
const existing = getItemCode(item);
|
|
1893
|
+
if (allTargetsActive && existing) {
|
|
1894
|
+
const code = existing.querySelector(":scope > code");
|
|
1895
|
+
const source = code || existing;
|
|
1896
|
+
while (source.firstChild)
|
|
1897
|
+
item.insertBefore(source.firstChild, existing);
|
|
1898
|
+
existing.remove();
|
|
1899
|
+
lastTarget = item;
|
|
1900
|
+
return;
|
|
1901
|
+
}
|
|
1902
|
+
if (existing) {
|
|
1903
|
+
lastTarget = existing;
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
const pre = document.createElement("pre");
|
|
1907
|
+
pre.dataset.srteListCode = "true";
|
|
1908
|
+
pre.style.textAlign = "left";
|
|
1909
|
+
const code = document.createElement("code");
|
|
1910
|
+
const nestedList = Array.from(item.children).find((child) => child.matches("ul,ol")) || null;
|
|
1911
|
+
Array.from(item.childNodes).forEach((node) => {
|
|
1912
|
+
if (node === nestedList)
|
|
1913
|
+
return;
|
|
1914
|
+
if (node instanceof HTMLElement && node.dataset.srteCheck === "true")
|
|
1915
|
+
return;
|
|
1916
|
+
code.appendChild(node);
|
|
1917
|
+
});
|
|
1918
|
+
if (!code.childNodes.length)
|
|
1919
|
+
code.innerHTML = "<br>";
|
|
1920
|
+
pre.appendChild(code);
|
|
1921
|
+
item.insertBefore(pre, nestedList);
|
|
1922
|
+
lastTarget = pre;
|
|
1923
|
+
});
|
|
1924
|
+
sortInDocumentOrder(plainBlocks).forEach((block) => {
|
|
1925
|
+
if (allTargetsActive && block.tagName === "PRE") {
|
|
1926
|
+
const paragraph = document.createElement("p");
|
|
1927
|
+
const code = block.querySelector(":scope > code");
|
|
1928
|
+
paragraph.innerHTML = code?.innerHTML || block.innerHTML || "<br>";
|
|
1929
|
+
block.parentElement?.replaceChild(paragraph, block);
|
|
1930
|
+
lastTarget = paragraph;
|
|
1931
|
+
}
|
|
1932
|
+
else if (block.tagName !== "PRE") {
|
|
1933
|
+
const pre = replaceBlockTag(block, "pre");
|
|
1934
|
+
pre.style.textAlign = "left";
|
|
1935
|
+
if (!pre.querySelector(":scope > code")) {
|
|
1936
|
+
const code = document.createElement("code");
|
|
1937
|
+
while (pre.firstChild)
|
|
1938
|
+
code.appendChild(pre.firstChild);
|
|
1939
|
+
pre.appendChild(code);
|
|
1940
|
+
}
|
|
1941
|
+
lastTarget = pre;
|
|
1942
|
+
}
|
|
1943
|
+
});
|
|
1944
|
+
if (lastTarget)
|
|
1945
|
+
focusElementEnd(lastTarget);
|
|
1946
|
+
handleInput();
|
|
1947
|
+
requestAnimationFrame(updateActiveState);
|
|
1948
|
+
}
|
|
1949
|
+
catch { }
|
|
1950
|
+
};
|
|
772
1951
|
const applyFontSize = (size) => {
|
|
773
1952
|
try {
|
|
774
|
-
// Update current font size state
|
|
775
|
-
setCurrentFontSize(size);
|
|
776
1953
|
const editor = editableRef.current;
|
|
777
1954
|
if (!editor)
|
|
778
1955
|
return;
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
// Use current range if it's within our editor
|
|
786
|
-
if (editor.contains(currentRange.commonAncestorContainer)) {
|
|
787
|
-
range = currentRange;
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
// Fallback to saved range if current range is not available
|
|
791
|
-
if (!range && savedRangeRef.current) {
|
|
792
|
-
range = savedRangeRef.current.cloneRange();
|
|
793
|
-
}
|
|
794
|
-
// If no range at all, just update state for future typing
|
|
1956
|
+
const valuePx = Number(size);
|
|
1957
|
+
if (!Number.isFinite(valuePx) || valuePx <= 0)
|
|
1958
|
+
return;
|
|
1959
|
+
if (!restoreSavedSelection())
|
|
1960
|
+
safeSelectRange(getSelectionRangeInEditor());
|
|
1961
|
+
const range = getSelectionRangeInEditor();
|
|
795
1962
|
if (!range)
|
|
796
1963
|
return;
|
|
797
|
-
|
|
1964
|
+
setCurrentFontSize(String(Math.round(valuePx)));
|
|
798
1965
|
if (range.collapsed) {
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
const newRange = document.createRange();
|
|
806
|
-
newRange.setStart(span.firstChild, 1);
|
|
807
|
-
newRange.collapse(true);
|
|
808
|
-
if (sel) {
|
|
809
|
-
sel.removeAllRanges();
|
|
810
|
-
sel.addRange(newRange);
|
|
811
|
-
}
|
|
812
|
-
handleInput();
|
|
1966
|
+
pendingFontSizeRef.current = {
|
|
1967
|
+
valuePx,
|
|
1968
|
+
container: range.startContainer,
|
|
1969
|
+
offset: range.startOffset,
|
|
1970
|
+
};
|
|
1971
|
+
savedRangeRef.current = range.cloneRange();
|
|
813
1972
|
return;
|
|
814
1973
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1974
|
+
pushEditorHistory();
|
|
1975
|
+
pendingFontSizeRef.current = null;
|
|
1976
|
+
const textNodes = [];
|
|
1977
|
+
const walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT);
|
|
1978
|
+
let candidate = walker.nextNode();
|
|
1979
|
+
while (candidate) {
|
|
1980
|
+
const text = candidate;
|
|
1981
|
+
const owner = text.parentElement;
|
|
1982
|
+
try {
|
|
1983
|
+
if (text.data.length > 0 && range.intersectsNode(text) &&
|
|
1984
|
+
!owner?.closest('[contenteditable="false"],button,[data-srte-editor-only="true"]'))
|
|
1985
|
+
textNodes.push(text);
|
|
1986
|
+
}
|
|
1987
|
+
catch { }
|
|
1988
|
+
candidate = walker.nextNode();
|
|
1989
|
+
}
|
|
1990
|
+
const selectedTexts = [];
|
|
1991
|
+
[...textNodes].reverse().forEach((text) => {
|
|
1992
|
+
const start = text === range.startContainer ? range.startOffset : 0;
|
|
1993
|
+
const end = text === range.endContainer ? range.endOffset : text.data.length;
|
|
1994
|
+
if (end <= start)
|
|
1995
|
+
return;
|
|
1996
|
+
if (end < text.data.length)
|
|
1997
|
+
text.splitText(end);
|
|
1998
|
+
const selected = start > 0 ? text.splitText(start) : text;
|
|
1999
|
+
const parent = selected.parentElement;
|
|
2000
|
+
if (parent?.tagName === "SPAN" &&
|
|
2001
|
+
parent.childNodes.length === 1 &&
|
|
2002
|
+
parent.textContent === selected.data) {
|
|
2003
|
+
parent.style.fontSize = `${valuePx}px`;
|
|
2004
|
+
}
|
|
2005
|
+
else {
|
|
2006
|
+
const span = document.createElement("span");
|
|
2007
|
+
span.style.fontSize = `${valuePx}px`;
|
|
2008
|
+
selected.parentNode?.insertBefore(span, selected);
|
|
2009
|
+
span.appendChild(selected);
|
|
2010
|
+
}
|
|
2011
|
+
selectedTexts.unshift(selected);
|
|
2012
|
+
});
|
|
2013
|
+
if (selectedTexts.length > 0) {
|
|
2014
|
+
normalizeFontSizeSpans(editor);
|
|
2015
|
+
const nextRange = document.createRange();
|
|
2016
|
+
nextRange.setStart(selectedTexts[0], 0);
|
|
2017
|
+
const last = selectedTexts[selectedTexts.length - 1];
|
|
2018
|
+
nextRange.setEnd(last, last.data.length);
|
|
2019
|
+
safeSelectRange(nextRange);
|
|
2020
|
+
savedRangeRef.current = nextRange.cloneRange();
|
|
828
2021
|
}
|
|
829
|
-
// Trigger change event
|
|
830
2022
|
handleInput();
|
|
2023
|
+
requestAnimationFrame(updateActiveState);
|
|
831
2024
|
}
|
|
832
2025
|
catch (error) {
|
|
833
2026
|
console.error('Error applying font size:', error);
|
|
834
2027
|
}
|
|
835
2028
|
};
|
|
2029
|
+
useEffect(() => {
|
|
2030
|
+
const editor = editableRef.current;
|
|
2031
|
+
if (!editor)
|
|
2032
|
+
return;
|
|
2033
|
+
const applyPendingFontSize = (event) => {
|
|
2034
|
+
const pending = pendingFontSizeRef.current;
|
|
2035
|
+
if (!pending || event.inputType !== "insertText" || !event.data)
|
|
2036
|
+
return;
|
|
2037
|
+
const range = getSelectionRangeInEditor();
|
|
2038
|
+
if (!range?.collapsed ||
|
|
2039
|
+
range.startContainer !== pending.container ||
|
|
2040
|
+
range.startOffset !== pending.offset)
|
|
2041
|
+
return;
|
|
2042
|
+
event.preventDefault();
|
|
2043
|
+
event.stopPropagation();
|
|
2044
|
+
pushEditorHistory();
|
|
2045
|
+
const text = document.createTextNode(event.data);
|
|
2046
|
+
const sizedAncestor = range.startContainer instanceof HTMLElement
|
|
2047
|
+
? range.startContainer.closest("span")
|
|
2048
|
+
: range.startContainer.parentElement?.closest("span");
|
|
2049
|
+
if (sizedAncestor instanceof HTMLElement &&
|
|
2050
|
+
parseFontSizePx(sizedAncestor.style.fontSize) === pending.valuePx) {
|
|
2051
|
+
range.insertNode(text);
|
|
2052
|
+
}
|
|
2053
|
+
else {
|
|
2054
|
+
const span = document.createElement("span");
|
|
2055
|
+
span.style.fontSize = `${pending.valuePx}px`;
|
|
2056
|
+
span.appendChild(text);
|
|
2057
|
+
range.insertNode(span);
|
|
2058
|
+
}
|
|
2059
|
+
const nextRange = document.createRange();
|
|
2060
|
+
nextRange.setStartAfter(text);
|
|
2061
|
+
nextRange.collapse(true);
|
|
2062
|
+
safeSelectRange(nextRange);
|
|
2063
|
+
pendingFontSizeRef.current = {
|
|
2064
|
+
valuePx: pending.valuePx,
|
|
2065
|
+
container: nextRange.startContainer,
|
|
2066
|
+
offset: nextRange.startOffset,
|
|
2067
|
+
};
|
|
2068
|
+
savedRangeRef.current = nextRange.cloneRange();
|
|
2069
|
+
handleInput();
|
|
2070
|
+
};
|
|
2071
|
+
editor.addEventListener("beforeinput", applyPendingFontSize);
|
|
2072
|
+
return () => editor.removeEventListener("beforeinput", applyPendingFontSize);
|
|
2073
|
+
}, []);
|
|
836
2074
|
const applyFontFamily = (font) => {
|
|
837
2075
|
try {
|
|
838
2076
|
setCurrentFont(font);
|
|
@@ -1920,164 +3158,6 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1920
3158
|
.replace(/&/g, "&")
|
|
1921
3159
|
.replace(/</g, "<")
|
|
1922
3160
|
.replace(/>/g, ">");
|
|
1923
|
-
const escapeHtmlAttribute = (value) => escapeHtml(value).replace(/"/g, """);
|
|
1924
|
-
const markdownToHtml = (markdown) => {
|
|
1925
|
-
const lines = markdown.replace(/\r\n/g, "\n").split("\n");
|
|
1926
|
-
let html = "";
|
|
1927
|
-
let listType = null;
|
|
1928
|
-
let paragraph = [];
|
|
1929
|
-
let codeFence = null;
|
|
1930
|
-
const closeList = () => {
|
|
1931
|
-
if (listType) {
|
|
1932
|
-
html += `</${listType}>`;
|
|
1933
|
-
listType = null;
|
|
1934
|
-
}
|
|
1935
|
-
};
|
|
1936
|
-
const inline = (text) => {
|
|
1937
|
-
const codeTokens = [];
|
|
1938
|
-
let value = text.replace(/`([^`]+)`/g, (_match, code) => {
|
|
1939
|
-
const token = `@@SRTE_CODE_${codeTokens.length}@@`;
|
|
1940
|
-
codeTokens.push(`<code>${escapeHtml(code)}</code>`);
|
|
1941
|
-
return token;
|
|
1942
|
-
});
|
|
1943
|
-
value = escapeHtml(value)
|
|
1944
|
-
.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^"]+)")?\)/g, (_match, alt, src, title) => {
|
|
1945
|
-
const titleAttr = title ? ` title="${escapeHtmlAttribute(title)}"` : "";
|
|
1946
|
-
return `<img src="${escapeHtmlAttribute(src)}" alt="${escapeHtmlAttribute(alt)}"${titleAttr}>`;
|
|
1947
|
-
})
|
|
1948
|
-
.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"([^"]+)")?\)/g, (_match, label, href, title) => {
|
|
1949
|
-
const titleAttr = title ? ` title="${escapeHtmlAttribute(title)}"` : "";
|
|
1950
|
-
return `<a href="${escapeHtmlAttribute(href)}"${titleAttr}>${label}</a>`;
|
|
1951
|
-
})
|
|
1952
|
-
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
|
|
1953
|
-
.replace(/__([^_]+)__/g, "<strong>$1</strong>")
|
|
1954
|
-
.replace(/~~([^~]+)~~/g, "<s>$1</s>")
|
|
1955
|
-
.replace(/(^|[^*])\*([^*\n]+)\*/g, "$1<em>$2</em>")
|
|
1956
|
-
.replace(/(^|[^_])_([^_\n]+)_/g, "$1<em>$2</em>");
|
|
1957
|
-
codeTokens.forEach((replacement, index) => {
|
|
1958
|
-
value = value.replace(`@@SRTE_CODE_${index}@@`, replacement);
|
|
1959
|
-
});
|
|
1960
|
-
return value;
|
|
1961
|
-
};
|
|
1962
|
-
const closeParagraph = () => {
|
|
1963
|
-
if (!paragraph.length)
|
|
1964
|
-
return;
|
|
1965
|
-
html += `<p>${inline(paragraph.join(" "))}</p>`;
|
|
1966
|
-
paragraph = [];
|
|
1967
|
-
};
|
|
1968
|
-
const isTableSeparator = (line) => /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(line);
|
|
1969
|
-
const parseTableRow = (line) => {
|
|
1970
|
-
let value = line.trim();
|
|
1971
|
-
if (value.startsWith("|"))
|
|
1972
|
-
value = value.slice(1);
|
|
1973
|
-
if (value.endsWith("|"))
|
|
1974
|
-
value = value.slice(0, -1);
|
|
1975
|
-
return value.split("|").map((cell) => cell.trim());
|
|
1976
|
-
};
|
|
1977
|
-
const renderTable = (startIndex) => {
|
|
1978
|
-
const header = parseTableRow(lines[startIndex]);
|
|
1979
|
-
let index = startIndex + 2;
|
|
1980
|
-
const rows = [];
|
|
1981
|
-
while (index < lines.length && lines[index].includes("|") && lines[index].trim()) {
|
|
1982
|
-
rows.push(parseTableRow(lines[index]));
|
|
1983
|
-
index += 1;
|
|
1984
|
-
}
|
|
1985
|
-
const headHtml = `<thead><tr>${header.map((cell) => `<th>${inline(cell)}</th>`).join("")}</tr></thead>`;
|
|
1986
|
-
const bodyHtml = rows.length
|
|
1987
|
-
? `<tbody>${rows.map((row) => `<tr>${header.map((_cell, cellIndex) => `<td>${inline(row[cellIndex] || "")}</td>`).join("")}</tr>`).join("")}</tbody>`
|
|
1988
|
-
: "";
|
|
1989
|
-
html += `<table style="border-collapse: collapse; width: 100%; margin: 12px 0;">${headHtml}${bodyHtml}</table>`;
|
|
1990
|
-
return index;
|
|
1991
|
-
};
|
|
1992
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
1993
|
-
const line = lines[i];
|
|
1994
|
-
const trimmed = line.trim();
|
|
1995
|
-
const fence = /^```([A-Za-z0-9_-]+)?\s*$/.exec(trimmed);
|
|
1996
|
-
if (fence) {
|
|
1997
|
-
closeParagraph();
|
|
1998
|
-
closeList();
|
|
1999
|
-
if (codeFence) {
|
|
2000
|
-
const langClass = codeFence.lang ? ` class="language-${escapeHtmlAttribute(codeFence.lang)}"` : "";
|
|
2001
|
-
html += `<pre><code${langClass}>${escapeHtml(codeFence.lines.join("\n"))}</code></pre>`;
|
|
2002
|
-
codeFence = null;
|
|
2003
|
-
}
|
|
2004
|
-
else {
|
|
2005
|
-
codeFence = { lang: fence[1] || "", lines: [] };
|
|
2006
|
-
}
|
|
2007
|
-
continue;
|
|
2008
|
-
}
|
|
2009
|
-
if (codeFence) {
|
|
2010
|
-
codeFence.lines.push(line);
|
|
2011
|
-
continue;
|
|
2012
|
-
}
|
|
2013
|
-
if (!trimmed) {
|
|
2014
|
-
closeParagraph();
|
|
2015
|
-
closeList();
|
|
2016
|
-
continue;
|
|
2017
|
-
}
|
|
2018
|
-
if (/^(-{3,}|\*{3,}|_{3,})$/.test(trimmed)) {
|
|
2019
|
-
closeParagraph();
|
|
2020
|
-
closeList();
|
|
2021
|
-
html += "<hr>";
|
|
2022
|
-
continue;
|
|
2023
|
-
}
|
|
2024
|
-
if (i + 1 < lines.length && trimmed.includes("|") && isTableSeparator(lines[i + 1])) {
|
|
2025
|
-
closeParagraph();
|
|
2026
|
-
closeList();
|
|
2027
|
-
i = renderTable(i) - 1;
|
|
2028
|
-
continue;
|
|
2029
|
-
}
|
|
2030
|
-
const heading = /^(#{1,6})\s+(.+)$/.exec(trimmed);
|
|
2031
|
-
if (heading) {
|
|
2032
|
-
closeParagraph();
|
|
2033
|
-
closeList();
|
|
2034
|
-
const level = heading[1].length;
|
|
2035
|
-
html += `<h${level}>${inline(heading[2])}</h${level}>`;
|
|
2036
|
-
continue;
|
|
2037
|
-
}
|
|
2038
|
-
const bullet = /^[-*+]\s+(.+)$/.exec(trimmed);
|
|
2039
|
-
if (bullet) {
|
|
2040
|
-
closeParagraph();
|
|
2041
|
-
if (listType !== "ul") {
|
|
2042
|
-
closeList();
|
|
2043
|
-
html += "<ul>";
|
|
2044
|
-
listType = "ul";
|
|
2045
|
-
}
|
|
2046
|
-
html += `<li>${inline(bullet[1])}</li>`;
|
|
2047
|
-
continue;
|
|
2048
|
-
}
|
|
2049
|
-
const numbered = /^\d+[.)]\s+(.+)$/.exec(trimmed);
|
|
2050
|
-
if (numbered) {
|
|
2051
|
-
closeParagraph();
|
|
2052
|
-
if (listType !== "ol") {
|
|
2053
|
-
closeList();
|
|
2054
|
-
html += "<ol>";
|
|
2055
|
-
listType = "ol";
|
|
2056
|
-
}
|
|
2057
|
-
html += `<li>${inline(numbered[1])}</li>`;
|
|
2058
|
-
continue;
|
|
2059
|
-
}
|
|
2060
|
-
const quote = /^>\s?(.*)$/.exec(trimmed);
|
|
2061
|
-
if (quote) {
|
|
2062
|
-
closeParagraph();
|
|
2063
|
-
closeList();
|
|
2064
|
-
html += `<blockquote>${inline(quote[1]) || "<br>"}</blockquote>`;
|
|
2065
|
-
continue;
|
|
2066
|
-
}
|
|
2067
|
-
closeList();
|
|
2068
|
-
paragraph.push(trimmed);
|
|
2069
|
-
}
|
|
2070
|
-
if (codeFence) {
|
|
2071
|
-
const langClass = codeFence.lang ? ` class="language-${escapeHtmlAttribute(codeFence.lang)}"` : "";
|
|
2072
|
-
html += `<pre><code${langClass}>${escapeHtml(codeFence.lines.join("\n"))}</code></pre>`;
|
|
2073
|
-
}
|
|
2074
|
-
closeParagraph();
|
|
2075
|
-
closeList();
|
|
2076
|
-
const root = document.createElement("div");
|
|
2077
|
-
root.innerHTML = html;
|
|
2078
|
-
enhanceImportedTables(root);
|
|
2079
|
-
return root.innerHTML;
|
|
2080
|
-
};
|
|
2081
3161
|
const htmlToMarkdown = (html) => {
|
|
2082
3162
|
const root = document.createElement("div");
|
|
2083
3163
|
root.innerHTML = html;
|
|
@@ -2121,7 +3201,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
2121
3201
|
return;
|
|
2122
3202
|
const file = files[0];
|
|
2123
3203
|
const text = await file.text();
|
|
2124
|
-
const html = type === "html" ? text :
|
|
3204
|
+
const html = type === "html" ? text : markdownToCompatibilityHtml(text);
|
|
2125
3205
|
const el = editableRef.current;
|
|
2126
3206
|
const hasContent = el && el.textContent && el.textContent.trim().length > 0;
|
|
2127
3207
|
insertImportedHtml(html, hasContent ? "append" : "replace", {
|
|
@@ -2587,11 +3667,73 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
2587
3667
|
console.error("Error wrapping tables", e);
|
|
2588
3668
|
}
|
|
2589
3669
|
};
|
|
3670
|
+
const normalizeInvalidTableNesting = (root) => {
|
|
3671
|
+
const getOuterList = (item) => {
|
|
3672
|
+
let list = item.parentElement;
|
|
3673
|
+
while (list?.parentElement?.tagName === "LI") {
|
|
3674
|
+
const parentList = list.parentElement.parentElement;
|
|
3675
|
+
if (!parentList || !["UL", "OL"].includes(parentList.tagName))
|
|
3676
|
+
break;
|
|
3677
|
+
list = parentList;
|
|
3678
|
+
}
|
|
3679
|
+
return list;
|
|
3680
|
+
};
|
|
3681
|
+
root.querySelectorAll("table").forEach((table) => {
|
|
3682
|
+
const tableBlock = (table.closest('[data-table-wrapper="true"]') || table);
|
|
3683
|
+
const codeBlock = tableBlock.closest("pre");
|
|
3684
|
+
if (codeBlock?.parentElement) {
|
|
3685
|
+
codeBlock.parentElement.insertBefore(tableBlock, codeBlock.nextSibling);
|
|
3686
|
+
return;
|
|
3687
|
+
}
|
|
3688
|
+
const listItem = tableBlock.closest("li");
|
|
3689
|
+
if (!listItem)
|
|
3690
|
+
return;
|
|
3691
|
+
const outerList = getOuterList(listItem);
|
|
3692
|
+
if (outerList?.parentElement) {
|
|
3693
|
+
outerList.parentElement.insertBefore(tableBlock, outerList.nextSibling);
|
|
3694
|
+
}
|
|
3695
|
+
});
|
|
3696
|
+
};
|
|
3697
|
+
const normalizeInvalidQuoteNesting = (root) => {
|
|
3698
|
+
root.querySelectorAll("blockquote blockquote").forEach((quote) => {
|
|
3699
|
+
const outerQuote = quote.parentElement?.closest("blockquote");
|
|
3700
|
+
if (outerQuote?.parentElement) {
|
|
3701
|
+
outerQuote.parentElement.insertBefore(quote, outerQuote.nextSibling);
|
|
3702
|
+
}
|
|
3703
|
+
});
|
|
3704
|
+
root.querySelectorAll("p,h1,h2,h3,h4,h5,h6,pre").forEach((container) => {
|
|
3705
|
+
const nestedQuotes = Array.from(container.children).filter((child) => child.tagName === "BLOCKQUOTE");
|
|
3706
|
+
nestedQuotes.forEach((quote) => {
|
|
3707
|
+
container.parentElement?.insertBefore(quote, container.nextSibling);
|
|
3708
|
+
});
|
|
3709
|
+
if (nestedQuotes.length > 0 &&
|
|
3710
|
+
container.tagName === "P" &&
|
|
3711
|
+
!container.textContent?.trim() &&
|
|
3712
|
+
Array.from(container.children).every((child) => child.tagName === "BR")) {
|
|
3713
|
+
container.remove();
|
|
3714
|
+
}
|
|
3715
|
+
});
|
|
3716
|
+
};
|
|
3717
|
+
const normalizeInvalidCodeBlockNesting = (root) => {
|
|
3718
|
+
root.querySelectorAll("pre pre").forEach((codeBlock) => {
|
|
3719
|
+
const outerCodeBlock = codeBlock.parentElement?.closest("pre");
|
|
3720
|
+
if (outerCodeBlock?.parentElement) {
|
|
3721
|
+
outerCodeBlock.parentElement.insertBefore(codeBlock, outerCodeBlock.nextSibling);
|
|
3722
|
+
}
|
|
3723
|
+
});
|
|
3724
|
+
root.querySelectorAll("p,h1,h2,h3,h4,h5,h6,blockquote").forEach((container) => {
|
|
3725
|
+
const nestedCodeBlocks = Array.from(container.children).filter((child) => child.tagName === "PRE");
|
|
3726
|
+
nestedCodeBlocks.forEach((codeBlock) => {
|
|
3727
|
+
container.parentElement?.insertBefore(codeBlock, container.nextSibling);
|
|
3728
|
+
});
|
|
3729
|
+
});
|
|
3730
|
+
};
|
|
2590
3731
|
const isCaretBoundaryBlock = (node) => {
|
|
2591
3732
|
if (!(node instanceof HTMLElement))
|
|
2592
3733
|
return false;
|
|
2593
3734
|
const tag = node.tagName.toLowerCase();
|
|
2594
3735
|
return (tag === "blockquote" ||
|
|
3736
|
+
tag === "pre" ||
|
|
2595
3737
|
tag === "table" ||
|
|
2596
3738
|
node.getAttribute("data-table-wrapper") === "true");
|
|
2597
3739
|
};
|
|
@@ -2624,6 +3766,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
2624
3766
|
return;
|
|
2625
3767
|
// Auto-fix negative margins that might cause visibility issues
|
|
2626
3768
|
fixNegativeMargins(el);
|
|
3769
|
+
// Quotes are document blocks and cannot be nested by drag and drop.
|
|
3770
|
+
normalizeInvalidQuoteNesting(el);
|
|
3771
|
+
// Code blocks are document blocks and cannot be nested by drag and drop.
|
|
3772
|
+
normalizeInvalidCodeBlockNesting(el);
|
|
3773
|
+
// Tables are document-level blocks and must not remain inside code or list items.
|
|
3774
|
+
normalizeInvalidTableNesting(el);
|
|
2627
3775
|
// Ensure tables are wrapped for horizontal scrolling
|
|
2628
3776
|
ensureTableWrappers(el);
|
|
2629
3777
|
// Keep a reachable typing position around isolating blocks at document edges
|
|
@@ -3211,13 +4359,19 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3211
4359
|
if (image && editor.contains(image)) {
|
|
3212
4360
|
return (image.parentElement?.tagName === "A" ? image.parentElement : image);
|
|
3213
4361
|
}
|
|
4362
|
+
const listElement = element.closest("ul,ol");
|
|
4363
|
+
if (listElement && editor.contains(listElement))
|
|
4364
|
+
return listElement;
|
|
3214
4365
|
const tableElement = element.closest("table");
|
|
3215
4366
|
if (tableElement && editor.contains(tableElement)) {
|
|
3216
4367
|
return (tableElement.closest('[data-table-wrapper="true"]') || tableElement);
|
|
3217
4368
|
}
|
|
3218
|
-
const
|
|
3219
|
-
if (
|
|
3220
|
-
return
|
|
4369
|
+
const quoteElement = element.closest("blockquote");
|
|
4370
|
+
if (quoteElement && editor.contains(quoteElement))
|
|
4371
|
+
return quoteElement;
|
|
4372
|
+
const codeBlock = element.closest("pre");
|
|
4373
|
+
if (codeBlock && editor.contains(codeBlock))
|
|
4374
|
+
return codeBlock;
|
|
3221
4375
|
const block = element.closest('[data-table-wrapper="true"],blockquote,pre,p,h1,h2,h3,h4,h5,h6,div');
|
|
3222
4376
|
if (!block || block === editor || !editor.contains(block))
|
|
3223
4377
|
return null;
|
|
@@ -3237,8 +4391,13 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3237
4391
|
}
|
|
3238
4392
|
const targetRect = target.getBoundingClientRect();
|
|
3239
4393
|
const scrollRect = scroller.getBoundingClientRect();
|
|
4394
|
+
const isTableTarget = target.matches("table,[data-table-wrapper='true']") ||
|
|
4395
|
+
Boolean(target.querySelector("table"));
|
|
4396
|
+
const isListInsideCell = target.matches("ul,ol") &&
|
|
4397
|
+
Boolean(target.closest("td,th"));
|
|
3240
4398
|
const next = {
|
|
3241
|
-
|
|
4399
|
+
// Table handles sit on the table's left border so nested tables remain reachable.
|
|
4400
|
+
left: Math.max(4, targetRect.left - scrollRect.left + scroller.scrollLeft - (isTableTarget || isListInsideCell ? 12 : 30)),
|
|
3242
4401
|
top: targetRect.top - scrollRect.top + scroller.scrollTop,
|
|
3243
4402
|
height: Math.max(24, targetRect.height),
|
|
3244
4403
|
target,
|
|
@@ -3261,7 +4420,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3261
4420
|
dragHandleHideTimerRef.current = null;
|
|
3262
4421
|
if (!draggedBlockRef.current)
|
|
3263
4422
|
setDragHandle(null);
|
|
3264
|
-
},
|
|
4423
|
+
}, 350);
|
|
3265
4424
|
};
|
|
3266
4425
|
const getImageFromMovableBlock = (block) => {
|
|
3267
4426
|
if (block.tagName === "IMG")
|
|
@@ -3273,6 +4432,50 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3273
4432
|
if (!editor || !editor.contains(block))
|
|
3274
4433
|
return false;
|
|
3275
4434
|
const under = document.elementFromPoint(x, y);
|
|
4435
|
+
const draggedQuote = block.tagName === "BLOCKQUOTE";
|
|
4436
|
+
const draggedCodeBlock = block.tagName === "PRE";
|
|
4437
|
+
if (draggedQuote) {
|
|
4438
|
+
const underElement = under instanceof HTMLElement ? under : under?.parentElement;
|
|
4439
|
+
const quoteTarget = underElement?.closest("blockquote");
|
|
4440
|
+
if (quoteTarget && quoteTarget !== block) {
|
|
4441
|
+
let rootQuote = quoteTarget;
|
|
4442
|
+
while (rootQuote.parentElement?.closest("blockquote")) {
|
|
4443
|
+
rootQuote = rootQuote.parentElement.closest("blockquote");
|
|
4444
|
+
}
|
|
4445
|
+
const parent = rootQuote.parentElement;
|
|
4446
|
+
if (!parent)
|
|
4447
|
+
return false;
|
|
4448
|
+
const rect = rootQuote.getBoundingClientRect();
|
|
4449
|
+
if (rootQuote.contains(block) || y >= rect.top + rect.height / 2) {
|
|
4450
|
+
parent.insertBefore(block, rootQuote.nextSibling);
|
|
4451
|
+
}
|
|
4452
|
+
else {
|
|
4453
|
+
parent.insertBefore(block, rootQuote);
|
|
4454
|
+
}
|
|
4455
|
+
return true;
|
|
4456
|
+
}
|
|
4457
|
+
}
|
|
4458
|
+
if (draggedCodeBlock) {
|
|
4459
|
+
const underElement = under instanceof HTMLElement ? under : under?.parentElement;
|
|
4460
|
+
const codeTarget = underElement?.closest("pre");
|
|
4461
|
+
if (codeTarget && codeTarget !== block) {
|
|
4462
|
+
let rootCodeBlock = codeTarget;
|
|
4463
|
+
while (rootCodeBlock.parentElement?.closest("pre")) {
|
|
4464
|
+
rootCodeBlock = rootCodeBlock.parentElement.closest("pre");
|
|
4465
|
+
}
|
|
4466
|
+
const parent = rootCodeBlock.parentElement;
|
|
4467
|
+
if (!parent)
|
|
4468
|
+
return false;
|
|
4469
|
+
const rect = rootCodeBlock.getBoundingClientRect();
|
|
4470
|
+
if (rootCodeBlock.contains(block) || y >= rect.top + rect.height / 2) {
|
|
4471
|
+
parent.insertBefore(block, rootCodeBlock.nextSibling);
|
|
4472
|
+
}
|
|
4473
|
+
else {
|
|
4474
|
+
parent.insertBefore(block, rootCodeBlock);
|
|
4475
|
+
}
|
|
4476
|
+
return true;
|
|
4477
|
+
}
|
|
4478
|
+
}
|
|
3276
4479
|
const draggedImage = getImageFromMovableBlock(block);
|
|
3277
4480
|
const targetCell = draggedImage ? getClosestCell(under) : null;
|
|
3278
4481
|
if (draggedImage && targetCell && !block.contains(targetCell)) {
|
|
@@ -3303,6 +4506,78 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3303
4506
|
if (range && editor.contains(range.commonAncestorContainer)) {
|
|
3304
4507
|
if (block.contains(range.commonAncestorContainer))
|
|
3305
4508
|
return false;
|
|
4509
|
+
if (draggedQuote) {
|
|
4510
|
+
const element = range.commonAncestorContainer instanceof HTMLElement
|
|
4511
|
+
? range.commonAncestorContainer
|
|
4512
|
+
: range.commonAncestorContainer.parentElement;
|
|
4513
|
+
const container = element?.closest("p,h1,h2,h3,h4,h5,h6,pre");
|
|
4514
|
+
if (container?.parentElement) {
|
|
4515
|
+
const isEmpty = !container.textContent?.trim() &&
|
|
4516
|
+
Array.from(container.children).every((child) => child.tagName === "BR");
|
|
4517
|
+
if (isEmpty) {
|
|
4518
|
+
container.parentElement.insertBefore(block, container);
|
|
4519
|
+
container.remove();
|
|
4520
|
+
}
|
|
4521
|
+
else {
|
|
4522
|
+
const rect = container.getBoundingClientRect();
|
|
4523
|
+
container.parentElement.insertBefore(block, y < rect.top + rect.height / 2 ? container : container.nextSibling);
|
|
4524
|
+
}
|
|
4525
|
+
return true;
|
|
4526
|
+
}
|
|
4527
|
+
}
|
|
4528
|
+
if (draggedCodeBlock) {
|
|
4529
|
+
const element = range.commonAncestorContainer instanceof HTMLElement
|
|
4530
|
+
? range.commonAncestorContainer
|
|
4531
|
+
: range.commonAncestorContainer.parentElement;
|
|
4532
|
+
const codeTarget = element?.closest("pre");
|
|
4533
|
+
if (codeTarget?.parentElement) {
|
|
4534
|
+
let rootCodeBlock = codeTarget;
|
|
4535
|
+
while (rootCodeBlock.parentElement?.closest("pre")) {
|
|
4536
|
+
rootCodeBlock = rootCodeBlock.parentElement.closest("pre");
|
|
4537
|
+
}
|
|
4538
|
+
rootCodeBlock.parentElement.insertBefore(block, rootCodeBlock.nextSibling);
|
|
4539
|
+
return true;
|
|
4540
|
+
}
|
|
4541
|
+
const container = element?.closest("p,h1,h2,h3,h4,h5,h6,blockquote");
|
|
4542
|
+
if (container?.parentElement) {
|
|
4543
|
+
const isEmpty = !container.textContent?.trim() &&
|
|
4544
|
+
Array.from(container.children).every((child) => child.tagName === "BR");
|
|
4545
|
+
if (isEmpty) {
|
|
4546
|
+
container.parentElement.insertBefore(block, container);
|
|
4547
|
+
container.remove();
|
|
4548
|
+
}
|
|
4549
|
+
else {
|
|
4550
|
+
const rect = container.getBoundingClientRect();
|
|
4551
|
+
container.parentElement.insertBefore(block, y < rect.top + rect.height / 2 ? container : container.nextSibling);
|
|
4552
|
+
}
|
|
4553
|
+
return true;
|
|
4554
|
+
}
|
|
4555
|
+
}
|
|
4556
|
+
const isTableBlock = block.matches("table,[data-table-wrapper='true']") || Boolean(block.querySelector("table"));
|
|
4557
|
+
if (isTableBlock) {
|
|
4558
|
+
const element = range.commonAncestorContainer instanceof HTMLElement
|
|
4559
|
+
? range.commonAncestorContainer
|
|
4560
|
+
: range.commonAncestorContainer.parentElement;
|
|
4561
|
+
const codeBlock = element?.closest("pre");
|
|
4562
|
+
if (codeBlock?.parentElement) {
|
|
4563
|
+
codeBlock.parentElement.insertBefore(block, codeBlock.nextSibling);
|
|
4564
|
+
return true;
|
|
4565
|
+
}
|
|
4566
|
+
const listItem = element?.closest("li");
|
|
4567
|
+
if (listItem) {
|
|
4568
|
+
let outerList = listItem.parentElement;
|
|
4569
|
+
while (outerList?.parentElement?.tagName === "LI") {
|
|
4570
|
+
const parentList = outerList.parentElement.parentElement;
|
|
4571
|
+
if (!parentList || !["UL", "OL"].includes(parentList.tagName))
|
|
4572
|
+
break;
|
|
4573
|
+
outerList = parentList;
|
|
4574
|
+
}
|
|
4575
|
+
if (outerList?.parentElement) {
|
|
4576
|
+
outerList.parentElement.insertBefore(block, outerList.nextSibling);
|
|
4577
|
+
return true;
|
|
4578
|
+
}
|
|
4579
|
+
}
|
|
4580
|
+
}
|
|
3306
4581
|
range.insertNode(block);
|
|
3307
4582
|
return true;
|
|
3308
4583
|
}
|
|
@@ -3327,20 +4602,145 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3327
4602
|
return null;
|
|
3328
4603
|
return getMovableElementFromNode(element);
|
|
3329
4604
|
};
|
|
4605
|
+
const getMoveTarget = () => {
|
|
4606
|
+
const editor = editableRef.current;
|
|
4607
|
+
if (!editor)
|
|
4608
|
+
return null;
|
|
4609
|
+
const imageTarget = selectedImage?.parentElement?.tagName === "A"
|
|
4610
|
+
? selectedImage.parentElement
|
|
4611
|
+
: selectedImage;
|
|
4612
|
+
if (imageTarget && editor.contains(imageTarget))
|
|
4613
|
+
return imageTarget;
|
|
4614
|
+
const range = getSelectionRangeInEditor();
|
|
4615
|
+
let node = range?.commonAncestorContainer || null;
|
|
4616
|
+
if (node?.nodeType === Node.TEXT_NODE)
|
|
4617
|
+
node = node.parentNode;
|
|
4618
|
+
const element = node instanceof HTMLElement ? node : null;
|
|
4619
|
+
if (!element)
|
|
4620
|
+
return null;
|
|
4621
|
+
const listItem = element.closest("li");
|
|
4622
|
+
if (listItem && editor.contains(listItem))
|
|
4623
|
+
return listItem;
|
|
4624
|
+
const cell = getClosestCell(element);
|
|
4625
|
+
if (cell) {
|
|
4626
|
+
const block = element.closest("p,h1,h2,h3,h4,h5,h6,blockquote,pre,div");
|
|
4627
|
+
if (block && block !== cell && cell.contains(block))
|
|
4628
|
+
return block;
|
|
4629
|
+
return cell;
|
|
4630
|
+
}
|
|
4631
|
+
return getTopLevelMovableElement();
|
|
4632
|
+
};
|
|
4633
|
+
const outdentListItem = (item) => {
|
|
4634
|
+
const list = item.parentElement;
|
|
4635
|
+
const parentItem = list?.parentElement;
|
|
4636
|
+
const outerList = parentItem?.parentElement;
|
|
4637
|
+
if (!list ||
|
|
4638
|
+
!parentItem ||
|
|
4639
|
+
parentItem.tagName.toLowerCase() !== "li" ||
|
|
4640
|
+
!outerList ||
|
|
4641
|
+
!["ul", "ol"].includes(outerList.tagName.toLowerCase())) {
|
|
4642
|
+
return false;
|
|
4643
|
+
}
|
|
4644
|
+
outerList.insertBefore(item, parentItem.nextSibling);
|
|
4645
|
+
if (!list.querySelector("li"))
|
|
4646
|
+
list.remove();
|
|
4647
|
+
return true;
|
|
4648
|
+
};
|
|
3330
4649
|
const elementSibling = (element, direction) => {
|
|
3331
4650
|
let sibling = direction === "previous" ? element.previousSibling : element.nextSibling;
|
|
3332
|
-
while (sibling &&
|
|
4651
|
+
while (sibling &&
|
|
4652
|
+
((sibling.nodeType === Node.TEXT_NODE && !sibling.textContent?.trim()) ||
|
|
4653
|
+
(sibling instanceof HTMLElement && sibling.getAttribute("data-srte-caret-boundary") === "true"))) {
|
|
3333
4654
|
sibling = direction === "previous" ? sibling.previousSibling : sibling.nextSibling;
|
|
3334
4655
|
}
|
|
3335
4656
|
return sibling;
|
|
3336
4657
|
};
|
|
4658
|
+
const moveSelectedBlocks = (direction) => {
|
|
4659
|
+
const editor = editableRef.current;
|
|
4660
|
+
const range = getSelectionRangeInEditor();
|
|
4661
|
+
if (!editor || !range || range.collapsed)
|
|
4662
|
+
return false;
|
|
4663
|
+
const blocks = sortInDocumentOrder(getSelectedBlocks(range)).filter((block) => {
|
|
4664
|
+
if (!editor.contains(block) || !block.parentElement)
|
|
4665
|
+
return false;
|
|
4666
|
+
const parentBlock = block.parentElement.closest(blockSelector);
|
|
4667
|
+
return !parentBlock || parentBlock === editor;
|
|
4668
|
+
});
|
|
4669
|
+
if (blocks.length === 0)
|
|
4670
|
+
return false;
|
|
4671
|
+
const parent = blocks[0].parentElement;
|
|
4672
|
+
if (!parent || blocks.some((block) => block.parentElement !== parent))
|
|
4673
|
+
return false;
|
|
4674
|
+
pushEditorHistory();
|
|
4675
|
+
if (direction === "up") {
|
|
4676
|
+
const previous = elementSibling(blocks[0], "previous");
|
|
4677
|
+
if (previous && !blocks.includes(previous)) {
|
|
4678
|
+
blocks.forEach((block) => parent.insertBefore(block, previous));
|
|
4679
|
+
}
|
|
4680
|
+
}
|
|
4681
|
+
else if (direction === "down") {
|
|
4682
|
+
const next = elementSibling(blocks[blocks.length - 1], "next");
|
|
4683
|
+
if (next && !blocks.includes(next)) {
|
|
4684
|
+
const afterNext = next.nextSibling;
|
|
4685
|
+
blocks.forEach((block) => parent.insertBefore(block, afterNext));
|
|
4686
|
+
}
|
|
4687
|
+
}
|
|
4688
|
+
else {
|
|
4689
|
+
blocks.forEach((block) => {
|
|
4690
|
+
const current = parseInt(block.style.marginLeft || "0", 10) || 0;
|
|
4691
|
+
const nextMargin = direction === "right"
|
|
4692
|
+
? Math.min(current + 24, 240)
|
|
4693
|
+
: Math.max(current - 24, 0);
|
|
4694
|
+
block.style.marginLeft = nextMargin ? `${nextMargin}px` : "";
|
|
4695
|
+
});
|
|
4696
|
+
}
|
|
4697
|
+
const movedRange = document.createRange();
|
|
4698
|
+
movedRange.setStartBefore(blocks[0]);
|
|
4699
|
+
movedRange.setEndAfter(blocks[blocks.length - 1]);
|
|
4700
|
+
safeSelectRange(movedRange);
|
|
4701
|
+
savedRangeRef.current = movedRange.cloneRange();
|
|
4702
|
+
handleInput();
|
|
4703
|
+
requestAnimationFrame(updateActiveState);
|
|
4704
|
+
return true;
|
|
4705
|
+
};
|
|
3337
4706
|
const moveCurrentElement = (direction) => {
|
|
3338
4707
|
const editor = editableRef.current;
|
|
3339
|
-
|
|
4708
|
+
if (moveSelectedBlocks(direction))
|
|
4709
|
+
return;
|
|
4710
|
+
let target = getMoveTarget();
|
|
3340
4711
|
if (!editor || !target)
|
|
3341
4712
|
return;
|
|
3342
4713
|
pushEditorHistory();
|
|
3343
|
-
if (
|
|
4714
|
+
if (target.tagName === "TD" || target.tagName === "TH") {
|
|
4715
|
+
const cell = target;
|
|
4716
|
+
const paragraph = document.createElement("p");
|
|
4717
|
+
while (cell.firstChild)
|
|
4718
|
+
paragraph.appendChild(cell.firstChild);
|
|
4719
|
+
cell.appendChild(paragraph);
|
|
4720
|
+
target = paragraph;
|
|
4721
|
+
}
|
|
4722
|
+
if (target.tagName.toLowerCase() === "li") {
|
|
4723
|
+
const list = target.parentElement;
|
|
4724
|
+
if (!list)
|
|
4725
|
+
return;
|
|
4726
|
+
if (direction === "up") {
|
|
4727
|
+
const previous = elementSibling(target, "previous");
|
|
4728
|
+
if (previous?.nodeName === "LI")
|
|
4729
|
+
list.insertBefore(target, previous);
|
|
4730
|
+
}
|
|
4731
|
+
else if (direction === "down") {
|
|
4732
|
+
const next = elementSibling(target, "next");
|
|
4733
|
+
if (next?.nodeName === "LI")
|
|
4734
|
+
list.insertBefore(next, target);
|
|
4735
|
+
}
|
|
4736
|
+
else if (direction === "right") {
|
|
4737
|
+
nestSelectedListItems([target], list.tagName.toLowerCase());
|
|
4738
|
+
}
|
|
4739
|
+
else {
|
|
4740
|
+
outdentListItem(target);
|
|
4741
|
+
}
|
|
4742
|
+
}
|
|
4743
|
+
else if (direction === "up") {
|
|
3344
4744
|
const previous = elementSibling(target, "previous");
|
|
3345
4745
|
if (previous)
|
|
3346
4746
|
target.parentElement?.insertBefore(target, previous);
|
|
@@ -3488,7 +4888,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3488
4888
|
} }), _jsx("input", { ref: mdInputRef, type: "file", accept: ".md,.markdown,text/markdown,text/plain", style: { display: "none" }, onChange: (e) => {
|
|
3489
4889
|
importTextFile(e.currentTarget.files, "md");
|
|
3490
4890
|
e.currentTarget.value = "";
|
|
3491
|
-
} }), _jsxs("select", { value: currentBlockType, onMouseDown: preserveEditorSelection, onChange: (e) => {
|
|
4891
|
+
} }), _jsxs("select", { value: currentBlockType, onPointerDown: preserveEditorSelection, onMouseDown: preserveEditorSelection, onChange: (e) => {
|
|
3492
4892
|
const val = e.target.value;
|
|
3493
4893
|
if (val === "p")
|
|
3494
4894
|
applyFormatBlock("<p>");
|
|
@@ -3498,6 +4898,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3498
4898
|
applyFormatBlock("<h2>");
|
|
3499
4899
|
else if (val === "h3")
|
|
3500
4900
|
applyFormatBlock("<h3>");
|
|
4901
|
+
else if (val === "h4")
|
|
4902
|
+
applyFormatBlock("<h4>");
|
|
4903
|
+
else if (val === "h5")
|
|
4904
|
+
applyFormatBlock("<h5>");
|
|
4905
|
+
else if (val === "h6")
|
|
4906
|
+
applyFormatBlock("<h6>");
|
|
3501
4907
|
}, title: "Paragraph/Heading", style: {
|
|
3502
4908
|
height: 32,
|
|
3503
4909
|
padding: "0 8px",
|
|
@@ -3505,24 +4911,23 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3505
4911
|
borderRadius: 6,
|
|
3506
4912
|
background: "var(--srte-input-bg)",
|
|
3507
4913
|
color: "var(--srte-input-text)",
|
|
3508
|
-
}, children: [_jsx("option", { value: "
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
}, onChange: (e) => applyFontSize(e.target.value), title: "Font Size", style: {
|
|
4914
|
+
}, 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" })] }), [
|
|
4915
|
+
["left", "Left", "Align left"],
|
|
4916
|
+
["center", "Center", "Align center"],
|
|
4917
|
+
["right", "Right", "Align right"],
|
|
4918
|
+
["justify", "Justify", "Justify"],
|
|
4919
|
+
].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, {
|
|
4920
|
+
minWidth: 32,
|
|
4921
|
+
padding: "0 6px",
|
|
4922
|
+
fontSize: 10,
|
|
4923
|
+
}), 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: {
|
|
3519
4924
|
height: 32,
|
|
3520
4925
|
padding: "0 8px",
|
|
3521
4926
|
border: "1px solid var(--srte-input-border)",
|
|
3522
4927
|
borderRadius: 6,
|
|
3523
4928
|
background: "var(--srte-input-bg)",
|
|
3524
4929
|
color: "var(--srte-input-text)",
|
|
3525
|
-
}, 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: () => {
|
|
4930
|
+
}, 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: () => {
|
|
3526
4931
|
const sel = window.getSelection();
|
|
3527
4932
|
if (sel && sel.rangeCount > 0) {
|
|
3528
4933
|
const range = sel.getRangeAt(0);
|
|
@@ -3562,7 +4967,50 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3562
4967
|
borderRadius: 6,
|
|
3563
4968
|
background: "var(--srte-input-bg)",
|
|
3564
4969
|
color: "var(--srte-input-text)",
|
|
3565
|
-
}, children: _jsx("span", { style: { fontWeight: 700, padding: "1px 4px", background: "var(--srte-accent-bg)", borderRadius: 3 }, children: "A" }) }), _jsxs("button", { title: "Subscript", onClick: () =>
|
|
4970
|
+
}, 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" })] }), [
|
|
4971
|
+
{
|
|
4972
|
+
key: "check",
|
|
4973
|
+
label: "☐",
|
|
4974
|
+
title: "Checklist",
|
|
4975
|
+
active: activeState.checklist,
|
|
4976
|
+
action: () => applyChecklist(false, true),
|
|
4977
|
+
options: [["check:plain", "☐ Checklist"], ["check:strike", "☑ Checked + strike"]],
|
|
4978
|
+
},
|
|
4979
|
+
{
|
|
4980
|
+
key: "bullet",
|
|
4981
|
+
label: "•≡",
|
|
4982
|
+
title: "Bulleted list",
|
|
4983
|
+
active: activeState.unorderedList,
|
|
4984
|
+
action: () => applyListStyle("bullet:disc"),
|
|
4985
|
+
options: [["bullet:disc", "• Disc"], ["bullet:circle", "○ Circle"], ["bullet:square", "▪ Square"]],
|
|
4986
|
+
},
|
|
4987
|
+
{
|
|
4988
|
+
key: "ordered",
|
|
4989
|
+
label: "1≡",
|
|
4990
|
+
title: "Numbered list",
|
|
4991
|
+
active: activeState.orderedList,
|
|
4992
|
+
action: () => applyListStyle("ordered:decimal"),
|
|
4993
|
+
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."]],
|
|
4994
|
+
},
|
|
4995
|
+
].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) => {
|
|
4996
|
+
const selected = event.currentTarget.value;
|
|
4997
|
+
if (selected === "check:plain")
|
|
4998
|
+
applyChecklist(false);
|
|
4999
|
+
else if (selected === "check:strike")
|
|
5000
|
+
applyChecklist(true);
|
|
5001
|
+
else if (selected)
|
|
5002
|
+
applyListStyle(selected);
|
|
5003
|
+
event.currentTarget.value = "";
|
|
5004
|
+
}, style: {
|
|
5005
|
+
width: 28,
|
|
5006
|
+
height: 32,
|
|
5007
|
+
padding: 0,
|
|
5008
|
+
border: "1px solid var(--srte-input-border)",
|
|
5009
|
+
borderLeft: 0,
|
|
5010
|
+
borderRadius: "0 6px 6px 0",
|
|
5011
|
+
background: "var(--srte-input-bg)",
|
|
5012
|
+
color: "var(--srte-input-text)",
|
|
5013
|
+
}, 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: {
|
|
3566
5014
|
height: 32,
|
|
3567
5015
|
minWidth: 32,
|
|
3568
5016
|
padding: "0 8px",
|
|
@@ -3570,7 +5018,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3570
5018
|
borderRadius: 6,
|
|
3571
5019
|
background: "var(--srte-input-bg)",
|
|
3572
5020
|
color: "var(--srte-input-text)",
|
|
3573
|
-
}, children: "\u03A9" }), _jsx("button", { title: "Code block",
|
|
5021
|
+
}, children: "\u03A9" }), _jsx("button", { type: "button", title: "Code block", onPointerDown: preserveEditorSelection, onClick: toggleCodeBlock, "aria-pressed": activeState.codeBlock, style: activeButtonStyle(activeState.codeBlock, {
|
|
3574
5022
|
minWidth: 36,
|
|
3575
5023
|
fontFamily: "ui-monospace, SFMono-Regular, Menlo",
|
|
3576
5024
|
}), children: "< />" }), formula && (_jsx("button", { title: "Insert formula", onClick: () => setShowFormulaDialog(true), style: {
|
|
@@ -3581,21 +5029,18 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3581
5029
|
borderRadius: 6,
|
|
3582
5030
|
background: "var(--srte-input-bg)",
|
|
3583
5031
|
color: "var(--srte-input-text)",
|
|
3584
|
-
}, children: "\u2211" })), _jsx("button", { title: "Insert link", onClick:
|
|
3585
|
-
height: 32,
|
|
3586
|
-
padding: "0 10px",
|
|
3587
|
-
border: "1px solid var(--srte-input-border)",
|
|
3588
|
-
borderRadius: 6,
|
|
3589
|
-
background: "var(--srte-input-bg)",
|
|
3590
|
-
color: "var(--srte-input-text)",
|
|
3591
|
-
}, children: "Link" }), _jsx("button", { title: "Remove link", onClick: () => exec("unlink"), style: {
|
|
5032
|
+
}, 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: {
|
|
3592
5033
|
height: 32,
|
|
3593
|
-
|
|
5034
|
+
minWidth: 34,
|
|
5035
|
+
padding: "0 8px",
|
|
3594
5036
|
border: "1px solid var(--srte-input-border)",
|
|
3595
5037
|
borderRadius: 6,
|
|
3596
5038
|
background: "var(--srte-input-bg)",
|
|
3597
5039
|
color: "var(--srte-input-text)",
|
|
3598
|
-
|
|
5040
|
+
cursor: activeState.link ? "pointer" : "not-allowed",
|
|
5041
|
+
opacity: activeState.link ? 1 : 0.45,
|
|
5042
|
+
fontSize: 16,
|
|
5043
|
+
}, children: _jsx("span", { "aria-hidden": "true", children: "\u2197\u0338" }) }), media && (_jsxs(_Fragment, { children: [_jsx("button", { title: "Insert image", onClick: insertImage, style: {
|
|
3599
5044
|
height: 32,
|
|
3600
5045
|
padding: "0 10px",
|
|
3601
5046
|
border: "1px solid var(--srte-input-border)",
|
|
@@ -4084,14 +5529,29 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
4084
5529
|
boxSizing: "border-box",
|
|
4085
5530
|
position: "relative",
|
|
4086
5531
|
scrollPaddingBottom: 24,
|
|
4087
|
-
}, children: [_jsx("div", { ref: editableRef, contentEditable: !readOnly, suppressContentEditableWarning: true,
|
|
5532
|
+
}, children: [_jsx("div", { ref: editableRef, contentEditable: !readOnly, suppressContentEditableWarning: true, onBeforeInput: (e) => {
|
|
5533
|
+
const inputType = e.nativeEvent.inputType || "input";
|
|
5534
|
+
if (inputType === "historyUndo" || inputType === "historyRedo") {
|
|
5535
|
+
if (restoreEditorHistory(inputType === "historyUndo" ? "undo" : "redo")) {
|
|
5536
|
+
e.preventDefault();
|
|
5537
|
+
}
|
|
5538
|
+
return;
|
|
5539
|
+
}
|
|
5540
|
+
captureInputHistory(inputType);
|
|
5541
|
+
}, onInput: handleInput, onKeyUp: updateActiveState, onMouseUp: updateActiveState, onCompositionStart: () => (isComposingRef.current = true), onCompositionEnd: () => {
|
|
4088
5542
|
isComposingRef.current = false;
|
|
4089
5543
|
handleInput();
|
|
4090
5544
|
}, onMouseMove: (e) => {
|
|
4091
5545
|
if (draggedBlockRef.current)
|
|
4092
5546
|
return;
|
|
4093
|
-
updateDragHandleForTarget(getMovableElementFromNode(e.target));
|
|
4094
|
-
},
|
|
5547
|
+
updateDragHandleForTarget(isNode(e.target) ? getMovableElementFromNode(e.target) : null);
|
|
5548
|
+
}, onMouseOver: (e) => {
|
|
5549
|
+
if (draggedBlockRef.current)
|
|
5550
|
+
return;
|
|
5551
|
+
updateDragHandleForTarget(isNode(e.target) ? getMovableElementFromNode(e.target) : null);
|
|
5552
|
+
}, onMouseLeave: (e) => {
|
|
5553
|
+
if (closestFromTarget(e.relatedTarget, "[data-srte-drag-handle]"))
|
|
5554
|
+
return;
|
|
4095
5555
|
if (!draggedBlockRef.current)
|
|
4096
5556
|
scheduleDragHandleHide();
|
|
4097
5557
|
}, onPaste: (e) => {
|
|
@@ -4104,6 +5564,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
4104
5564
|
return;
|
|
4105
5565
|
}
|
|
4106
5566
|
}
|
|
5567
|
+
pushEditorHistory();
|
|
4107
5568
|
const html = e.clipboardData?.getData("text/html");
|
|
4108
5569
|
if (html) {
|
|
4109
5570
|
e.preventDefault();
|
|
@@ -4186,6 +5647,31 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
4186
5647
|
}
|
|
4187
5648
|
}, onClick: (e) => {
|
|
4188
5649
|
const t = e.target;
|
|
5650
|
+
if (t.dataset.srteCheck === "true") {
|
|
5651
|
+
const item = t.closest("li");
|
|
5652
|
+
const list = item?.closest('[data-srte-checklist="true"]');
|
|
5653
|
+
if (item && list) {
|
|
5654
|
+
pushEditorHistory();
|
|
5655
|
+
const checked = item.dataset.checked !== "true";
|
|
5656
|
+
item.dataset.checked = checked ? "true" : "false";
|
|
5657
|
+
t.textContent = checked ? "☑" : "☐";
|
|
5658
|
+
t.setAttribute("aria-label", checked ? "Mark incomplete" : "Mark complete");
|
|
5659
|
+
item.style.textDecoration =
|
|
5660
|
+
list.dataset.srteChecklistStrike === "true" && checked ? "line-through" : "";
|
|
5661
|
+
handleInput();
|
|
5662
|
+
return;
|
|
5663
|
+
}
|
|
5664
|
+
}
|
|
5665
|
+
const anchor = t?.closest("a");
|
|
5666
|
+
if (anchor && editableRef.current?.contains(anchor)) {
|
|
5667
|
+
e.preventDefault();
|
|
5668
|
+
e.stopPropagation();
|
|
5669
|
+
openLinkEditor(anchor);
|
|
5670
|
+
setTableMenu(null);
|
|
5671
|
+
setImageMenu(null);
|
|
5672
|
+
return;
|
|
5673
|
+
}
|
|
5674
|
+
setLinkMenu(null);
|
|
4189
5675
|
if (t && t.tagName === "IMG") {
|
|
4190
5676
|
setSelectedImage(t);
|
|
4191
5677
|
scheduleImageOverlay();
|
|
@@ -4237,6 +5723,11 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
4237
5723
|
}
|
|
4238
5724
|
updateActiveState();
|
|
4239
5725
|
}, onKeyDown: (e) => {
|
|
5726
|
+
if ((e.metaKey || e.ctrlKey) && String(e.key).toLowerCase() === "k") {
|
|
5727
|
+
e.preventDefault();
|
|
5728
|
+
openLinkEditor();
|
|
5729
|
+
return;
|
|
5730
|
+
}
|
|
4240
5731
|
if ((e.metaKey || e.ctrlKey) && String(e.key).toLowerCase() === "z") {
|
|
4241
5732
|
const restored = restoreEditorHistory(e.shiftKey ? "redo" : "undo");
|
|
4242
5733
|
if (restored) {
|
|
@@ -4261,13 +5752,29 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
4261
5752
|
// Keep Tab for indentation in lists; otherwise insert 2 spaces
|
|
4262
5753
|
if (e.key === "Tab") {
|
|
4263
5754
|
e.preventDefault();
|
|
4264
|
-
|
|
4265
|
-
|
|
5755
|
+
const selection = getSelectionRangeInEditor();
|
|
5756
|
+
const selectedListItems = selection && !selection.collapsed
|
|
5757
|
+
? getSelectedListItems(getSelectedBlocks(selection))
|
|
5758
|
+
: [];
|
|
5759
|
+
if (selectedListItems.length > 0) {
|
|
5760
|
+
if (!e.shiftKey) {
|
|
5761
|
+
nestListSelection();
|
|
5762
|
+
return;
|
|
5763
|
+
}
|
|
5764
|
+
pushEditorHistory();
|
|
5765
|
+
exec("outdent");
|
|
5766
|
+
return;
|
|
5767
|
+
}
|
|
5768
|
+
const currentBlock = getCurrentBlock();
|
|
5769
|
+
if (currentBlock?.closest("li")) {
|
|
5770
|
+
pushEditorHistory();
|
|
4266
5771
|
exec(e.shiftKey ? "outdent" : "indent");
|
|
4267
5772
|
}
|
|
4268
5773
|
else {
|
|
5774
|
+
captureInputHistory("insertText");
|
|
4269
5775
|
document.execCommand("insertText", false, " ");
|
|
4270
5776
|
}
|
|
5777
|
+
return;
|
|
4271
5778
|
}
|
|
4272
5779
|
// Table navigation with arrows inside cells
|
|
4273
5780
|
if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) {
|
|
@@ -4372,7 +5879,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
4372
5879
|
setTableMenu(null);
|
|
4373
5880
|
setImageMenu(null);
|
|
4374
5881
|
}
|
|
4375
|
-
} }), dragHandle && !readOnly && (_jsx("button", { type: "button", draggable: true, title: "Drag block", "aria-label": "Drag block", onMouseEnter: () => {
|
|
5882
|
+
} }), dragHandle && !readOnly && (_jsx("button", { type: "button", draggable: true, "data-srte-drag-handle": "true", title: "Drag block", "aria-label": "Drag block", onMouseEnter: () => {
|
|
4376
5883
|
if (dragHandleHideTimerRef.current != null) {
|
|
4377
5884
|
window.clearTimeout(dragHandleHideTimerRef.current);
|
|
4378
5885
|
dragHandleHideTimerRef.current = null;
|
|
@@ -4509,7 +6016,10 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
4509
6016
|
borderRadius: 2,
|
|
4510
6017
|
cursor: "ew-resize",
|
|
4511
6018
|
pointerEvents: "auto",
|
|
4512
|
-
} })] }))] }),
|
|
6019
|
+
} })] }))] }), 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 ? () => {
|
|
6020
|
+
openEditorLink(linkMenu.anchor);
|
|
6021
|
+
setLinkMenu(null);
|
|
6022
|
+
} : undefined, onRemove: linkMenu.anchor ? () => removeAnchorLink(linkMenu.anchor) : undefined, onCancel: () => setLinkMenu(null) })), tableMenu && (_jsx("div", { style: {
|
|
4513
6023
|
position: "fixed",
|
|
4514
6024
|
inset: 0,
|
|
4515
6025
|
zIndex: 60,
|