smartrte-react 0.2.10 → 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 +1400 -307
- package/dist/components/LinkEditorPopover.d.ts +20 -0
- package/dist/components/LinkEditorPopover.js +87 -0
- package/dist/standalone/editor.js +841 -0
- package/dist/theme.d.ts +1 -1
- package/dist/theme.js +61 -0
- package/package.json +12 -7
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef, useState } from "react";
|
|
3
3
|
import { MediaManager } from "./MediaManager.js";
|
|
4
|
+
import { LinkEditorPopover } from "./LinkEditorPopover.js";
|
|
4
5
|
import * as pdfjsLib from 'pdfjs-dist';
|
|
5
6
|
import mammoth from 'mammoth';
|
|
6
7
|
import JSZip from 'jszip';
|
|
8
|
+
import { applyLink, applyTextColor as coreApplyTextColor, markdownToCompatibilityHtml, removeLink, sanitizeLinkHref, toggleBold, toggleItalic, toggleSubscript, toggleSuperscript, toggleUnderline } from 'smartrte-core';
|
|
9
|
+
import { restoreSelectionToDom, selectionFromDom } from '../adapters/domSelectionBridge.js';
|
|
10
|
+
import { serializeSmartDocument, smartDocumentFromEditorRoot } from '../adapters/domSmartDocument.js';
|
|
11
|
+
import { isShadowModeEnabled, runShadowCommand } from '../adapters/shadowMode.js';
|
|
12
|
+
import { getCoreInlineMarkResult, isCoreInlineMarkEnabled } from '../adapters/inlineMarkCoreExecution.js';
|
|
13
|
+
import { closestFromTarget, isNode } from '../adapters/domTargets.js';
|
|
7
14
|
import { ensureStyleSheet } from '../theme.js';
|
|
8
15
|
// Initialize PDF.js worker
|
|
9
16
|
if (typeof window !== 'undefined') {
|
|
@@ -50,11 +57,13 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
50
57
|
const selectionRef = useRef(null);
|
|
51
58
|
const selectingRef = useRef(null);
|
|
52
59
|
const [imageMenu, setImageMenu] = useState(null);
|
|
60
|
+
const [linkMenu, setLinkMenu] = useState(null);
|
|
53
61
|
const [showMediaManager, setShowMediaManager] = useState(false);
|
|
54
62
|
const [showColorPicker, setShowColorPicker] = useState(false);
|
|
55
63
|
const [showSpecialChars, setShowSpecialChars] = useState(false);
|
|
56
64
|
const [colorPickerType, setColorPickerType] = useState('text');
|
|
57
65
|
const savedRangeRef = useRef(null);
|
|
66
|
+
const pendingFontSizeRef = useRef(null);
|
|
58
67
|
const inlineScriptCaretOverrideRef = useRef(null);
|
|
59
68
|
const historyRef = useRef({
|
|
60
69
|
undo: [],
|
|
@@ -64,6 +73,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
64
73
|
const [currentFontSize, setCurrentFontSize] = useState("");
|
|
65
74
|
const [currentFont, setCurrentFont] = useState("");
|
|
66
75
|
const [currentBlockType, setCurrentBlockType] = useState("p");
|
|
76
|
+
const [currentAlignment, setCurrentAlignment] = useState("left");
|
|
67
77
|
const [activeState, setActiveState] = useState({
|
|
68
78
|
bold: false,
|
|
69
79
|
italic: false,
|
|
@@ -71,10 +81,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
71
81
|
strikeThrough: false,
|
|
72
82
|
subscript: false,
|
|
73
83
|
superscript: false,
|
|
84
|
+
checklist: false,
|
|
74
85
|
unorderedList: false,
|
|
75
86
|
orderedList: false,
|
|
76
87
|
blockquote: false,
|
|
77
88
|
codeBlock: false,
|
|
89
|
+
link: false,
|
|
78
90
|
});
|
|
79
91
|
useEffect(() => {
|
|
80
92
|
const el = editableRef.current;
|
|
@@ -84,9 +96,17 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
84
96
|
if (typeof value === "string" && value !== el.innerHTML) {
|
|
85
97
|
el.innerHTML = value || "";
|
|
86
98
|
fixNegativeMargins(el);
|
|
99
|
+
normalizeInvalidQuoteNesting(el);
|
|
100
|
+
normalizeInvalidCodeBlockNesting(el);
|
|
101
|
+
normalizeInvalidTableNesting(el);
|
|
87
102
|
ensureTableWrappers(el);
|
|
103
|
+
ensureCaretBoundaryParagraphs(el);
|
|
88
104
|
addTableResizeHandles();
|
|
89
105
|
}
|
|
106
|
+
normalizeInvalidQuoteNesting(el);
|
|
107
|
+
normalizeInvalidCodeBlockNesting(el);
|
|
108
|
+
normalizeInvalidTableNesting(el);
|
|
109
|
+
ensureCaretBoundaryParagraphs(el);
|
|
90
110
|
// Suppress native context menu inside table cells at capture phase
|
|
91
111
|
const onCtx = (evt) => {
|
|
92
112
|
const target = evt.target;
|
|
@@ -100,6 +120,83 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
100
120
|
el.removeEventListener("contextmenu", onCtx, { capture: true });
|
|
101
121
|
};
|
|
102
122
|
}, [value]);
|
|
123
|
+
const parseFontSizePx = (value) => {
|
|
124
|
+
const match = /^([\d.]+)(px|pt)?$/i.exec(value.trim());
|
|
125
|
+
if (!match)
|
|
126
|
+
return null;
|
|
127
|
+
const numeric = Number(match[1]);
|
|
128
|
+
if (!Number.isFinite(numeric) || numeric <= 0)
|
|
129
|
+
return null;
|
|
130
|
+
return match[2]?.toLowerCase() === "pt" ? numeric * 4 / 3 : numeric;
|
|
131
|
+
};
|
|
132
|
+
const explicitFontSizeAt = (node) => {
|
|
133
|
+
let element = node instanceof HTMLElement ? node : node.parentElement;
|
|
134
|
+
while (element && element !== editableRef.current) {
|
|
135
|
+
const parsed = parseFontSizePx(element.style.fontSize);
|
|
136
|
+
if (parsed)
|
|
137
|
+
return parsed;
|
|
138
|
+
element = element.parentElement;
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
};
|
|
142
|
+
const normalizeFontSizeSpans = (root) => {
|
|
143
|
+
Array.from(root.querySelectorAll('span[style*="font-size"]')).reverse().forEach((span) => {
|
|
144
|
+
const parent = span.parentElement;
|
|
145
|
+
if (parent?.tagName === "SPAN" &&
|
|
146
|
+
parseFontSizePx(parent.style.fontSize) === parseFontSizePx(span.style.fontSize) &&
|
|
147
|
+
span.attributes.length === 1) {
|
|
148
|
+
while (span.firstChild)
|
|
149
|
+
parent.insertBefore(span.firstChild, span);
|
|
150
|
+
span.remove();
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
Array.from(root.querySelectorAll('span[style*="font-size"]')).forEach((span) => {
|
|
154
|
+
let next = span.nextElementSibling;
|
|
155
|
+
while (next?.tagName === "SPAN" &&
|
|
156
|
+
next.getAttribute("style") === span.getAttribute("style") &&
|
|
157
|
+
next.attributes.length === span.attributes.length) {
|
|
158
|
+
while (next.firstChild)
|
|
159
|
+
span.appendChild(next.firstChild);
|
|
160
|
+
const following = next.nextElementSibling;
|
|
161
|
+
next.remove();
|
|
162
|
+
next = following;
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
const resolveFontSizeForRange = (range) => {
|
|
167
|
+
const pending = pendingFontSizeRef.current;
|
|
168
|
+
if (range.collapsed && pending &&
|
|
169
|
+
range.startContainer === pending.container &&
|
|
170
|
+
range.startOffset === pending.offset)
|
|
171
|
+
return String(Math.round(pending.valuePx));
|
|
172
|
+
if (range.collapsed) {
|
|
173
|
+
const explicit = explicitFontSizeAt(range.startContainer);
|
|
174
|
+
return explicit ? String(Math.round(explicit)) : "";
|
|
175
|
+
}
|
|
176
|
+
const editor = editableRef.current;
|
|
177
|
+
if (!editor)
|
|
178
|
+
return "";
|
|
179
|
+
const sizes = new Set();
|
|
180
|
+
const walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT);
|
|
181
|
+
let node = walker.nextNode();
|
|
182
|
+
while (node) {
|
|
183
|
+
try {
|
|
184
|
+
if (range.intersectsNode(node) && node.textContent) {
|
|
185
|
+
const size = explicitFontSizeAt(node);
|
|
186
|
+
if (size)
|
|
187
|
+
sizes.add(Math.round(size));
|
|
188
|
+
else
|
|
189
|
+
sizes.add(0);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch { }
|
|
193
|
+
if (sizes.size > 1)
|
|
194
|
+
return "";
|
|
195
|
+
node = walker.nextNode();
|
|
196
|
+
}
|
|
197
|
+
const only = Array.from(sizes)[0];
|
|
198
|
+
return only ? String(only) : "";
|
|
199
|
+
};
|
|
103
200
|
const updateActiveState = () => {
|
|
104
201
|
const editor = editableRef.current;
|
|
105
202
|
if (!editor)
|
|
@@ -116,6 +213,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
116
213
|
const element = node instanceof HTMLElement ? node : null;
|
|
117
214
|
const block = element?.closest("p,h1,h2,h3,h4,h5,h6,li,blockquote,pre,div");
|
|
118
215
|
const tag = block?.tagName.toLowerCase();
|
|
216
|
+
const queryState = (command) => typeof document.queryCommandState === "function" && document.queryCommandState(command);
|
|
119
217
|
const scriptOverride = inlineScriptCaretOverrideRef.current;
|
|
120
218
|
const overrideApplies = Boolean(scriptOverride &&
|
|
121
219
|
range.collapsed &&
|
|
@@ -124,24 +222,50 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
124
222
|
if (scriptOverride && !overrideApplies) {
|
|
125
223
|
inlineScriptCaretOverrideRef.current = null;
|
|
126
224
|
}
|
|
127
|
-
const subscriptActive =
|
|
128
|
-
const superscriptActive =
|
|
129
|
-
|
|
225
|
+
const subscriptActive = queryState("subscript") || Boolean(element?.closest("sub"));
|
|
226
|
+
const superscriptActive = queryState("superscript") || Boolean(element?.closest("sup"));
|
|
227
|
+
if (!range.collapsed) {
|
|
228
|
+
const formattingBlocks = getSelectedBlocks(range);
|
|
229
|
+
[range.startContainer, range.endContainer].forEach((endpoint) => {
|
|
230
|
+
const endpointElement = endpoint instanceof HTMLElement ? endpoint : endpoint.parentElement;
|
|
231
|
+
const endpointBlock = endpointElement?.closest("p,h1,h2,h3,h4,h5,h6,li");
|
|
232
|
+
if (endpointBlock && editor.contains(endpointBlock) && !formattingBlocks.includes(endpointBlock)) {
|
|
233
|
+
formattingBlocks.push(endpointBlock);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
const types = new Set(formattingBlocks.map((selectedBlock) => {
|
|
237
|
+
const contentBlock = selectedBlock.tagName === "LI"
|
|
238
|
+
? selectedBlock.querySelector(":scope > p,:scope > h1,:scope > h2,:scope > h3,:scope > h4,:scope > h5,:scope > h6")
|
|
239
|
+
: selectedBlock;
|
|
240
|
+
const selectedTag = contentBlock?.tagName.toLowerCase() || "p";
|
|
241
|
+
return /^h[1-6]$/.test(selectedTag) ? selectedTag : "p";
|
|
242
|
+
}));
|
|
243
|
+
setCurrentBlockType(types.size > 1 ? "mixed" : (Array.from(types)[0] || "p"));
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
setCurrentBlockType(/^h[1-6]$/.test(tag || "") ? tag : "p");
|
|
247
|
+
}
|
|
248
|
+
setCurrentFontSize(resolveFontSizeForRange(range));
|
|
249
|
+
const alignmentTargets = getAlignmentTargets(range);
|
|
250
|
+
const alignments = new Set(alignmentTargets.map(readTextAlignment));
|
|
251
|
+
setCurrentAlignment(alignments.size > 1 ? "mixed" : Array.from(alignments)[0] || "left");
|
|
130
252
|
setActiveState({
|
|
131
|
-
bold:
|
|
132
|
-
italic:
|
|
133
|
-
underline:
|
|
134
|
-
strikeThrough:
|
|
253
|
+
bold: queryState("bold"),
|
|
254
|
+
italic: queryState("italic"),
|
|
255
|
+
underline: queryState("underline"),
|
|
256
|
+
strikeThrough: queryState("strikeThrough"),
|
|
135
257
|
subscript: overrideApplies && scriptOverride?.command === "subscript"
|
|
136
258
|
? false
|
|
137
259
|
: subscriptActive,
|
|
138
260
|
superscript: overrideApplies && scriptOverride?.command === "superscript"
|
|
139
261
|
? false
|
|
140
262
|
: superscriptActive,
|
|
141
|
-
|
|
263
|
+
checklist: Boolean(element?.closest('[data-srte-checklist="true"]')),
|
|
264
|
+
unorderedList: Boolean(element?.closest("ul:not([data-srte-checklist=\"true\"])")),
|
|
142
265
|
orderedList: Boolean(element?.closest("ol")),
|
|
143
266
|
blockquote: Boolean(element?.closest("blockquote")),
|
|
144
267
|
codeBlock: Boolean(element?.closest("pre")),
|
|
268
|
+
link: Boolean(element?.closest("a")),
|
|
145
269
|
});
|
|
146
270
|
}
|
|
147
271
|
catch { }
|
|
@@ -154,6 +278,10 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
154
278
|
const range = sel.getRangeAt(0);
|
|
155
279
|
const editor = editableRef.current;
|
|
156
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;
|
|
157
285
|
savedRangeRef.current = range.cloneRange();
|
|
158
286
|
updateActiveState();
|
|
159
287
|
}
|
|
@@ -182,6 +310,52 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
182
310
|
return;
|
|
183
311
|
}
|
|
184
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;
|
|
185
359
|
const beforeHtml = editableRef.current?.innerHTML || "";
|
|
186
360
|
const ok = document.execCommand(command, false, valueArg);
|
|
187
361
|
const afterHtml = editableRef.current?.innerHTML || "";
|
|
@@ -191,6 +365,16 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
191
365
|
if (command === "formatBlock" && valueArg)
|
|
192
366
|
applyFormatBlockFallback(valueArg);
|
|
193
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
|
+
}
|
|
194
378
|
handleInput();
|
|
195
379
|
}
|
|
196
380
|
catch { }
|
|
@@ -268,16 +452,16 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
268
452
|
if (!restoreSavedSelection()) {
|
|
269
453
|
safeSelectRange(getSelectionRangeInEditor());
|
|
270
454
|
}
|
|
455
|
+
pushEditorHistory();
|
|
271
456
|
if (applyFormatBlockFallback(blockName)) {
|
|
272
457
|
const tag = normalizeBlockTag(blockName);
|
|
273
|
-
if (tag === "p" || tag
|
|
458
|
+
if (tag === "p" || /^h[1-6]$/.test(tag || "")) {
|
|
274
459
|
setCurrentBlockType(tag);
|
|
275
460
|
}
|
|
276
461
|
handleInput();
|
|
277
462
|
requestAnimationFrame(updateActiveState);
|
|
278
463
|
return;
|
|
279
464
|
}
|
|
280
|
-
exec("formatBlock", blockName);
|
|
281
465
|
}
|
|
282
466
|
catch { }
|
|
283
467
|
};
|
|
@@ -349,7 +533,9 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
349
533
|
to.push(currentHtml);
|
|
350
534
|
editor.innerHTML = html;
|
|
351
535
|
fixNegativeMargins(editor);
|
|
536
|
+
normalizeInvalidCodeBlockNesting(editor);
|
|
352
537
|
ensureTableWrappers(editor);
|
|
538
|
+
ensureCaretBoundaryParagraphs(editor);
|
|
353
539
|
addTableResizeHandles();
|
|
354
540
|
clearSelectionDecor();
|
|
355
541
|
setTableMenu(null);
|
|
@@ -398,18 +584,111 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
398
584
|
savedRangeRef.current = range.cloneRange();
|
|
399
585
|
}
|
|
400
586
|
};
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
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)
|
|
612
|
+
return;
|
|
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(" ");
|
|
404
673
|
return;
|
|
405
|
-
|
|
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);
|
|
406
685
|
};
|
|
407
686
|
const openEditorLink = (anchor) => {
|
|
408
|
-
const
|
|
409
|
-
if (!
|
|
687
|
+
const safeHref = sanitizeLinkHref(anchor.getAttribute("href"));
|
|
688
|
+
if (!safeHref)
|
|
410
689
|
return;
|
|
411
690
|
try {
|
|
412
|
-
const url = new URL(
|
|
691
|
+
const url = new URL(safeHref, window.location.href);
|
|
413
692
|
if (!["http:", "https:", "mailto:", "tel:"].includes(url.protocol))
|
|
414
693
|
return;
|
|
415
694
|
const target = anchor.getAttribute("target") || "_blank";
|
|
@@ -442,12 +721,22 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
442
721
|
const range = getSelectionRangeInEditor();
|
|
443
722
|
if (!editor || !range)
|
|
444
723
|
return null;
|
|
445
|
-
let node = range.
|
|
724
|
+
let node = range.startContainer;
|
|
446
725
|
if (node.nodeType === Node.TEXT_NODE)
|
|
447
726
|
node = node.parentNode;
|
|
448
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
|
+
}
|
|
449
738
|
const block = element?.closest("p,h1,h2,h3,h4,h5,h6,li,blockquote,pre,div");
|
|
450
|
-
if (!block || block === editor || !editor.contains(block))
|
|
739
|
+
if (!block || block === editor || block.getAttribute("data-table-wrapper") === "true" || !editor.contains(block))
|
|
451
740
|
return null;
|
|
452
741
|
return block;
|
|
453
742
|
};
|
|
@@ -456,22 +745,43 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
456
745
|
const editor = editableRef.current;
|
|
457
746
|
if (!editor)
|
|
458
747
|
return [];
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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")
|
|
462
759
|
return false;
|
|
463
760
|
const parentBlock = block.parentElement?.closest(blockSelector);
|
|
464
|
-
if (parentBlock && parentBlock
|
|
761
|
+
if (parentBlock && scope.contains(parentBlock) && parentBlock !== scope)
|
|
465
762
|
return false;
|
|
466
763
|
try {
|
|
467
|
-
|
|
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;
|
|
468
778
|
}
|
|
469
779
|
catch {
|
|
470
780
|
return false;
|
|
471
781
|
}
|
|
472
782
|
});
|
|
473
783
|
if (blocks.length > 0)
|
|
474
|
-
return blocks;
|
|
784
|
+
return sortInDocumentOrder(blocks);
|
|
475
785
|
const current = getCurrentBlock();
|
|
476
786
|
return current ? [current] : [];
|
|
477
787
|
};
|
|
@@ -489,6 +799,67 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
489
799
|
});
|
|
490
800
|
return items;
|
|
491
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
|
+
};
|
|
492
863
|
const copyCellOrBlockStyles = (from, to) => {
|
|
493
864
|
to.innerHTML = from.innerHTML || "<br>";
|
|
494
865
|
const style = from.getAttribute("style");
|
|
@@ -555,6 +926,39 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
555
926
|
block.parentElement?.replaceChild(replacement, block);
|
|
556
927
|
return replacement;
|
|
557
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
|
+
};
|
|
558
962
|
const applyFormatBlockFallback = (blockName) => {
|
|
559
963
|
const editor = editableRef.current;
|
|
560
964
|
const range = getSelectionRangeInEditor();
|
|
@@ -570,23 +974,36 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
570
974
|
}
|
|
571
975
|
if (range.collapsed) {
|
|
572
976
|
const block = getCurrentBlock();
|
|
573
|
-
if (!block || block === editor ||
|
|
977
|
+
if (!block || block === editor || !block.parentElement)
|
|
574
978
|
return false;
|
|
575
|
-
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);
|
|
576
983
|
focusElementEnd(replacement);
|
|
577
984
|
return true;
|
|
578
985
|
}
|
|
579
986
|
const selectedBlocks = sortInDocumentOrder(getSelectedBlocks(range)).filter((block) => {
|
|
580
987
|
if (!editor.contains(block) || block === editor)
|
|
581
988
|
return false;
|
|
582
|
-
if (block.closest("ul,ol"))
|
|
583
|
-
return false;
|
|
584
989
|
return Boolean(block.parentElement);
|
|
585
990
|
});
|
|
586
991
|
if (selectedBlocks.length > 0) {
|
|
587
992
|
let lastReplacement = null;
|
|
993
|
+
const handledItems = new Set();
|
|
588
994
|
selectedBlocks.forEach((block) => {
|
|
589
|
-
|
|
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);
|
|
590
1007
|
});
|
|
591
1008
|
if (lastReplacement)
|
|
592
1009
|
focusElementEnd(lastReplacement);
|
|
@@ -606,6 +1023,64 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
606
1023
|
Array.from(list.attributes).forEach((attr) => clone.setAttribute(attr.name, attr.value));
|
|
607
1024
|
return clone;
|
|
608
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
|
+
};
|
|
609
1084
|
const focusElementEnd = (element) => {
|
|
610
1085
|
const range = document.createRange();
|
|
611
1086
|
range.selectNodeContents(element);
|
|
@@ -632,6 +1107,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
632
1107
|
return;
|
|
633
1108
|
const paragraph = document.createElement("p");
|
|
634
1109
|
paragraph.innerHTML = li.innerHTML || "<br>";
|
|
1110
|
+
paragraph.style.textAlign = li.style.textAlign;
|
|
635
1111
|
const beforeList = cloneListShell(list);
|
|
636
1112
|
const afterList = cloneListShell(list);
|
|
637
1113
|
while (list.firstChild && list.firstChild !== li) {
|
|
@@ -721,6 +1197,50 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
721
1197
|
requestAnimationFrame(updateActiveState);
|
|
722
1198
|
}
|
|
723
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
|
+
};
|
|
724
1244
|
const applyListStyle = (value) => {
|
|
725
1245
|
const listTag = value.startsWith("ordered:") ? "ol" : "ul";
|
|
726
1246
|
const styleType = value.replace(/^(ordered|bullet):/, "");
|
|
@@ -729,6 +1249,19 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
729
1249
|
const range = getSelectionRangeInEditor();
|
|
730
1250
|
if (!range)
|
|
731
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
|
+
}
|
|
732
1265
|
const lists = new Set();
|
|
733
1266
|
getSelectedListItems(getSelectedBlocks(range)).forEach((item) => {
|
|
734
1267
|
const list = item.parentElement;
|
|
@@ -744,15 +1277,40 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
744
1277
|
lists.add(list);
|
|
745
1278
|
});
|
|
746
1279
|
if (lists.size === 0) {
|
|
747
|
-
const
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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
|
+
}
|
|
753
1298
|
handleInput();
|
|
1299
|
+
requestAnimationFrame(updateActiveState);
|
|
754
1300
|
return;
|
|
755
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;
|
|
756
1314
|
const currentList = block.closest("ul,ol");
|
|
757
1315
|
if (currentList)
|
|
758
1316
|
lists.add(currentList);
|
|
@@ -761,6 +1319,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
761
1319
|
return;
|
|
762
1320
|
pushEditorHistory();
|
|
763
1321
|
let lastList = null;
|
|
1322
|
+
const styledLists = [];
|
|
764
1323
|
lists.forEach((list) => {
|
|
765
1324
|
const target = list.tagName.toLowerCase() === listTag
|
|
766
1325
|
? list
|
|
@@ -769,8 +1328,15 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
769
1328
|
target.innerHTML = list.innerHTML;
|
|
770
1329
|
list.parentElement?.replaceChild(target, list);
|
|
771
1330
|
}
|
|
1331
|
+
clearChecklist(target);
|
|
772
1332
|
target.style.listStyleType = styleType;
|
|
773
1333
|
lastList = target;
|
|
1334
|
+
styledLists.push(target);
|
|
1335
|
+
});
|
|
1336
|
+
styledLists.forEach((list) => {
|
|
1337
|
+
if (editableRef.current?.contains(list)) {
|
|
1338
|
+
lastList = mergeAdjacentCompatibleLists(list);
|
|
1339
|
+
}
|
|
774
1340
|
});
|
|
775
1341
|
const lastItem = lastList?.lastElementChild;
|
|
776
1342
|
if (lastItem)
|
|
@@ -778,7 +1344,54 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
778
1344
|
handleInput();
|
|
779
1345
|
requestAnimationFrame(updateActiveState);
|
|
780
1346
|
};
|
|
781
|
-
const
|
|
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) => {
|
|
782
1395
|
const editor = editableRef.current;
|
|
783
1396
|
if (!editor || blocks.length === 0)
|
|
784
1397
|
return false;
|
|
@@ -809,19 +1422,130 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
809
1422
|
return position & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
|
|
810
1423
|
});
|
|
811
1424
|
const list = document.createElement(listTag);
|
|
1425
|
+
if (styleType)
|
|
1426
|
+
list.style.listStyleType = styleType;
|
|
812
1427
|
parent.insertBefore(list, group[0]);
|
|
813
1428
|
group.forEach((block) => {
|
|
814
1429
|
const li = document.createElement("li");
|
|
815
1430
|
li.innerHTML = block.innerHTML || "<br>";
|
|
1431
|
+
li.style.textAlign = block.style.textAlign;
|
|
816
1432
|
list.appendChild(li);
|
|
817
1433
|
block.remove();
|
|
818
1434
|
lastLi = li;
|
|
819
1435
|
});
|
|
1436
|
+
mergeAdjacentCompatibleLists(list);
|
|
820
1437
|
});
|
|
821
1438
|
if (lastLi)
|
|
822
1439
|
focusElementEnd(lastLi);
|
|
823
1440
|
return true;
|
|
824
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
|
+
};
|
|
825
1549
|
const transformSelectedListItems = (items, listTag) => {
|
|
826
1550
|
const editor = editableRef.current;
|
|
827
1551
|
if (!editor || items.length === 0)
|
|
@@ -860,6 +1584,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
860
1584
|
flushPendingList();
|
|
861
1585
|
const paragraph = document.createElement("p");
|
|
862
1586
|
paragraph.innerHTML = child.innerHTML || "<br>";
|
|
1587
|
+
paragraph.style.textAlign = child.style.textAlign;
|
|
863
1588
|
parent.insertBefore(paragraph, list);
|
|
864
1589
|
lastTarget = paragraph;
|
|
865
1590
|
child.remove();
|
|
@@ -919,6 +1644,21 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
919
1644
|
requestAnimationFrame(updateActiveState);
|
|
920
1645
|
return;
|
|
921
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
|
+
}
|
|
922
1662
|
}
|
|
923
1663
|
const block = getCurrentBlock();
|
|
924
1664
|
if (!block) {
|
|
@@ -1006,19 +1746,69 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1006
1746
|
const selected = sortInDocumentOrder(blocks).filter((block) => {
|
|
1007
1747
|
if (!editor.contains(block) || block === editor)
|
|
1008
1748
|
return false;
|
|
1009
|
-
if (block.closest("ul,ol"))
|
|
1010
|
-
return false;
|
|
1011
1749
|
if (block.tagName.toLowerCase() === "blockquote")
|
|
1012
1750
|
return false;
|
|
1013
1751
|
return Boolean(block.parentElement);
|
|
1014
1752
|
});
|
|
1015
1753
|
if (selected.length === 0)
|
|
1016
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)));
|
|
1017
1774
|
let lastWrapped = null;
|
|
1018
|
-
|
|
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) => {
|
|
1019
1808
|
const quote = document.createElement("blockquote");
|
|
1020
1809
|
block.parentElement?.insertBefore(quote, block);
|
|
1021
1810
|
quote.appendChild(block);
|
|
1811
|
+
mergeQuote(quote);
|
|
1022
1812
|
lastWrapped = block;
|
|
1023
1813
|
});
|
|
1024
1814
|
if (lastWrapped)
|
|
@@ -1044,93 +1834,243 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1044
1834
|
const block = getCurrentBlock();
|
|
1045
1835
|
if (block) {
|
|
1046
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
|
+
}
|
|
1047
1847
|
if (!wrapBlocks([block]))
|
|
1048
1848
|
return;
|
|
1049
1849
|
handleInput();
|
|
1050
1850
|
requestAnimationFrame(updateActiveState);
|
|
1051
1851
|
return;
|
|
1052
1852
|
}
|
|
1053
|
-
}
|
|
1054
|
-
else {
|
|
1055
|
-
const blocks = getSelectedBlocks(range);
|
|
1056
|
-
if (blocks.length > 0) {
|
|
1057
|
-
pushEditorHistory();
|
|
1058
|
-
if (!wrapBlocks(blocks))
|
|
1059
|
-
return;
|
|
1060
|
-
handleInput();
|
|
1061
|
-
requestAnimationFrame(updateActiveState);
|
|
1853
|
+
}
|
|
1854
|
+
else {
|
|
1855
|
+
const blocks = getSelectedBlocks(range);
|
|
1856
|
+
if (blocks.length > 0) {
|
|
1857
|
+
pushEditorHistory();
|
|
1858
|
+
if (!wrapBlocks(blocks))
|
|
1859
|
+
return;
|
|
1860
|
+
handleInput();
|
|
1861
|
+
requestAnimationFrame(updateActiveState);
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
pushEditorHistory();
|
|
1866
|
+
exec("formatBlock", "<blockquote>");
|
|
1867
|
+
}
|
|
1868
|
+
catch { }
|
|
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;
|
|
1062
1904
|
return;
|
|
1063
1905
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
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);
|
|
1067
1948
|
}
|
|
1068
1949
|
catch { }
|
|
1069
1950
|
};
|
|
1070
1951
|
const applyFontSize = (size) => {
|
|
1071
1952
|
try {
|
|
1072
|
-
// Update current font size state
|
|
1073
|
-
setCurrentFontSize(size);
|
|
1074
1953
|
const editor = editableRef.current;
|
|
1075
1954
|
if (!editor)
|
|
1076
1955
|
return;
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
// Use current range if it's within our editor
|
|
1084
|
-
if (editor.contains(currentRange.commonAncestorContainer)) {
|
|
1085
|
-
range = currentRange;
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
// Fallback to saved range if current range is not available
|
|
1089
|
-
if (!range && savedRangeRef.current) {
|
|
1090
|
-
range = savedRangeRef.current.cloneRange();
|
|
1091
|
-
}
|
|
1092
|
-
// If no range at all, just update state for future typing
|
|
1956
|
+
const valuePx = Number(size);
|
|
1957
|
+
if (!Number.isFinite(valuePx) || valuePx <= 0)
|
|
1958
|
+
return;
|
|
1959
|
+
if (!restoreSavedSelection())
|
|
1960
|
+
safeSelectRange(getSelectionRangeInEditor());
|
|
1961
|
+
const range = getSelectionRangeInEditor();
|
|
1093
1962
|
if (!range)
|
|
1094
1963
|
return;
|
|
1095
|
-
|
|
1964
|
+
setCurrentFontSize(String(Math.round(valuePx)));
|
|
1096
1965
|
if (range.collapsed) {
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
const newRange = document.createRange();
|
|
1104
|
-
newRange.setStart(span.firstChild, 1);
|
|
1105
|
-
newRange.collapse(true);
|
|
1106
|
-
if (sel) {
|
|
1107
|
-
sel.removeAllRanges();
|
|
1108
|
-
sel.addRange(newRange);
|
|
1109
|
-
}
|
|
1110
|
-
handleInput();
|
|
1966
|
+
pendingFontSizeRef.current = {
|
|
1967
|
+
valuePx,
|
|
1968
|
+
container: range.startContainer,
|
|
1969
|
+
offset: range.startOffset,
|
|
1970
|
+
};
|
|
1971
|
+
savedRangeRef.current = range.cloneRange();
|
|
1111
1972
|
return;
|
|
1112
1973
|
}
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
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();
|
|
1126
2021
|
}
|
|
1127
|
-
// Trigger change event
|
|
1128
2022
|
handleInput();
|
|
2023
|
+
requestAnimationFrame(updateActiveState);
|
|
1129
2024
|
}
|
|
1130
2025
|
catch (error) {
|
|
1131
2026
|
console.error('Error applying font size:', error);
|
|
1132
2027
|
}
|
|
1133
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
|
+
}, []);
|
|
1134
2074
|
const applyFontFamily = (font) => {
|
|
1135
2075
|
try {
|
|
1136
2076
|
setCurrentFont(font);
|
|
@@ -2218,164 +3158,6 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
2218
3158
|
.replace(/&/g, "&")
|
|
2219
3159
|
.replace(/</g, "<")
|
|
2220
3160
|
.replace(/>/g, ">");
|
|
2221
|
-
const escapeHtmlAttribute = (value) => escapeHtml(value).replace(/"/g, """);
|
|
2222
|
-
const markdownToHtml = (markdown) => {
|
|
2223
|
-
const lines = markdown.replace(/\r\n/g, "\n").split("\n");
|
|
2224
|
-
let html = "";
|
|
2225
|
-
let listType = null;
|
|
2226
|
-
let paragraph = [];
|
|
2227
|
-
let codeFence = null;
|
|
2228
|
-
const closeList = () => {
|
|
2229
|
-
if (listType) {
|
|
2230
|
-
html += `</${listType}>`;
|
|
2231
|
-
listType = null;
|
|
2232
|
-
}
|
|
2233
|
-
};
|
|
2234
|
-
const inline = (text) => {
|
|
2235
|
-
const codeTokens = [];
|
|
2236
|
-
let value = text.replace(/`([^`]+)`/g, (_match, code) => {
|
|
2237
|
-
const token = `@@SRTE_CODE_${codeTokens.length}@@`;
|
|
2238
|
-
codeTokens.push(`<code>${escapeHtml(code)}</code>`);
|
|
2239
|
-
return token;
|
|
2240
|
-
});
|
|
2241
|
-
value = escapeHtml(value)
|
|
2242
|
-
.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^"]+)")?\)/g, (_match, alt, src, title) => {
|
|
2243
|
-
const titleAttr = title ? ` title="${escapeHtmlAttribute(title)}"` : "";
|
|
2244
|
-
return `<img src="${escapeHtmlAttribute(src)}" alt="${escapeHtmlAttribute(alt)}"${titleAttr}>`;
|
|
2245
|
-
})
|
|
2246
|
-
.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"([^"]+)")?\)/g, (_match, label, href, title) => {
|
|
2247
|
-
const titleAttr = title ? ` title="${escapeHtmlAttribute(title)}"` : "";
|
|
2248
|
-
return `<a href="${escapeHtmlAttribute(href)}"${titleAttr}>${label}</a>`;
|
|
2249
|
-
})
|
|
2250
|
-
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
|
|
2251
|
-
.replace(/__([^_]+)__/g, "<strong>$1</strong>")
|
|
2252
|
-
.replace(/~~([^~]+)~~/g, "<s>$1</s>")
|
|
2253
|
-
.replace(/(^|[^*])\*([^*\n]+)\*/g, "$1<em>$2</em>")
|
|
2254
|
-
.replace(/(^|[^_])_([^_\n]+)_/g, "$1<em>$2</em>");
|
|
2255
|
-
codeTokens.forEach((replacement, index) => {
|
|
2256
|
-
value = value.replace(`@@SRTE_CODE_${index}@@`, replacement);
|
|
2257
|
-
});
|
|
2258
|
-
return value;
|
|
2259
|
-
};
|
|
2260
|
-
const closeParagraph = () => {
|
|
2261
|
-
if (!paragraph.length)
|
|
2262
|
-
return;
|
|
2263
|
-
html += `<p>${inline(paragraph.join(" "))}</p>`;
|
|
2264
|
-
paragraph = [];
|
|
2265
|
-
};
|
|
2266
|
-
const isTableSeparator = (line) => /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(line);
|
|
2267
|
-
const parseTableRow = (line) => {
|
|
2268
|
-
let value = line.trim();
|
|
2269
|
-
if (value.startsWith("|"))
|
|
2270
|
-
value = value.slice(1);
|
|
2271
|
-
if (value.endsWith("|"))
|
|
2272
|
-
value = value.slice(0, -1);
|
|
2273
|
-
return value.split("|").map((cell) => cell.trim());
|
|
2274
|
-
};
|
|
2275
|
-
const renderTable = (startIndex) => {
|
|
2276
|
-
const header = parseTableRow(lines[startIndex]);
|
|
2277
|
-
let index = startIndex + 2;
|
|
2278
|
-
const rows = [];
|
|
2279
|
-
while (index < lines.length && lines[index].includes("|") && lines[index].trim()) {
|
|
2280
|
-
rows.push(parseTableRow(lines[index]));
|
|
2281
|
-
index += 1;
|
|
2282
|
-
}
|
|
2283
|
-
const headHtml = `<thead><tr>${header.map((cell) => `<th>${inline(cell)}</th>`).join("")}</tr></thead>`;
|
|
2284
|
-
const bodyHtml = rows.length
|
|
2285
|
-
? `<tbody>${rows.map((row) => `<tr>${header.map((_cell, cellIndex) => `<td>${inline(row[cellIndex] || "")}</td>`).join("")}</tr>`).join("")}</tbody>`
|
|
2286
|
-
: "";
|
|
2287
|
-
html += `<table style="border-collapse: collapse; width: 100%; margin: 12px 0;">${headHtml}${bodyHtml}</table>`;
|
|
2288
|
-
return index;
|
|
2289
|
-
};
|
|
2290
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
2291
|
-
const line = lines[i];
|
|
2292
|
-
const trimmed = line.trim();
|
|
2293
|
-
const fence = /^```([A-Za-z0-9_-]+)?\s*$/.exec(trimmed);
|
|
2294
|
-
if (fence) {
|
|
2295
|
-
closeParagraph();
|
|
2296
|
-
closeList();
|
|
2297
|
-
if (codeFence) {
|
|
2298
|
-
const langClass = codeFence.lang ? ` class="language-${escapeHtmlAttribute(codeFence.lang)}"` : "";
|
|
2299
|
-
html += `<pre><code${langClass}>${escapeHtml(codeFence.lines.join("\n"))}</code></pre>`;
|
|
2300
|
-
codeFence = null;
|
|
2301
|
-
}
|
|
2302
|
-
else {
|
|
2303
|
-
codeFence = { lang: fence[1] || "", lines: [] };
|
|
2304
|
-
}
|
|
2305
|
-
continue;
|
|
2306
|
-
}
|
|
2307
|
-
if (codeFence) {
|
|
2308
|
-
codeFence.lines.push(line);
|
|
2309
|
-
continue;
|
|
2310
|
-
}
|
|
2311
|
-
if (!trimmed) {
|
|
2312
|
-
closeParagraph();
|
|
2313
|
-
closeList();
|
|
2314
|
-
continue;
|
|
2315
|
-
}
|
|
2316
|
-
if (/^(-{3,}|\*{3,}|_{3,})$/.test(trimmed)) {
|
|
2317
|
-
closeParagraph();
|
|
2318
|
-
closeList();
|
|
2319
|
-
html += "<hr>";
|
|
2320
|
-
continue;
|
|
2321
|
-
}
|
|
2322
|
-
if (i + 1 < lines.length && trimmed.includes("|") && isTableSeparator(lines[i + 1])) {
|
|
2323
|
-
closeParagraph();
|
|
2324
|
-
closeList();
|
|
2325
|
-
i = renderTable(i) - 1;
|
|
2326
|
-
continue;
|
|
2327
|
-
}
|
|
2328
|
-
const heading = /^(#{1,6})\s+(.+)$/.exec(trimmed);
|
|
2329
|
-
if (heading) {
|
|
2330
|
-
closeParagraph();
|
|
2331
|
-
closeList();
|
|
2332
|
-
const level = heading[1].length;
|
|
2333
|
-
html += `<h${level}>${inline(heading[2])}</h${level}>`;
|
|
2334
|
-
continue;
|
|
2335
|
-
}
|
|
2336
|
-
const bullet = /^[-*+]\s+(.+)$/.exec(trimmed);
|
|
2337
|
-
if (bullet) {
|
|
2338
|
-
closeParagraph();
|
|
2339
|
-
if (listType !== "ul") {
|
|
2340
|
-
closeList();
|
|
2341
|
-
html += "<ul>";
|
|
2342
|
-
listType = "ul";
|
|
2343
|
-
}
|
|
2344
|
-
html += `<li>${inline(bullet[1])}</li>`;
|
|
2345
|
-
continue;
|
|
2346
|
-
}
|
|
2347
|
-
const numbered = /^\d+[.)]\s+(.+)$/.exec(trimmed);
|
|
2348
|
-
if (numbered) {
|
|
2349
|
-
closeParagraph();
|
|
2350
|
-
if (listType !== "ol") {
|
|
2351
|
-
closeList();
|
|
2352
|
-
html += "<ol>";
|
|
2353
|
-
listType = "ol";
|
|
2354
|
-
}
|
|
2355
|
-
html += `<li>${inline(numbered[1])}</li>`;
|
|
2356
|
-
continue;
|
|
2357
|
-
}
|
|
2358
|
-
const quote = /^>\s?(.*)$/.exec(trimmed);
|
|
2359
|
-
if (quote) {
|
|
2360
|
-
closeParagraph();
|
|
2361
|
-
closeList();
|
|
2362
|
-
html += `<blockquote>${inline(quote[1]) || "<br>"}</blockquote>`;
|
|
2363
|
-
continue;
|
|
2364
|
-
}
|
|
2365
|
-
closeList();
|
|
2366
|
-
paragraph.push(trimmed);
|
|
2367
|
-
}
|
|
2368
|
-
if (codeFence) {
|
|
2369
|
-
const langClass = codeFence.lang ? ` class="language-${escapeHtmlAttribute(codeFence.lang)}"` : "";
|
|
2370
|
-
html += `<pre><code${langClass}>${escapeHtml(codeFence.lines.join("\n"))}</code></pre>`;
|
|
2371
|
-
}
|
|
2372
|
-
closeParagraph();
|
|
2373
|
-
closeList();
|
|
2374
|
-
const root = document.createElement("div");
|
|
2375
|
-
root.innerHTML = html;
|
|
2376
|
-
enhanceImportedTables(root);
|
|
2377
|
-
return root.innerHTML;
|
|
2378
|
-
};
|
|
2379
3161
|
const htmlToMarkdown = (html) => {
|
|
2380
3162
|
const root = document.createElement("div");
|
|
2381
3163
|
root.innerHTML = html;
|
|
@@ -2419,7 +3201,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
2419
3201
|
return;
|
|
2420
3202
|
const file = files[0];
|
|
2421
3203
|
const text = await file.text();
|
|
2422
|
-
const html = type === "html" ? text :
|
|
3204
|
+
const html = type === "html" ? text : markdownToCompatibilityHtml(text);
|
|
2423
3205
|
const el = editableRef.current;
|
|
2424
3206
|
const hasContent = el && el.textContent && el.textContent.trim().length > 0;
|
|
2425
3207
|
insertImportedHtml(html, hasContent ? "append" : "replace", {
|
|
@@ -2885,11 +3667,73 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
2885
3667
|
console.error("Error wrapping tables", e);
|
|
2886
3668
|
}
|
|
2887
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
|
+
};
|
|
2888
3731
|
const isCaretBoundaryBlock = (node) => {
|
|
2889
3732
|
if (!(node instanceof HTMLElement))
|
|
2890
3733
|
return false;
|
|
2891
3734
|
const tag = node.tagName.toLowerCase();
|
|
2892
3735
|
return (tag === "blockquote" ||
|
|
3736
|
+
tag === "pre" ||
|
|
2893
3737
|
tag === "table" ||
|
|
2894
3738
|
node.getAttribute("data-table-wrapper") === "true");
|
|
2895
3739
|
};
|
|
@@ -2922,6 +3766,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
2922
3766
|
return;
|
|
2923
3767
|
// Auto-fix negative margins that might cause visibility issues
|
|
2924
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);
|
|
2925
3775
|
// Ensure tables are wrapped for horizontal scrolling
|
|
2926
3776
|
ensureTableWrappers(el);
|
|
2927
3777
|
// Keep a reachable typing position around isolating blocks at document edges
|
|
@@ -3509,13 +4359,19 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3509
4359
|
if (image && editor.contains(image)) {
|
|
3510
4360
|
return (image.parentElement?.tagName === "A" ? image.parentElement : image);
|
|
3511
4361
|
}
|
|
4362
|
+
const listElement = element.closest("ul,ol");
|
|
4363
|
+
if (listElement && editor.contains(listElement))
|
|
4364
|
+
return listElement;
|
|
3512
4365
|
const tableElement = element.closest("table");
|
|
3513
4366
|
if (tableElement && editor.contains(tableElement)) {
|
|
3514
4367
|
return (tableElement.closest('[data-table-wrapper="true"]') || tableElement);
|
|
3515
4368
|
}
|
|
3516
|
-
const
|
|
3517
|
-
if (
|
|
3518
|
-
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;
|
|
3519
4375
|
const block = element.closest('[data-table-wrapper="true"],blockquote,pre,p,h1,h2,h3,h4,h5,h6,div');
|
|
3520
4376
|
if (!block || block === editor || !editor.contains(block))
|
|
3521
4377
|
return null;
|
|
@@ -3535,8 +4391,13 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3535
4391
|
}
|
|
3536
4392
|
const targetRect = target.getBoundingClientRect();
|
|
3537
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"));
|
|
3538
4398
|
const next = {
|
|
3539
|
-
|
|
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)),
|
|
3540
4401
|
top: targetRect.top - scrollRect.top + scroller.scrollTop,
|
|
3541
4402
|
height: Math.max(24, targetRect.height),
|
|
3542
4403
|
target,
|
|
@@ -3559,7 +4420,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3559
4420
|
dragHandleHideTimerRef.current = null;
|
|
3560
4421
|
if (!draggedBlockRef.current)
|
|
3561
4422
|
setDragHandle(null);
|
|
3562
|
-
},
|
|
4423
|
+
}, 350);
|
|
3563
4424
|
};
|
|
3564
4425
|
const getImageFromMovableBlock = (block) => {
|
|
3565
4426
|
if (block.tagName === "IMG")
|
|
@@ -3571,6 +4432,50 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3571
4432
|
if (!editor || !editor.contains(block))
|
|
3572
4433
|
return false;
|
|
3573
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
|
+
}
|
|
3574
4479
|
const draggedImage = getImageFromMovableBlock(block);
|
|
3575
4480
|
const targetCell = draggedImage ? getClosestCell(under) : null;
|
|
3576
4481
|
if (draggedImage && targetCell && !block.contains(targetCell)) {
|
|
@@ -3601,6 +4506,78 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3601
4506
|
if (range && editor.contains(range.commonAncestorContainer)) {
|
|
3602
4507
|
if (block.contains(range.commonAncestorContainer))
|
|
3603
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
|
+
}
|
|
3604
4581
|
range.insertNode(block);
|
|
3605
4582
|
return true;
|
|
3606
4583
|
}
|
|
@@ -3671,13 +4648,65 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3671
4648
|
};
|
|
3672
4649
|
const elementSibling = (element, direction) => {
|
|
3673
4650
|
let sibling = direction === "previous" ? element.previousSibling : element.nextSibling;
|
|
3674
|
-
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"))) {
|
|
3675
4654
|
sibling = direction === "previous" ? sibling.previousSibling : sibling.nextSibling;
|
|
3676
4655
|
}
|
|
3677
4656
|
return sibling;
|
|
3678
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
|
+
};
|
|
3679
4706
|
const moveCurrentElement = (direction) => {
|
|
3680
4707
|
const editor = editableRef.current;
|
|
4708
|
+
if (moveSelectedBlocks(direction))
|
|
4709
|
+
return;
|
|
3681
4710
|
let target = getMoveTarget();
|
|
3682
4711
|
if (!editor || !target)
|
|
3683
4712
|
return;
|
|
@@ -3859,7 +4888,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3859
4888
|
} }), _jsx("input", { ref: mdInputRef, type: "file", accept: ".md,.markdown,text/markdown,text/plain", style: { display: "none" }, onChange: (e) => {
|
|
3860
4889
|
importTextFile(e.currentTarget.files, "md");
|
|
3861
4890
|
e.currentTarget.value = "";
|
|
3862
|
-
} }), _jsxs("select", { value: currentBlockType, onMouseDown: preserveEditorSelection, onChange: (e) => {
|
|
4891
|
+
} }), _jsxs("select", { value: currentBlockType, onPointerDown: preserveEditorSelection, onMouseDown: preserveEditorSelection, onChange: (e) => {
|
|
3863
4892
|
const val = e.target.value;
|
|
3864
4893
|
if (val === "p")
|
|
3865
4894
|
applyFormatBlock("<p>");
|
|
@@ -3869,6 +4898,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3869
4898
|
applyFormatBlock("<h2>");
|
|
3870
4899
|
else if (val === "h3")
|
|
3871
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>");
|
|
3872
4907
|
}, title: "Paragraph/Heading", style: {
|
|
3873
4908
|
height: 32,
|
|
3874
4909
|
padding: "0 8px",
|
|
@@ -3876,24 +4911,23 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3876
4911
|
borderRadius: 6,
|
|
3877
4912
|
background: "var(--srte-input-bg)",
|
|
3878
4913
|
color: "var(--srte-input-text)",
|
|
3879
|
-
}, children: [_jsx("option", { value: "
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
}, onChange: (e) => applyFontSize(e.target.value), title: "Font Size", style: {
|
|
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: {
|
|
3890
4924
|
height: 32,
|
|
3891
4925
|
padding: "0 8px",
|
|
3892
4926
|
border: "1px solid var(--srte-input-border)",
|
|
3893
4927
|
borderRadius: 6,
|
|
3894
4928
|
background: "var(--srte-input-bg)",
|
|
3895
4929
|
color: "var(--srte-input-text)",
|
|
3896
|
-
}, children: [_jsx("option", { value: "", disabled: true, children: "Size" }), _jsx("option", { value: "8", children: "8" }), _jsx("option", { value: "9", children: "9" }), _jsx("option", { value: "10", children: "10" }), _jsx("option", { value: "11", children: "11" }), _jsx("option", { value: "12", children: "12" }), _jsx("option", { value: "14", children: "14" }), _jsx("option", { value: "18", children: "18" }), _jsx("option", { value: "24", children: "24" }), _jsx("option", { value: "30", children: "30" }), _jsx("option", { value: "36", children: "36" }), _jsx("option", { value: "48", children: "48" }), _jsx("option", { value: "60", children: "60" }), _jsx("option", { value: "72", children: "72" }), _jsx("option", { value: "96", children: "96" })] })), preserveFontFamily && (_jsxs("select", { value: currentFont, onMouseDown: () => {
|
|
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: () => {
|
|
3897
4931
|
const sel = window.getSelection();
|
|
3898
4932
|
if (sel && sel.rangeCount > 0) {
|
|
3899
4933
|
const range = sel.getRangeAt(0);
|
|
@@ -3933,20 +4967,50 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3933
4967
|
borderRadius: 6,
|
|
3934
4968
|
background: "var(--srte-input-bg)",
|
|
3935
4969
|
color: "var(--srte-input-text)",
|
|
3936
|
-
}, children: _jsx("span", { style: { fontWeight: 700, padding: "1px 4px", background: "var(--srte-accent-bg)", borderRadius: 3 }, children: "A" }) }), _jsxs("button", { title: "Subscript", onClick: () => toggleInlineScript("subscript"), "aria-pressed": activeState.subscript, style: activeButtonStyle(activeState.subscript), children: ["X", _jsx("sub", { children: "2" })] }), _jsxs("button", { title: "Superscript", onClick: () => toggleInlineScript("superscript"), "aria-pressed": activeState.superscript, style: activeButtonStyle(activeState.superscript), children: ["X", _jsx("sup", { children: "2" })] }),
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
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: {
|
|
3950
5014
|
height: 32,
|
|
3951
5015
|
minWidth: 32,
|
|
3952
5016
|
padding: "0 8px",
|
|
@@ -3954,7 +5018,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3954
5018
|
borderRadius: 6,
|
|
3955
5019
|
background: "var(--srte-input-bg)",
|
|
3956
5020
|
color: "var(--srte-input-text)",
|
|
3957
|
-
}, 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, {
|
|
3958
5022
|
minWidth: 36,
|
|
3959
5023
|
fontFamily: "ui-monospace, SFMono-Regular, Menlo",
|
|
3960
5024
|
}), children: "< />" }), formula && (_jsx("button", { title: "Insert formula", onClick: () => setShowFormulaDialog(true), style: {
|
|
@@ -3965,21 +5029,18 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
3965
5029
|
borderRadius: 6,
|
|
3966
5030
|
background: "var(--srte-input-bg)",
|
|
3967
5031
|
color: "var(--srte-input-text)",
|
|
3968
|
-
}, children: "\u2211" })), _jsx("button", { title: "Insert link", onClick:
|
|
3969
|
-
height: 32,
|
|
3970
|
-
padding: "0 10px",
|
|
3971
|
-
border: "1px solid var(--srte-input-border)",
|
|
3972
|
-
borderRadius: 6,
|
|
3973
|
-
background: "var(--srte-input-bg)",
|
|
3974
|
-
color: "var(--srte-input-text)",
|
|
3975
|
-
}, children: "Link" }), _jsx("button", { title: "Remove link", onClick: () => exec("unlink"), style: {
|
|
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: {
|
|
3976
5033
|
height: 32,
|
|
3977
|
-
|
|
5034
|
+
minWidth: 34,
|
|
5035
|
+
padding: "0 8px",
|
|
3978
5036
|
border: "1px solid var(--srte-input-border)",
|
|
3979
5037
|
borderRadius: 6,
|
|
3980
5038
|
background: "var(--srte-input-bg)",
|
|
3981
5039
|
color: "var(--srte-input-text)",
|
|
3982
|
-
|
|
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: {
|
|
3983
5044
|
height: 32,
|
|
3984
5045
|
padding: "0 10px",
|
|
3985
5046
|
border: "1px solid var(--srte-input-border)",
|
|
@@ -4483,8 +5544,14 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
4483
5544
|
}, onMouseMove: (e) => {
|
|
4484
5545
|
if (draggedBlockRef.current)
|
|
4485
5546
|
return;
|
|
4486
|
-
updateDragHandleForTarget(getMovableElementFromNode(e.target));
|
|
4487
|
-
},
|
|
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;
|
|
4488
5555
|
if (!draggedBlockRef.current)
|
|
4489
5556
|
scheduleDragHandleHide();
|
|
4490
5557
|
}, onPaste: (e) => {
|
|
@@ -4580,13 +5647,31 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
4580
5647
|
}
|
|
4581
5648
|
}, onClick: (e) => {
|
|
4582
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
|
+
}
|
|
4583
5665
|
const anchor = t?.closest("a");
|
|
4584
5666
|
if (anchor && editableRef.current?.contains(anchor)) {
|
|
4585
5667
|
e.preventDefault();
|
|
4586
5668
|
e.stopPropagation();
|
|
4587
|
-
|
|
5669
|
+
openLinkEditor(anchor);
|
|
5670
|
+
setTableMenu(null);
|
|
5671
|
+
setImageMenu(null);
|
|
4588
5672
|
return;
|
|
4589
5673
|
}
|
|
5674
|
+
setLinkMenu(null);
|
|
4590
5675
|
if (t && t.tagName === "IMG") {
|
|
4591
5676
|
setSelectedImage(t);
|
|
4592
5677
|
scheduleImageOverlay();
|
|
@@ -4638,6 +5723,11 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
4638
5723
|
}
|
|
4639
5724
|
updateActiveState();
|
|
4640
5725
|
}, onKeyDown: (e) => {
|
|
5726
|
+
if ((e.metaKey || e.ctrlKey) && String(e.key).toLowerCase() === "k") {
|
|
5727
|
+
e.preventDefault();
|
|
5728
|
+
openLinkEditor();
|
|
5729
|
+
return;
|
|
5730
|
+
}
|
|
4641
5731
|
if ((e.metaKey || e.ctrlKey) && String(e.key).toLowerCase() === "z") {
|
|
4642
5732
|
const restored = restoreEditorHistory(e.shiftKey ? "redo" : "undo");
|
|
4643
5733
|
if (restored) {
|
|
@@ -4789,7 +5879,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
4789
5879
|
setTableMenu(null);
|
|
4790
5880
|
setImageMenu(null);
|
|
4791
5881
|
}
|
|
4792
|
-
} }), 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: () => {
|
|
4793
5883
|
if (dragHandleHideTimerRef.current != null) {
|
|
4794
5884
|
window.clearTimeout(dragHandleHideTimerRef.current);
|
|
4795
5885
|
dragHandleHideTimerRef.current = null;
|
|
@@ -4926,7 +6016,10 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
4926
6016
|
borderRadius: 2,
|
|
4927
6017
|
cursor: "ew-resize",
|
|
4928
6018
|
pointerEvents: "auto",
|
|
4929
|
-
} })] }))] }),
|
|
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: {
|
|
4930
6023
|
position: "fixed",
|
|
4931
6024
|
inset: 0,
|
|
4932
6025
|
zIndex: 60,
|