smartrte-react 0.2.8 → 0.3.0

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