smartrte-react 0.2.10 → 0.3.1

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