smartrte-react 0.3.0 → 0.3.2

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,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from "react";
3
3
  import { MediaManager } from "./MediaManager.js";
4
4
  import { LinkEditorPopover } from "./LinkEditorPopover.js";
@@ -12,6 +12,72 @@ import { isShadowModeEnabled, runShadowCommand } from '../adapters/shadowMode.js
12
12
  import { getCoreInlineMarkResult, isCoreInlineMarkEnabled } from '../adapters/inlineMarkCoreExecution.js';
13
13
  import { closestFromTarget, isNode } from '../adapters/domTargets.js';
14
14
  import { ensureStyleSheet } from '../theme.js';
15
+ const iconPaths = {
16
+ undo: _jsxs(_Fragment, { children: [_jsx("path", { d: "M9 7 5 11l4 4" }), _jsx("path", { d: "M5 11h7a5 5 0 0 1 5 5" })] }),
17
+ redo: _jsxs(_Fragment, { children: [_jsx("path", { d: "m15 7 4 4-4 4" }), _jsx("path", { d: "M19 11h-7a5 5 0 0 0-5 5" })] }),
18
+ "align-left": _jsx(_Fragment, { children: _jsx("path", { d: "M4 6h16M4 10h11M4 14h16M4 18h11" }) }),
19
+ "align-center": _jsx(_Fragment, { children: _jsx("path", { d: "M4 6h16M7 10h10M4 14h16M7 18h10" }) }),
20
+ "align-right": _jsx(_Fragment, { children: _jsx("path", { d: "M4 6h16M9 10h11M4 14h16M9 18h11" }) }),
21
+ justify: _jsx(_Fragment, { children: _jsx("path", { d: "M4 6h16M4 10h16M4 14h16M4 18h16" }) }),
22
+ move: _jsx(_Fragment, { children: _jsx("path", { d: "m8 7 4-4 4 4M12 3v18M8 17l4 4 4-4" }) }),
23
+ up: _jsx(_Fragment, { children: _jsx("path", { d: "m7 10 5-5 5 5M12 5v14" }) }),
24
+ down: _jsx(_Fragment, { children: _jsx("path", { d: "m7 14 5 5 5-5M12 19V5" }) }),
25
+ outdent: _jsx(_Fragment, { children: _jsx("path", { d: "M11 6h9M11 10h7M11 14h9M11 18h7M8 9l-3 3 3 3" }) }),
26
+ indent: _jsx(_Fragment, { children: _jsx("path", { d: "M11 6h9M11 10h7M11 14h9M11 18h7M4 9l3 3-3 3" }) }),
27
+ link: _jsxs(_Fragment, { children: [_jsx("path", { d: "M10 13a5 5 0 0 0 7.54.54l2-2a5 5 0 0 0-7.07-7.07l-1.15 1.15" }), _jsx("path", { d: "M14 11a5 5 0 0 0-7.54-.54l-2 2a5 5 0 0 0 7.07 7.07l1.14-1.14" })] }),
28
+ quote: _jsx(_Fragment, { children: _jsx("path", { d: "M6 15h4V9H5v6a4 4 0 0 1-1 2.7M15 15h4V9h-5v6a4 4 0 0 1-1 2.7" }) }),
29
+ code: _jsx(_Fragment, { children: _jsx("path", { d: "m8 9-3 3 3 3M16 9l3 3-3 3M14 5l-4 14" }) }),
30
+ checklist: _jsx(_Fragment, { children: _jsx("path", { d: "m4 6 1.5 1.5L8 5M11 6h9M4 12l1.5 1.5L8 11M11 12h9M4 18l1.5 1.5L8 17M11 18h9" }) }),
31
+ bullets: _jsxs(_Fragment, { children: [_jsx("circle", { cx: "5", cy: "6", r: "1" }), _jsx("circle", { cx: "5", cy: "12", r: "1" }), _jsx("circle", { cx: "5", cy: "18", r: "1" }), _jsx("path", { d: "M9 6h11M9 12h11M9 18h11" })] }),
32
+ numbers: _jsx(_Fragment, { children: _jsx("path", { d: "M4 6h1V3.8L4 4.5M4 11h2l-2 2h2M4 17h2l-2 2h2M10 6h10M10 12h10M10 18h10" }) }),
33
+ image: _jsxs(_Fragment, { children: [_jsx("rect", { x: "3", y: "4", width: "18", height: "16", rx: "2" }), _jsx("circle", { cx: "8.5", cy: "9", r: "1.5" }), _jsx("path", { d: "m21 15-5-5L5 20" })] }),
34
+ media: _jsxs(_Fragment, { children: [_jsx("path", { d: "M4 7h6l2 2h8v10H4z" }), _jsx("path", { d: "M8 14h8M12 11v6" })] }),
35
+ table: _jsxs(_Fragment, { children: [_jsx("rect", { x: "3", y: "4", width: "18", height: "16", rx: "1" }), _jsx("path", { d: "M3 10h18M9 4v16M15 4v16" })] }),
36
+ formula: _jsx(_Fragment, { children: _jsx("path", { d: "M18 5H8l5 7-5 7h10" }) }),
37
+ insert: _jsxs(_Fragment, { children: [_jsx("circle", { cx: "12", cy: "12", r: "9" }), _jsx("path", { d: "M12 8v8M8 12h8" })] }),
38
+ import: _jsx(_Fragment, { children: _jsx("path", { d: "M12 3v12M8 11l4 4 4-4M5 20h14" }) }),
39
+ export: _jsx(_Fragment, { children: _jsx("path", { d: "M12 16V4M8 8l4-4 4 4M5 20h14" }) }),
40
+ omega: _jsx(_Fragment, { children: _jsx("path", { d: "M7 19h4v-2a7 7 0 1 1 2 0v2h4" }) }),
41
+ more: _jsxs(_Fragment, { children: [_jsx("circle", { cx: "5", cy: "12", r: "1" }), _jsx("circle", { cx: "12", cy: "12", r: "1" }), _jsx("circle", { cx: "19", cy: "12", r: "1" })] }),
42
+ chevron: _jsx("path", { d: "m8 10 4 4 4-4" }),
43
+ };
44
+ function ToolbarIcon({ name, size = 16 }) {
45
+ return _jsx("svg", { "aria-hidden": "true", width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", children: iconPaths[name] });
46
+ }
47
+ function ToolbarGroup({ label, priority, children }) {
48
+ return _jsx("div", { className: "srte-toolbar-group", role: "group", "aria-label": label, "data-srte-priority": priority || 1, children: children });
49
+ }
50
+ function ToolbarMenu({ label, icon, active, priority, children }) {
51
+ const menuRef = useRef(null);
52
+ useEffect(() => {
53
+ const closeOnOutsidePointer = (event) => {
54
+ if (menuRef.current?.open && !menuRef.current.contains(event.target))
55
+ menuRef.current.open = false;
56
+ };
57
+ document.addEventListener("pointerdown", closeOnOutsidePointer);
58
+ return () => document.removeEventListener("pointerdown", closeOnOutsidePointer);
59
+ }, []);
60
+ return (_jsxs("details", { ref: menuRef, className: "srte-toolbar-menu", "data-srte-priority": priority, onKeyDown: (event) => {
61
+ if (event.key === "Escape") {
62
+ event.preventDefault();
63
+ menuRef.current?.removeAttribute("open");
64
+ menuRef.current?.querySelector("summary")?.focus();
65
+ return;
66
+ }
67
+ if (event.key !== "ArrowDown" && event.key !== "ArrowUp")
68
+ return;
69
+ const items = Array.from(menuRef.current?.querySelectorAll('[role="menuitem"]:not([disabled])') || []);
70
+ if (!items.length)
71
+ return;
72
+ event.preventDefault();
73
+ const currentIndex = items.indexOf(document.activeElement);
74
+ const direction = event.key === "ArrowDown" ? 1 : -1;
75
+ items[(currentIndex + direction + items.length) % items.length]?.focus();
76
+ }, children: [_jsxs("summary", { className: `srte-tool-button srte-menu-trigger${active ? " srte-active" : ""}`, "aria-label": label, title: label, children: [_jsx(ToolbarIcon, { name: icon }), _jsx(ToolbarIcon, { name: "chevron", size: 12 })] }), _jsx("div", { className: "srte-menu", role: "menu", "aria-label": label, children: children })] }));
77
+ }
78
+ function MenuItem({ icon, label, active, disabled, onClick, title }) {
79
+ return _jsxs("button", { type: "button", role: "menuitem", className: "srte-menu-item", "aria-label": title || label, "aria-pressed": Boolean(active), "aria-checked": active || undefined, disabled: disabled, title: title || label, onClick: (event) => { onClick(); event.currentTarget.closest("details")?.removeAttribute("open"); }, children: [_jsx(ToolbarIcon, { name: icon }), _jsx("span", { children: label }), active && _jsx("span", { className: "srte-menu-check", "aria-hidden": "true", children: "\u2713" })] });
80
+ }
15
81
  // Initialize PDF.js worker
16
82
  if (typeof window !== 'undefined') {
17
83
  pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
@@ -254,12 +320,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
254
320
  italic: queryState("italic"),
255
321
  underline: queryState("underline"),
256
322
  strikeThrough: queryState("strikeThrough"),
257
- subscript: overrideApplies && scriptOverride?.command === "subscript"
258
- ? false
259
- : subscriptActive,
260
- superscript: overrideApplies && scriptOverride?.command === "superscript"
261
- ? false
262
- : superscriptActive,
323
+ subscript: overrideApplies ? scriptOverride?.command === "subscript" : subscriptActive,
324
+ superscript: overrideApplies ? scriptOverride?.command === "superscript" : superscriptActive,
263
325
  checklist: Boolean(element?.closest('[data-srte-checklist="true"]')),
264
326
  unorderedList: Boolean(element?.closest("ul:not([data-srte-checklist=\"true\"])")),
265
327
  orderedList: Boolean(element?.closest("ol")),
@@ -398,55 +460,121 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
398
460
  const range = getSelectionRangeInEditor();
399
461
  if (!editor || !range)
400
462
  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();
463
+ if (range.collapsed) {
464
+ const tagName = command === "subscript" ? "sub" : "sup";
465
+ const pending = inlineScriptCaretOverrideRef.current;
466
+ const pendingApplies = pending &&
467
+ pending.container === range.startContainer && pending.offset === range.startOffset;
468
+ const active = pendingApplies
469
+ ? pending.command === command
470
+ : Boolean(getScriptAncestor(range.startContainer, tagName));
471
+ const nextCommand = active ? "normal" : command;
431
472
  inlineScriptCaretOverrideRef.current = {
432
- command,
433
- container: nextRange.startContainer,
434
- offset: nextRange.startOffset,
473
+ command: nextCommand,
474
+ container: range.startContainer,
475
+ offset: range.startOffset,
435
476
  };
436
477
  setActiveState((current) => ({
437
478
  ...current,
438
- subscript: command === "subscript" ? false : current.subscript,
439
- superscript: command === "superscript" ? false : current.superscript,
479
+ subscript: nextCommand === "subscript",
480
+ superscript: nextCommand === "superscript",
440
481
  }));
441
- requestAnimationFrame(updateActiveState);
482
+ savedRangeRef.current = range.cloneRange();
442
483
  return;
443
484
  }
444
485
  inlineScriptCaretOverrideRef.current = null;
445
- exec(command);
486
+ pushEditorHistory();
487
+ const result = getCoreInlineMarkResult(editor, command);
488
+ if (result) {
489
+ editor.innerHTML = result.html;
490
+ ensureTableWrappers(editor);
491
+ addTableResizeHandles();
492
+ restoreSelectionToDom(editor, result.selectionAfter);
493
+ handleInput();
494
+ }
495
+ else {
496
+ exec(command);
497
+ }
446
498
  requestAnimationFrame(updateActiveState);
447
499
  }
448
500
  catch { }
449
501
  };
502
+ useEffect(() => {
503
+ const editor = editableRef.current;
504
+ if (!editor)
505
+ return;
506
+ const splitScriptAtRange = (script, range) => {
507
+ const rightRange = document.createRange();
508
+ rightRange.selectNodeContents(script);
509
+ rightRange.setStart(range.startContainer, range.startOffset);
510
+ const rightContent = rightRange.extractContents();
511
+ const rightScript = script.cloneNode(false);
512
+ rightScript.appendChild(rightContent);
513
+ script.parentNode?.insertBefore(rightScript, script.nextSibling);
514
+ const insertion = document.createRange();
515
+ insertion.setStartAfter(script);
516
+ insertion.collapse(true);
517
+ if (!script.textContent)
518
+ script.remove();
519
+ if (!rightScript.textContent)
520
+ rightScript.remove();
521
+ return insertion;
522
+ };
523
+ const applyPendingInlineScript = (event) => {
524
+ const pending = inlineScriptCaretOverrideRef.current;
525
+ if (!pending || event.inputType !== "insertText" || !event.data)
526
+ return;
527
+ const range = getSelectionRangeInEditor();
528
+ if (!range?.collapsed || range.startContainer !== pending.container || range.startOffset !== pending.offset)
529
+ return;
530
+ event.preventDefault();
531
+ event.stopImmediatePropagation();
532
+ pushEditorHistory();
533
+ const currentScript = getScriptAncestor(range.startContainer, "sub") ||
534
+ getScriptAncestor(range.startContainer, "sup");
535
+ const desiredTag = pending.command === "normal"
536
+ ? null
537
+ : pending.command === "subscript" ? "sub" : "sup";
538
+ let insertion = range;
539
+ if (currentScript && currentScript.tagName.toLowerCase() !== desiredTag) {
540
+ insertion = splitScriptAtRange(currentScript, range);
541
+ }
542
+ const text = document.createTextNode(event.data);
543
+ let inserted = text;
544
+ if (desiredTag && currentScript?.tagName.toLowerCase() !== desiredTag) {
545
+ const script = document.createElement(desiredTag);
546
+ script.appendChild(text);
547
+ inserted = script;
548
+ }
549
+ const pendingSize = pendingFontSizeRef.current;
550
+ if (pendingSize) {
551
+ const span = document.createElement("span");
552
+ span.style.fontSize = `${pendingSize.valuePx}px`;
553
+ span.appendChild(inserted);
554
+ inserted = span;
555
+ }
556
+ insertion.insertNode(inserted);
557
+ const nextRange = document.createRange();
558
+ nextRange.setStartAfter(text);
559
+ nextRange.collapse(true);
560
+ safeSelectRange(nextRange);
561
+ inlineScriptCaretOverrideRef.current = {
562
+ command: pending.command,
563
+ container: nextRange.startContainer,
564
+ offset: nextRange.startOffset,
565
+ };
566
+ if (pendingSize)
567
+ pendingFontSizeRef.current = {
568
+ valuePx: pendingSize.valuePx,
569
+ container: nextRange.startContainer,
570
+ offset: nextRange.startOffset,
571
+ };
572
+ savedRangeRef.current = nextRange.cloneRange();
573
+ handleInput();
574
+ };
575
+ editor.addEventListener("beforeinput", applyPendingInlineScript);
576
+ return () => editor.removeEventListener("beforeinput", applyPendingInlineScript);
577
+ }, []);
450
578
  const applyFormatBlock = (blockName) => {
451
579
  try {
452
580
  if (!restoreSavedSelection()) {
@@ -785,19 +913,28 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
785
913
  const current = getCurrentBlock();
786
914
  return current ? [current] : [];
787
915
  };
788
- const getSelectedListItems = (blocks) => {
789
- const seen = new Set();
790
- const items = [];
791
- blocks.forEach((block) => {
792
- const li = block.tagName.toLowerCase() === "li"
793
- ? block
794
- : block.closest("li");
795
- if (li && editableRef.current?.contains(li) && !seen.has(li)) {
796
- seen.add(li);
797
- items.push(li);
916
+ const resolveSelectedListItems = (range) => {
917
+ const editor = editableRef.current;
918
+ if (!editor)
919
+ return [];
920
+ if (range.collapsed) {
921
+ const element = range.startContainer instanceof HTMLElement
922
+ ? range.startContainer
923
+ : range.startContainer.parentElement;
924
+ const item = element?.closest("li");
925
+ return item && editor.contains(item) ? [item] : [];
926
+ }
927
+ const ownsSelectedContent = (item) => Array.from(item.childNodes).some((node) => {
928
+ if (node instanceof HTMLElement && (node.matches("ul,ol") || node.dataset.srteCheck === "true"))
929
+ return false;
930
+ try {
931
+ return range.intersectsNode(node);
932
+ }
933
+ catch {
934
+ return false;
798
935
  }
799
936
  });
800
- return items;
937
+ return sortInDocumentOrder(Array.from(editor.querySelectorAll("li")).filter(ownsSelectedContent));
801
938
  };
802
939
  const getAlignmentTarget = (block) => {
803
940
  const item = block.tagName === "LI" ? block : block.closest("li");
@@ -1023,6 +1160,49 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1023
1160
  Array.from(list.attributes).forEach((attr) => clone.setAttribute(attr.name, attr.value));
1024
1161
  return clone;
1025
1162
  };
1163
+ const listItemContentSelector = "p,h1,h2,h3,h4,h5,h6,blockquote,pre";
1164
+ const createListItemFromBlock = (block) => {
1165
+ const item = document.createElement("li");
1166
+ item.style.textAlign = block.style.textAlign;
1167
+ item.appendChild(block);
1168
+ return item;
1169
+ };
1170
+ const extractListItemContent = (item) => {
1171
+ item.querySelectorAll(':scope > [data-srte-check]').forEach((control) => control.remove());
1172
+ const output = [];
1173
+ let inlineParagraph = null;
1174
+ const flushInlineParagraph = () => {
1175
+ if (!inlineParagraph)
1176
+ return;
1177
+ if (!inlineParagraph.childNodes.length)
1178
+ inlineParagraph.innerHTML = "<br>";
1179
+ output.push(inlineParagraph);
1180
+ inlineParagraph = null;
1181
+ };
1182
+ Array.from(item.childNodes).forEach((node) => {
1183
+ if (node instanceof HTMLElement && node.matches(listItemContentSelector)) {
1184
+ flushInlineParagraph();
1185
+ output.push(node);
1186
+ return;
1187
+ }
1188
+ if (node instanceof HTMLElement && node.matches("ul,ol")) {
1189
+ flushInlineParagraph();
1190
+ output.push(node);
1191
+ return;
1192
+ }
1193
+ inlineParagraph || (inlineParagraph = document.createElement("p"));
1194
+ inlineParagraph.appendChild(node);
1195
+ });
1196
+ flushInlineParagraph();
1197
+ if (item.style.textAlign) {
1198
+ output.forEach((node) => {
1199
+ if (node instanceof HTMLElement && node.matches(listItemContentSelector) && !node.style.textAlign) {
1200
+ node.style.textAlign = item.style.textAlign;
1201
+ }
1202
+ });
1203
+ }
1204
+ return output;
1205
+ };
1026
1206
  const mergeAdjacentCompatibleLists = (list) => {
1027
1207
  const isCompatible = (candidate) => candidate instanceof HTMLElement &&
1028
1208
  candidate.tagName === list.tagName &&
@@ -1056,6 +1236,40 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1056
1236
  }
1057
1237
  });
1058
1238
  };
1239
+ const syncChecklistItemControl = (item, strikeCompleted) => {
1240
+ const legacyCheckbox = item.querySelector(':scope > input[data-srte-check]');
1241
+ const checked = item.dataset.checked === "true" || Boolean(legacyCheckbox?.checked);
1242
+ const controls = Array.from(item.querySelectorAll(':scope > [data-srte-check]'));
1243
+ let control = controls.find((candidate) => candidate.tagName === "BUTTON");
1244
+ controls.forEach((candidate) => {
1245
+ if (candidate !== control)
1246
+ candidate.remove();
1247
+ });
1248
+ if (!control) {
1249
+ control = document.createElement("button");
1250
+ control.type = "button";
1251
+ control.dataset.srteCheck = "true";
1252
+ control.contentEditable = "false";
1253
+ control.tabIndex = -1;
1254
+ control.style.cssText = "border:0;padding:0;background:transparent;color:inherit;font:inherit;cursor:pointer";
1255
+ item.prepend(control);
1256
+ }
1257
+ item.dataset.checked = checked ? "true" : "false";
1258
+ control.dataset.checked = checked ? "true" : "false";
1259
+ control.setAttribute("aria-pressed", checked ? "true" : "false");
1260
+ control.setAttribute("aria-label", checked ? "Mark incomplete" : "Mark complete");
1261
+ control.textContent = "";
1262
+ item.style.textDecoration = strikeCompleted && checked ? "line-through" : "";
1263
+ };
1264
+ const syncChecklistControls = (root) => {
1265
+ root.querySelectorAll('ul[data-srte-checklist="true"]').forEach((list) => {
1266
+ const strikeCompleted = list.dataset.srteChecklistStrike === "true";
1267
+ Array.from(list.children).forEach((item) => {
1268
+ if (item instanceof HTMLElement && item.tagName === "LI")
1269
+ syncChecklistItemControl(item, strikeCompleted);
1270
+ });
1271
+ });
1272
+ };
1059
1273
  const decorateChecklist = (list, strikeCompleted) => {
1060
1274
  list.dataset.srteChecklist = "true";
1061
1275
  list.dataset.srteChecklistStrike = strikeCompleted ? "true" : "false";
@@ -1064,20 +1278,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1064
1278
  Array.from(list.children).forEach((item) => {
1065
1279
  if (!(item instanceof HTMLElement) || item.tagName !== "LI")
1066
1280
  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" : "";
1281
+ syncChecklistItemControl(item, strikeCompleted);
1081
1282
  });
1082
1283
  return mergeAdjacentCompatibleLists(list);
1083
1284
  };
@@ -1105,9 +1306,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1105
1306
  const parent = list.parentElement;
1106
1307
  if (!parent)
1107
1308
  return;
1108
- const paragraph = document.createElement("p");
1109
- paragraph.innerHTML = li.innerHTML || "<br>";
1110
- paragraph.style.textAlign = li.style.textAlign;
1309
+ const content = extractListItemContent(li);
1111
1310
  const beforeList = cloneListShell(list);
1112
1311
  const afterList = cloneListShell(list);
1113
1312
  while (list.firstChild && list.firstChild !== li) {
@@ -1118,13 +1317,14 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1118
1317
  }
1119
1318
  if (beforeList.childNodes.length)
1120
1319
  parent.insertBefore(beforeList, list);
1121
- parent.insertBefore(paragraph, list);
1320
+ content.forEach((node) => parent.insertBefore(node, list));
1122
1321
  if (afterList.childNodes.length)
1123
1322
  parent.insertBefore(afterList, list);
1124
1323
  li.remove();
1125
1324
  if (!list.querySelector("li"))
1126
1325
  list.remove();
1127
- focusElementEnd(paragraph);
1326
+ const focusTarget = [...content].reverse().find((node) => node instanceof HTMLElement);
1327
+ focusElementEnd(focusTarget || parent);
1128
1328
  };
1129
1329
  const convertListTag = (list, listTag) => {
1130
1330
  const replacement = cloneListShell(list, listTag);
@@ -1133,69 +1333,149 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1133
1333
  const li = replacement.querySelector("li");
1134
1334
  focusElementEnd(li || replacement);
1135
1335
  };
1136
- const getOrCreateNestedList = (li, listTag) => {
1137
- let nested = Array.from(li.children).find((child) => {
1138
- const tag = child.tagName.toLowerCase();
1139
- return tag === "ul" || tag === "ol";
1336
+ const getOrCreateNestedList = (item, source) => {
1337
+ const compatible = Array.from(item.children).find((child) => child instanceof HTMLElement &&
1338
+ ["UL", "OL"].includes(child.tagName) &&
1339
+ child.tagName === source.tagName &&
1340
+ child.style.listStyleType === source.style.listStyleType &&
1341
+ child.dataset.srteChecklist === source.dataset.srteChecklist &&
1342
+ child.dataset.srteChecklistStrike === source.dataset.srteChecklistStrike);
1343
+ if (compatible)
1344
+ return compatible;
1345
+ const nested = cloneListShell(source);
1346
+ item.appendChild(nested);
1347
+ return nested;
1348
+ };
1349
+ const normalizeListItemStructure = (item) => {
1350
+ if (!item || item.tagName !== "LI")
1351
+ return;
1352
+ Array.from(item.children)
1353
+ .filter((child) => child instanceof HTMLElement && ["UL", "OL"].includes(child.tagName))
1354
+ .forEach((list) => {
1355
+ if (!list.querySelector(":scope > li"))
1356
+ list.remove();
1357
+ else
1358
+ item.appendChild(list);
1140
1359
  });
1141
- if (nested && nested.tagName.toLowerCase() !== listTag) {
1142
- const replacement = cloneListShell(nested, listTag);
1143
- replacement.innerHTML = nested.innerHTML;
1144
- nested.parentElement?.replaceChild(replacement, nested);
1145
- nested = replacement;
1360
+ };
1361
+ const captureRangeForListMove = () => {
1362
+ const range = getSelectionRangeInEditor();
1363
+ return range ? {
1364
+ startContainer: range.startContainer,
1365
+ startOffset: range.startOffset,
1366
+ endContainer: range.endContainer,
1367
+ endOffset: range.endOffset,
1368
+ } : null;
1369
+ };
1370
+ const restoreRangeAfterListMove = (snapshot) => {
1371
+ const editor = editableRef.current;
1372
+ if (!snapshot || !editor || !editor.contains(snapshot.startContainer) || !editor.contains(snapshot.endContainer))
1373
+ return false;
1374
+ try {
1375
+ const range = document.createRange();
1376
+ range.setStart(snapshot.startContainer, snapshot.startOffset);
1377
+ range.setEnd(snapshot.endContainer, snapshot.endOffset);
1378
+ safeSelectRange(range);
1379
+ savedRangeRef.current = range.cloneRange();
1380
+ return true;
1146
1381
  }
1147
- if (!nested) {
1148
- nested = document.createElement(listTag);
1149
- li.appendChild(nested);
1382
+ catch {
1383
+ return false;
1150
1384
  }
1151
- return nested;
1152
1385
  };
1153
- const nestSelectedListItems = (items, listTag) => {
1386
+ const nestSelectedListItems = (items) => {
1387
+ if (items.length === 0)
1388
+ return false;
1389
+ const savedRange = captureRangeForListMove();
1390
+ const selected = new Set(items);
1391
+ const sourceLists = Array.from(new Set(items.map((item) => item.parentElement)));
1392
+ let changed = false;
1393
+ let lastMoved = null;
1394
+ sourceLists.forEach((source) => {
1395
+ if (!source || !["UL", "OL"].includes(source.tagName))
1396
+ return;
1397
+ const sourceItems = Array.from(source.children).filter((child) => child instanceof HTMLElement && child.tagName === "LI");
1398
+ const runs = [];
1399
+ sourceItems.forEach((item) => {
1400
+ if (!selected.has(item))
1401
+ return;
1402
+ const currentRun = runs[runs.length - 1];
1403
+ const previousIndex = currentRun ? sourceItems.indexOf(currentRun[currentRun.length - 1]) : -2;
1404
+ const itemIndex = sourceItems.indexOf(item);
1405
+ if (!currentRun || itemIndex !== previousIndex + 1)
1406
+ runs.push([item]);
1407
+ else
1408
+ currentRun.push(item);
1409
+ });
1410
+ runs.forEach((run) => {
1411
+ const previous = run[0].previousElementSibling;
1412
+ if (!previous || previous.tagName !== "LI" || selected.has(previous))
1413
+ return;
1414
+ const target = getOrCreateNestedList(previous, source);
1415
+ run.forEach((item) => {
1416
+ target.appendChild(item);
1417
+ lastMoved = item;
1418
+ changed = true;
1419
+ });
1420
+ normalizeListItemStructure(previous);
1421
+ });
1422
+ });
1423
+ if (changed && !restoreRangeAfterListMove(savedRange) && lastMoved)
1424
+ focusElementEnd(lastMoved);
1425
+ return changed;
1426
+ };
1427
+ const outdentSelectedListItems = (items) => {
1154
1428
  if (items.length === 0)
1155
1429
  return false;
1430
+ const savedRange = captureRangeForListMove();
1431
+ const sourceLists = Array.from(new Set(items.map((item) => item.parentElement)));
1432
+ const selected = new Set(items);
1156
1433
  let changed = false;
1157
- let lastTargetList = null;
1158
- items.forEach((li) => {
1159
- const previous = li.previousElementSibling;
1160
- if (!previous || previous.tagName.toLowerCase() !== "li")
1434
+ let lastMoved = null;
1435
+ sourceLists.forEach((source) => {
1436
+ const parentItem = source?.parentElement;
1437
+ const outerList = parentItem?.parentElement;
1438
+ if (!source || parentItem?.tagName !== "LI" || !outerList || !["UL", "OL"].includes(outerList.tagName))
1161
1439
  return;
1162
- const targetList = getOrCreateNestedList(previous, listTag);
1163
- targetList.appendChild(li);
1164
- lastTargetList = targetList;
1165
- changed = true;
1440
+ const moving = Array.from(source.children).filter((child) => child instanceof HTMLElement && child.tagName === "LI" && selected.has(child));
1441
+ let insertionPoint = parentItem.nextSibling;
1442
+ moving.forEach((item) => {
1443
+ outerList.insertBefore(item, insertionPoint);
1444
+ lastMoved = item;
1445
+ changed = true;
1446
+ });
1447
+ if (!source.querySelector(":scope > li"))
1448
+ source.remove();
1449
+ normalizeListItemStructure(parentItem);
1166
1450
  });
1167
- if (changed) {
1168
- const lastItem = lastTargetList?.lastElementChild;
1169
- if (lastItem)
1170
- focusElementEnd(lastItem);
1171
- }
1451
+ if (changed && !restoreRangeAfterListMove(savedRange) && lastMoved)
1452
+ focusElementEnd(lastMoved);
1172
1453
  return changed;
1173
1454
  };
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;
1455
+ const canIndentListItems = (items) => {
1456
+ const selected = new Set(items);
1457
+ return items.some((item) => {
1458
+ const previous = item.previousElementSibling;
1459
+ return previous instanceof HTMLElement && previous.tagName === "LI" && !selected.has(previous);
1460
+ });
1461
+ };
1462
+ const canOutdentListItems = (items) => items.some((item) => {
1463
+ const list = item.parentElement;
1464
+ return list?.parentElement?.tagName === "LI" && ["UL", "OL"].includes(list.parentElement.parentElement?.tagName || "");
1465
+ });
1466
+ const applyListDepthChange = (items, direction) => {
1467
+ const enabled = direction === "indent" ? canIndentListItems(items) : canOutdentListItems(items);
1468
+ if (!enabled)
1469
+ return false;
1194
1470
  pushEditorHistory();
1195
- if (nestSelectedListItems(directItems, listTag)) {
1471
+ const changed = direction === "indent"
1472
+ ? nestSelectedListItems(items)
1473
+ : outdentSelectedListItems(items);
1474
+ if (changed) {
1196
1475
  handleInput();
1197
1476
  requestAnimationFrame(updateActiveState);
1198
1477
  }
1478
+ return changed;
1199
1479
  };
1200
1480
  const restyleSelectedListItems = (items, listTag, styleType) => {
1201
1481
  const editor = editableRef.current;
@@ -1241,6 +1521,66 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1241
1521
  });
1242
1522
  return lastSelected;
1243
1523
  };
1524
+ const getDirectSelectionCell = (range) => {
1525
+ const startCell = getClosestCell(range.startContainer);
1526
+ const endCell = getClosestCell(range.endContainer);
1527
+ if (!startCell || startCell !== endCell)
1528
+ return null;
1529
+ const contentBlock = (node) => {
1530
+ const element = node instanceof HTMLElement ? node : node.parentElement;
1531
+ const block = element?.closest(listItemContentSelector);
1532
+ return block && startCell.contains(block) ? block : null;
1533
+ };
1534
+ return contentBlock(range.startContainer) || contentBlock(range.endContainer)
1535
+ ? null
1536
+ : startCell;
1537
+ };
1538
+ const convertDirectTableCellSelectionToList = (range, listTag) => {
1539
+ const cell = getDirectSelectionCell(range);
1540
+ if (!cell || range.collapsed)
1541
+ return null;
1542
+ const fragment = range.extractContents();
1543
+ const parts = [document.createDocumentFragment()];
1544
+ Array.from(fragment.childNodes).forEach((node) => {
1545
+ if (node instanceof HTMLBRElement)
1546
+ parts.push(document.createDocumentFragment());
1547
+ else
1548
+ parts[parts.length - 1].appendChild(node);
1549
+ });
1550
+ const nonEmptyParts = parts.filter((part) => part.textContent?.trim() || part.childNodes.length > 0);
1551
+ if (nonEmptyParts.length === 0) {
1552
+ range.insertNode(fragment);
1553
+ return null;
1554
+ }
1555
+ const list = document.createElement(listTag);
1556
+ nonEmptyParts.forEach((part) => {
1557
+ const paragraph = document.createElement("p");
1558
+ paragraph.appendChild(part);
1559
+ if (!paragraph.innerHTML.trim())
1560
+ paragraph.innerHTML = "<br>";
1561
+ list.appendChild(createListItemFromBlock(paragraph));
1562
+ });
1563
+ range.insertNode(list);
1564
+ const lastItem = list.lastElementChild;
1565
+ if (lastItem)
1566
+ focusElementEnd(lastItem);
1567
+ return list;
1568
+ };
1569
+ const convertDirectTableCellContentsToList = (cell, listTag) => {
1570
+ const range = document.createRange();
1571
+ range.selectNodeContents(cell);
1572
+ if (!cell.textContent?.trim() && !cell.querySelector("img,br")) {
1573
+ const list = document.createElement(listTag);
1574
+ const paragraph = document.createElement("p");
1575
+ paragraph.innerHTML = "<br>";
1576
+ const item = createListItemFromBlock(paragraph);
1577
+ list.appendChild(item);
1578
+ cell.replaceChildren(list);
1579
+ focusElementEnd(item);
1580
+ return list;
1581
+ }
1582
+ return convertDirectTableCellSelectionToList(range, listTag);
1583
+ };
1244
1584
  const applyListStyle = (value) => {
1245
1585
  const listTag = value.startsWith("ordered:") ? "ol" : "ul";
1246
1586
  const styleType = value.replace(/^(ordered|bullet):/, "");
@@ -1249,8 +1589,29 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1249
1589
  const range = getSelectionRangeInEditor();
1250
1590
  if (!range)
1251
1591
  return;
1592
+ const currentBlock = range.collapsed ? getCurrentBlock() : null;
1593
+ if (currentBlock && ["TD", "TH"].includes(currentBlock.tagName)) {
1594
+ pushEditorHistory();
1595
+ const list = convertDirectTableCellContentsToList(currentBlock, listTag);
1596
+ if (list) {
1597
+ list.style.listStyleType = styleType;
1598
+ handleInput();
1599
+ requestAnimationFrame(updateActiveState);
1600
+ }
1601
+ return;
1602
+ }
1603
+ if (!range.collapsed && getDirectSelectionCell(range)) {
1604
+ pushEditorHistory();
1605
+ const list = convertDirectTableCellSelectionToList(range, listTag);
1606
+ if (list) {
1607
+ list.style.listStyleType = styleType;
1608
+ handleInput();
1609
+ requestAnimationFrame(updateActiveState);
1610
+ }
1611
+ return;
1612
+ }
1252
1613
  const selectedBlocks = getSelectedBlocks(range);
1253
- const selectedItems = getSelectedListItems(selectedBlocks);
1614
+ const selectedItems = resolveSelectedListItems(range);
1254
1615
  const selectedPlainBlocks = selectedBlocks.filter((block) => !block.closest("ul,ol"));
1255
1616
  if (selectedItems.length > 0) {
1256
1617
  pushEditorHistory();
@@ -1263,7 +1624,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1263
1624
  return;
1264
1625
  }
1265
1626
  const lists = new Set();
1266
- getSelectedListItems(getSelectedBlocks(range)).forEach((item) => {
1627
+ resolveSelectedListItems(range).forEach((item) => {
1267
1628
  const list = item.parentElement;
1268
1629
  if (list &&
1269
1630
  ["ul", "ol"].includes(list.tagName.toLowerCase())) {
@@ -1350,7 +1711,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1350
1711
  const range = getSelectionRangeInEditor();
1351
1712
  if (!range)
1352
1713
  return;
1353
- const selectedItems = getSelectedListItems(getSelectedBlocks(range));
1714
+ const selectedItems = resolveSelectedListItems(range);
1354
1715
  const selectedChecklists = selectedItems.filter((item) => item.parentElement?.dataset.srteChecklist === "true");
1355
1716
  if (toggleOff && selectedItems.length > 0 && selectedChecklists.length === selectedItems.length) {
1356
1717
  pushEditorHistory();
@@ -1398,7 +1759,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1398
1759
  const selected = blocks.filter((block) => {
1399
1760
  if (!editor.contains(block))
1400
1761
  return false;
1401
- if (block.tagName.toLowerCase() === "li")
1762
+ if (["li", "td", "th"].includes(block.tagName.toLowerCase()))
1402
1763
  return false;
1403
1764
  if (block.closest("ul,ol"))
1404
1765
  return false;
@@ -1426,11 +1787,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1426
1787
  list.style.listStyleType = styleType;
1427
1788
  parent.insertBefore(list, group[0]);
1428
1789
  group.forEach((block) => {
1429
- const li = document.createElement("li");
1430
- li.innerHTML = block.innerHTML || "<br>";
1431
- li.style.textAlign = block.style.textAlign;
1790
+ const li = createListItemFromBlock(block);
1432
1791
  list.appendChild(li);
1433
- block.remove();
1434
1792
  lastLi = li;
1435
1793
  });
1436
1794
  mergeAdjacentCompatibleLists(list);
@@ -1439,42 +1797,6 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1439
1797
  focusElementEnd(lastLi);
1440
1798
  return true;
1441
1799
  };
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
1800
  const convertRootLineSelectionToList = (range, listTag) => {
1479
1801
  const editor = editableRef.current;
1480
1802
  if (!editor || range.collapsed)
@@ -1582,11 +1904,9 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1582
1904
  const isSelected = selected.has(child);
1583
1905
  if (isSelected && toggleOff) {
1584
1906
  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;
1907
+ const content = extractListItemContent(child);
1908
+ content.forEach((node) => parent.insertBefore(node, list));
1909
+ lastTarget = [...content].reverse().find((node) => node instanceof HTMLElement) || parent;
1590
1910
  child.remove();
1591
1911
  changed = true;
1592
1912
  return;
@@ -1624,8 +1944,17 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1624
1944
  };
1625
1945
  const range = getSelectionRangeInEditor();
1626
1946
  if (range && !range.collapsed) {
1947
+ if (getDirectSelectionCell(range)) {
1948
+ pushEditorHistory();
1949
+ if (convertDirectTableCellSelectionToList(range, listTag)) {
1950
+ setListActiveState(true);
1951
+ handleInput();
1952
+ requestAnimationFrame(updateActiveState);
1953
+ }
1954
+ return;
1955
+ }
1627
1956
  const blocks = getSelectedBlocks(range);
1628
- const selectedListItems = getSelectedListItems(blocks);
1957
+ const selectedListItems = resolveSelectedListItems(range);
1629
1958
  if (selectedListItems.length > 0) {
1630
1959
  pushEditorHistory();
1631
1960
  if (transformSelectedListItems(selectedListItems, listTag)) {
@@ -1652,12 +1981,6 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1652
1981
  requestAnimationFrame(updateActiveState);
1653
1982
  return;
1654
1983
  }
1655
- if (convertTableCellSelectionToList(range, listTag)) {
1656
- setListActiveState(true);
1657
- handleInput();
1658
- requestAnimationFrame(updateActiveState);
1659
- return;
1660
- }
1661
1984
  }
1662
1985
  }
1663
1986
  const block = getCurrentBlock();
@@ -1687,12 +2010,24 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1687
2010
  requestAnimationFrame(updateActiveState);
1688
2011
  return;
1689
2012
  }
2013
+ if (["TD", "TH"].includes(block.tagName)) {
2014
+ pushEditorHistory();
2015
+ if (convertDirectTableCellContentsToList(block, listTag)) {
2016
+ setListActiveState(true);
2017
+ handleInput();
2018
+ requestAnimationFrame(updateActiveState);
2019
+ }
2020
+ return;
2021
+ }
1690
2022
  pushEditorHistory();
2023
+ const parent = block.parentElement;
2024
+ if (!parent)
2025
+ return;
2026
+ const nextSibling = block.nextSibling;
1691
2027
  const list = document.createElement(listTag);
1692
- const li = document.createElement("li");
1693
- li.innerHTML = block.innerHTML || "<br>";
2028
+ const li = createListItemFromBlock(block);
1694
2029
  list.appendChild(li);
1695
- block.parentElement?.replaceChild(list, block);
2030
+ parent.insertBefore(list, nextSibling);
1696
2031
  focusElementEnd(li);
1697
2032
  setListActiveState(true);
1698
2033
  handleInput();
@@ -1769,7 +2104,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1769
2104
  }
1770
2105
  return merged;
1771
2106
  };
1772
- const selectedItems = new Set(getSelectedListItems(selected));
2107
+ const selectedItems = new Set(resolveSelectedListItems(range));
1773
2108
  const sourceLists = Array.from(new Set(Array.from(selectedItems, (item) => item.parentElement)));
1774
2109
  let lastWrapped = null;
1775
2110
  sourceLists.forEach((source) => {
@@ -1880,7 +2215,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
1880
2215
  : getSelectedBlocks(range)).filter((block) => editor.contains(block));
1881
2216
  if (blocks.length === 0)
1882
2217
  return;
1883
- const listItems = getSelectedListItems(blocks);
2218
+ const listItems = resolveSelectedListItems(range);
1884
2219
  const plainBlocks = blocks.filter((block) => !block.closest("ul,ol") && !block.closest("table"));
1885
2220
  const getItemCode = (item) => item.querySelector(":scope > pre[data-srte-list-code]");
1886
2221
  const allTargetsActive = listItems.every((item) => Boolean(getItemCode(item))) &&
@@ -2514,15 +2849,16 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
2514
2849
  const host = editableRef.current;
2515
2850
  if (!host)
2516
2851
  return;
2517
- host.focus();
2852
+ restoreSavedSelection();
2518
2853
  let sel = window.getSelection();
2519
- let range = sel && sel.rangeCount > 0 ? sel.getRangeAt(0) : null;
2854
+ let range = sel && sel.rangeCount > 0 ? sel.getRangeAt(0).cloneRange() : null;
2520
2855
  if (!range || !host.contains(range.commonAncestorContainer)) {
2521
2856
  range = document.createRange();
2522
2857
  range.selectNodeContents(host);
2523
2858
  range.collapse(false);
2524
2859
  safeSelectRange(range);
2525
2860
  }
2861
+ pushEditorHistory();
2526
2862
  const span = document.createElement("span");
2527
2863
  span.setAttribute("data-formula", tex);
2528
2864
  try {
@@ -3778,6 +4114,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
3778
4114
  ensureCaretBoundaryParagraphs(el);
3779
4115
  // Add resize handles to tables
3780
4116
  addTableResizeHandles();
4117
+ // Native Enter creates new list items; keep checklist controls in sync.
4118
+ syncChecklistControls(el);
3781
4119
  if (!onChange)
3782
4120
  return;
3783
4121
  const html = el.innerHTML;
@@ -4630,22 +4968,6 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4630
4968
  }
4631
4969
  return getTopLevelMovableElement();
4632
4970
  };
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
- };
4649
4971
  const elementSibling = (element, direction) => {
4650
4972
  let sibling = direction === "previous" ? element.previousSibling : element.nextSibling;
4651
4973
  while (sibling &&
@@ -4704,14 +5026,23 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4704
5026
  return true;
4705
5027
  };
4706
5028
  const moveCurrentElement = (direction) => {
5029
+ restoreSavedSelection();
4707
5030
  const editor = editableRef.current;
5031
+ const range = getSelectionRangeInEditor();
5032
+ const selectedListItems = range ? resolveSelectedListItems(range) : [];
5033
+ if (selectedListItems.length > 0 && (direction === "left" || direction === "right")) {
5034
+ applyListDepthChange(selectedListItems, direction === "right" ? "indent" : "outdent");
5035
+ return;
5036
+ }
4708
5037
  if (moveSelectedBlocks(direction))
4709
5038
  return;
4710
5039
  let target = getMoveTarget();
4711
5040
  if (!editor || !target)
4712
5041
  return;
4713
- pushEditorHistory();
5042
+ let historyPushed = false;
4714
5043
  if (target.tagName === "TD" || target.tagName === "TH") {
5044
+ pushEditorHistory();
5045
+ historyPushed = true;
4715
5046
  const cell = target;
4716
5047
  const paragraph = document.createElement("p");
4717
5048
  while (cell.firstChild)
@@ -4723,30 +5054,58 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4723
5054
  const list = target.parentElement;
4724
5055
  if (!list)
4725
5056
  return;
5057
+ let changed = false;
4726
5058
  if (direction === "up") {
4727
5059
  const previous = elementSibling(target, "previous");
4728
- if (previous?.nodeName === "LI")
4729
- list.insertBefore(target, previous);
5060
+ if (previous?.nodeName !== "LI")
5061
+ return;
5062
+ pushEditorHistory();
5063
+ list.insertBefore(target, previous);
5064
+ changed = true;
4730
5065
  }
4731
5066
  else if (direction === "down") {
4732
5067
  const next = elementSibling(target, "next");
4733
- if (next?.nodeName === "LI")
4734
- list.insertBefore(next, target);
5068
+ if (next?.nodeName !== "LI")
5069
+ return;
5070
+ pushEditorHistory();
5071
+ list.insertBefore(next, target);
5072
+ changed = true;
4735
5073
  }
4736
5074
  else if (direction === "right") {
4737
- nestSelectedListItems([target], list.tagName.toLowerCase());
5075
+ if (!canIndentListItems([target]))
5076
+ return;
5077
+ pushEditorHistory();
5078
+ changed = nestSelectedListItems([target]);
4738
5079
  }
4739
5080
  else {
4740
- outdentListItem(target);
5081
+ if (!canOutdentListItems([target]))
5082
+ return;
5083
+ pushEditorHistory();
5084
+ changed = outdentSelectedListItems([target]);
4741
5085
  }
5086
+ if (!changed)
5087
+ return;
5088
+ if (direction === "up" || direction === "down")
5089
+ focusElementEnd(target);
5090
+ handleInput();
5091
+ requestAnimationFrame(updateActiveState);
5092
+ return;
4742
5093
  }
4743
5094
  else if (direction === "up") {
4744
5095
  const previous = elementSibling(target, "previous");
5096
+ if (!previous)
5097
+ return;
5098
+ if (!historyPushed)
5099
+ pushEditorHistory();
4745
5100
  if (previous)
4746
5101
  target.parentElement?.insertBefore(target, previous);
4747
5102
  }
4748
5103
  else if (direction === "down") {
4749
5104
  const next = elementSibling(target, "next");
5105
+ if (!next)
5106
+ return;
5107
+ if (!historyPushed)
5108
+ pushEditorHistory();
4750
5109
  if (next)
4751
5110
  target.parentElement?.insertBefore(next, target);
4752
5111
  }
@@ -4755,6 +5114,10 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4755
5114
  const nextMargin = direction === "right"
4756
5115
  ? Math.min(current + 24, 240)
4757
5116
  : Math.max(current - 24, 0);
5117
+ if (nextMargin === current)
5118
+ return;
5119
+ if (!historyPushed)
5120
+ pushEditorHistory();
4758
5121
  target.style.marginLeft = nextMargin ? `${nextMargin}px` : "";
4759
5122
  }
4760
5123
  focusElementEnd(target);
@@ -4789,14 +5152,13 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4789
5152
  const activeButtonStyle = (active = false, extra = {}) => ({
4790
5153
  height: 32,
4791
5154
  minWidth: 32,
4792
- padding: "0 8px",
4793
- border: active
4794
- ? "2px solid var(--srte-accent)"
4795
- : "1px solid var(--srte-input-border)",
4796
- borderRadius: 6,
4797
- background: active ? "var(--srte-accent-bg)" : "var(--srte-input-bg)",
4798
- color: "var(--srte-input-text)",
4799
- boxShadow: active ? "inset 0 0 0 1px var(--srte-accent)" : "none",
5155
+ padding: "0 7px",
5156
+ border: active ? "1px solid color-mix(in srgb, var(--srte-primary) 35%, transparent)" : "1px solid transparent",
5157
+ borderRadius: 8,
5158
+ background: active ? "var(--srte-accent-bg)" : "transparent",
5159
+ color: active ? "var(--srte-primary)" : "var(--srte-foreground)",
5160
+ boxShadow: "none",
5161
+ cursor: "pointer",
4800
5162
  ...extra,
4801
5163
  });
4802
5164
  const preserveToolbarMouseDown = (event) => {
@@ -4810,16 +5172,16 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4810
5172
  const editorClass = `srte-editor${theme === 'dark' ? ' srte-dark' : ''}${className ? ' ' + className : ''}`;
4811
5173
  return (_jsxs("div", { className: editorClass, style: {
4812
5174
  border: "1px solid var(--srte-border)",
4813
- borderRadius: 6,
5175
+ borderRadius: "var(--srte-radius)",
4814
5176
  width: "100%",
4815
5177
  maxWidth: "100vw",
4816
5178
  overflow: "visible",
4817
5179
  display: "flex",
4818
5180
  flexDirection: "column",
4819
- background: "var(--srte-bg)",
4820
- color: "var(--srte-text)",
5181
+ background: "var(--srte-background)",
5182
+ color: "var(--srte-foreground)",
4821
5183
  boxSizing: "border-box"
4822
- }, children: [_jsxs("div", { "aria-disabled": readOnly, onMouseDown: (event) => {
5184
+ }, children: [_jsxs("div", { className: "srte-toolbar", role: "toolbar", "aria-label": "Text formatting", "aria-disabled": readOnly, onMouseDown: (event) => {
4823
5185
  if (readOnly) {
4824
5186
  event.preventDefault();
4825
5187
  event.stopPropagation();
@@ -4837,16 +5199,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4837
5199
  event.preventDefault();
4838
5200
  event.stopPropagation();
4839
5201
  }, style: {
4840
- display: "flex",
4841
- flexWrap: "wrap",
4842
5202
  maxWidth: "100%",
4843
- gap: 8,
4844
- padding: 8,
4845
- borderBottom: "1px solid var(--srte-border-light)",
4846
- background: "var(--srte-toolbar-bg)",
4847
- position: "sticky",
4848
- top: 0,
4849
- zIndex: 1,
4850
5203
  opacity: readOnly ? 0.55 : 1,
4851
5204
  pointerEvents: readOnly ? "none" : "auto",
4852
5205
  userSelect: readOnly ? "none" : undefined,
@@ -4888,7 +5241,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4888
5241
  } }), _jsx("input", { ref: mdInputRef, type: "file", accept: ".md,.markdown,text/markdown,text/plain", style: { display: "none" }, onChange: (e) => {
4889
5242
  importTextFile(e.currentTarget.files, "md");
4890
5243
  e.currentTarget.value = "";
4891
- } }), _jsxs("select", { value: currentBlockType, onPointerDown: preserveEditorSelection, onMouseDown: preserveEditorSelection, onChange: (e) => {
5244
+ } }), _jsxs(ToolbarGroup, { label: "History", children: [_jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Undo", title: "Undo", onClick: () => exec("undo"), children: _jsx(ToolbarIcon, { name: "undo" }) }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Redo", title: "Redo", onClick: () => exec("redo"), children: _jsx(ToolbarIcon, { name: "redo" }) })] }), _jsxs("select", { value: currentBlockType, onPointerDown: preserveEditorSelection, onMouseDown: preserveEditorSelection, onChange: (e) => {
4892
5245
  const val = e.target.value;
4893
5246
  if (val === "p")
4894
5247
  applyFormatBlock("<p>");
@@ -4911,16 +5264,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4911
5264
  borderRadius: 6,
4912
5265
  background: "var(--srte-input-bg)",
4913
5266
  color: "var(--srte-input-text)",
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: {
5267
+ }, 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" })] }), _jsx("button", { type: "button", className: `srte-tool-button${activeState.bold ? " srte-active" : ""}`, title: "Bold", onClick: () => exec("bold"), "aria-pressed": activeState.bold, style: activeButtonStyle(activeState.bold), children: _jsx("span", { style: { fontWeight: 700 }, children: "B" }) }), _jsx("button", { type: "button", className: `srte-tool-button${activeState.italic ? " srte-active" : ""}`, title: "Italic", onClick: () => exec("italic"), "aria-pressed": activeState.italic, style: activeButtonStyle(activeState.italic, { fontStyle: "italic" }), children: "I" }), _jsx("button", { type: "button", className: `srte-tool-button${activeState.underline ? " srte-active" : ""}`, title: "Underline", onClick: () => exec("underline"), "aria-pressed": activeState.underline, style: activeButtonStyle(activeState.underline, { textDecoration: "underline" }), children: "U" }), _jsx("button", { type: "button", className: `srte-tool-button${activeState.strikeThrough ? " srte-active" : ""}`, 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: {
4924
5268
  height: 32,
4925
5269
  padding: "0 8px",
4926
5270
  border: "1px solid var(--srte-input-border)",
@@ -4944,7 +5288,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4944
5288
  background: "var(--srte-input-bg)",
4945
5289
  color: "var(--srte-input-text)",
4946
5290
  maxWidth: 100,
4947
- }, children: [_jsx("option", { value: "", disabled: true, children: "Font" }), fonts.map((f) => (_jsx("option", { value: f.value, children: f.name }, f.value)))] })), _jsx("button", { title: "Text Color", onClick: () => {
5291
+ }, children: [_jsx("option", { value: "", disabled: true, children: "Font" }), fonts.map((f) => (_jsx("option", { value: f.value, children: f.name }, f.value)))] })), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Text color", title: "Text Color", onClick: () => {
4948
5292
  setColorPickerType('text');
4949
5293
  setShowColorPicker(true);
4950
5294
  }, style: {
@@ -4956,7 +5300,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4956
5300
  background: "var(--srte-input-bg)",
4957
5301
  color: "var(--srte-input-text)",
4958
5302
  position: "relative",
4959
- }, children: _jsx("span", { style: { fontWeight: 700, borderBottom: "3px solid currentColor", lineHeight: 1 }, children: "A" }) }), _jsx("button", { title: "Background Color", onClick: () => {
5303
+ }, children: _jsx("span", { style: { fontWeight: 700, borderBottom: "3px solid currentColor", lineHeight: 1 }, children: "A" }) }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Background color", title: "Background Color", onClick: () => {
4960
5304
  setColorPickerType('background');
4961
5305
  setShowColorPicker(true);
4962
5306
  }, style: {
@@ -4967,10 +5311,10 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4967
5311
  borderRadius: 6,
4968
5312
  background: "var(--srte-input-bg)",
4969
5313
  color: "var(--srte-input-text)",
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" })] }), [
5314
+ }, children: _jsx("span", { style: { fontWeight: 700, padding: "1px 4px", background: "var(--srte-accent-bg)", borderRadius: 3 }, children: "A" }) }), _jsxs("button", { type: "button", className: `srte-tool-button${activeState.subscript ? " srte-active" : ""}`, title: "Subscript", onMouseDown: preserveEditorSelection, onClick: () => toggleInlineScript("subscript"), "aria-pressed": activeState.subscript, style: activeButtonStyle(activeState.subscript), children: ["X", _jsx("sub", { children: "2" })] }), _jsxs("button", { type: "button", className: `srte-tool-button${activeState.superscript ? " srte-active" : ""}`, title: "Superscript", onMouseDown: preserveEditorSelection, onClick: () => toggleInlineScript("superscript"), "aria-pressed": activeState.superscript, style: activeButtonStyle(activeState.superscript), children: ["X", _jsx("sup", { children: "2" })] }), [
4971
5315
  {
4972
5316
  key: "check",
4973
- label: "",
5317
+ icon: "checklist",
4974
5318
  title: "Checklist",
4975
5319
  active: activeState.checklist,
4976
5320
  action: () => applyChecklist(false, true),
@@ -4978,7 +5322,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4978
5322
  },
4979
5323
  {
4980
5324
  key: "bullet",
4981
- label: "•≡",
5325
+ icon: "bullets",
4982
5326
  title: "Bulleted list",
4983
5327
  active: activeState.unorderedList,
4984
5328
  action: () => applyListStyle("bullet:disc"),
@@ -4986,13 +5330,13 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
4986
5330
  },
4987
5331
  {
4988
5332
  key: "ordered",
4989
- label: "1≡",
5333
+ icon: "numbers",
4990
5334
  title: "Numbered list",
4991
5335
  active: activeState.orderedList,
4992
5336
  action: () => applyListStyle("ordered:decimal"),
4993
5337
  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
5338
  },
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) => {
5339
+ ].map((control) => (_jsxs("span", { className: "srte-split-control", children: [_jsx("button", { type: "button", className: `srte-tool-button${control.active ? " srte-active" : ""}`, title: control.title, onPointerDown: preserveEditorSelection, onClick: control.action, "aria-pressed": control.active, children: _jsx(ToolbarIcon, { name: control.icon }) }), _jsxs("select", { defaultValue: "", "aria-label": `${control.title} styles`, title: `${control.title} styles`, onPointerDown: preserveEditorSelection, onMouseDown: preserveEditorSelection, onChange: (event) => {
4996
5340
  const selected = event.currentTarget.value;
4997
5341
  if (selected === "check:plain")
4998
5342
  applyChecklist(false);
@@ -5001,200 +5345,62 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
5001
5345
  else if (selected)
5002
5346
  applyListStyle(selected);
5003
5347
  event.currentTarget.value = "";
5348
+ }, 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", className: `srte-tool-button${activeState.blockquote ? " srte-active" : ""}`, title: "Blockquote", onPointerDown: preserveEditorSelection, onClick: toggleBlockquote, "aria-pressed": activeState.blockquote, children: _jsx(ToolbarIcon, { name: "quote" }) }), _jsx("button", { type: "button", className: "srte-tool-button", "aria-label": "Special characters", title: "Special characters", onClick: () => setShowSpecialChars(true), children: _jsx(ToolbarIcon, { name: "omega" }) }), _jsx("button", { type: "button", className: `srte-tool-button${activeState.codeBlock ? " srte-active" : ""}`, title: "Code block", onPointerDown: preserveEditorSelection, onClick: toggleCodeBlock, "aria-pressed": activeState.codeBlock, children: _jsx(ToolbarIcon, { name: "code" }) }), _jsx("button", { type: "button", className: `srte-tool-button${activeState.link ? " srte-active" : ""}`, title: "Insert link", "aria-label": "Insert or edit link", "aria-pressed": activeState.link, onPointerDown: preserveEditorSelection, onClick: () => openLinkEditor(), children: _jsx(ToolbarIcon, { name: "link" }) }), (table || media || formula) && (_jsxs(ToolbarMenu, { label: "Insert", icon: "insert", priority: 2, children: [table && _jsx(MenuItem, { icon: "table", label: "Table", title: "Insert table", onClick: () => setShowTableDialog(true) }), media && _jsx(MenuItem, { icon: "image", label: "Upload image", title: "Insert image", onClick: insertImage }), media && _jsx(MenuItem, { icon: "media", label: "Media", title: "Open media", onClick: () => mediaManager ? setShowMediaManager(true) : insertImage() }), formula && _jsx(MenuItem, { icon: "formula", label: "Formula", title: "Insert formula", onClick: () => setShowFormulaDialog(true) })] })), selectedImage && (_jsxs(ToolbarMenu, { label: "Image alignment", icon: "image", children: [_jsx(MenuItem, { icon: "align-center", label: "Center image", onClick: () => {
5349
+ selectedImage.style.display = "block";
5350
+ selectedImage.style.margin = "0 auto";
5351
+ selectedImage.style.float = "none";
5352
+ scheduleImageOverlay();
5353
+ handleInput();
5354
+ } }), _jsx(MenuItem, { icon: "align-left", label: "Float left", onClick: () => {
5355
+ selectedImage.style.display = "inline";
5356
+ selectedImage.style.float = "left";
5357
+ selectedImage.style.margin = "0 8px 8px 0";
5358
+ scheduleImageOverlay();
5359
+ handleInput();
5360
+ } }), _jsx(MenuItem, { icon: "align-right", label: "Float right", onClick: () => {
5361
+ selectedImage.style.display = "inline";
5362
+ selectedImage.style.float = "right";
5363
+ selectedImage.style.margin = "0 0 8px 8px";
5364
+ scheduleImageOverlay();
5365
+ handleInput();
5366
+ } })] })), _jsxs(ToolbarGroup, { label: "Document", priority: 3, children: [_jsxs(ToolbarMenu, { label: loadingPdf || loadingDocx ? "Importing document" : "Import document", icon: "import", children: [_jsx(MenuItem, { icon: "import", label: "PDF (.pdf)", disabled: loadingPdf || loadingDocx, onClick: () => pdfInputRef.current?.click() }), _jsx(MenuItem, { icon: "import", label: "Microsoft Word (.docx)", disabled: loadingPdf || loadingDocx, onClick: () => docxInputRef.current?.click() }), _jsx(MenuItem, { icon: "import", label: "HTML (.html)", disabled: loadingPdf || loadingDocx, onClick: () => htmlInputRef.current?.click() }), _jsx(MenuItem, { icon: "import", label: "Markdown (.md)", disabled: loadingPdf || loadingDocx, onClick: () => mdInputRef.current?.click() })] }), _jsxs(ToolbarMenu, { label: "Export document", icon: "export", children: [_jsx(MenuItem, { icon: "export", label: "PDF", onClick: exportPdf }), _jsx(MenuItem, { icon: "export", label: "Microsoft Word (.docx)", onClick: () => void exportDocx() }), _jsx(MenuItem, { icon: "export", label: "HTML", onClick: exportHtml }), _jsx(MenuItem, { icon: "export", label: "Markdown", onClick: exportMarkdown })] }), _jsxs("select", { className: "srte-command-proxy", "aria-hidden": "true", tabIndex: -1, defaultValue: "", "aria-label": "Import document", title: "Import document", disabled: loadingPdf || loadingDocx, onChange: (event) => {
5367
+ const format = event.currentTarget.value;
5368
+ event.currentTarget.value = "";
5369
+ if (format === "pdf")
5370
+ pdfInputRef.current?.click();
5371
+ else if (format === "docx")
5372
+ docxInputRef.current?.click();
5373
+ else if (format === "html")
5374
+ htmlInputRef.current?.click();
5375
+ else if (format === "markdown")
5376
+ mdInputRef.current?.click();
5004
5377
  }, style: {
5005
- width: 28,
5006
5378
  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: {
5014
- height: 32,
5015
- minWidth: 32,
5016
- padding: "0 8px",
5017
- border: "1px solid var(--srte-input-border)",
5018
- borderRadius: 6,
5019
- background: "var(--srte-input-bg)",
5020
- color: "var(--srte-input-text)",
5021
- }, children: "\u03A9" }), _jsx("button", { type: "button", title: "Code block", onPointerDown: preserveEditorSelection, onClick: toggleCodeBlock, "aria-pressed": activeState.codeBlock, style: activeButtonStyle(activeState.codeBlock, {
5022
- minWidth: 36,
5023
- fontFamily: "ui-monospace, SFMono-Regular, Menlo",
5024
- }), children: "< />" }), formula && (_jsx("button", { title: "Insert formula", onClick: () => setShowFormulaDialog(true), style: {
5025
- height: 32,
5026
- minWidth: 32,
5027
- padding: "0 8px",
5028
- border: "1px solid var(--srte-input-border)",
5029
- borderRadius: 6,
5030
- background: "var(--srte-input-bg)",
5031
- color: "var(--srte-input-text)",
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: {
5033
- height: 32,
5034
- minWidth: 34,
5035
- padding: "0 8px",
5036
- border: "1px solid var(--srte-input-border)",
5037
- borderRadius: 6,
5038
- background: "var(--srte-input-bg)",
5039
- color: "var(--srte-input-text)",
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: {
5044
- height: 32,
5045
- padding: "0 10px",
5046
- border: "1px solid var(--srte-input-border)",
5047
- borderRadius: 6,
5048
- background: "var(--srte-input-bg)",
5049
- color: "var(--srte-input-text)",
5050
- }, children: "\uD83D\uDDBC\uFE0F Image" }), mediaManager && (_jsx("button", { title: "Open media manager", onClick: () => setShowMediaManager(true), style: {
5051
- height: 32,
5052
- padding: "0 10px",
5053
- border: "1px solid var(--srte-input-border)",
5054
- borderRadius: 6,
5055
- background: "var(--srte-input-bg)",
5056
- color: "var(--srte-input-text)",
5057
- }, children: "\uD83D\uDCC1 Media" })), _jsx("button", { title: "Import PDF", onClick: () => pdfInputRef.current?.click(), disabled: loadingPdf, style: {
5058
- height: 32,
5059
- padding: "0 10px",
5060
- border: "1px solid var(--srte-input-border)",
5061
- borderRadius: 6,
5062
- background: "var(--srte-input-bg)",
5063
- color: "var(--srte-input-text)",
5064
- opacity: loadingPdf ? 0.5 : 1,
5065
- }, children: loadingPdf ? '⌛ Importing...' : '📄 PDF' }), _jsx("button", { title: "Import DOCX", onClick: () => docxInputRef.current?.click(), disabled: loadingDocx, style: {
5066
- height: 32,
5067
- padding: "0 10px",
5068
- border: "1px solid var(--srte-input-border)",
5069
- borderRadius: 6,
5070
- background: "var(--srte-input-bg)",
5071
- color: "var(--srte-input-text)",
5072
- opacity: loadingDocx ? 0.5 : 1,
5073
- }, children: loadingDocx ? '⌛ Importing...' : '📝 DOCX' }), _jsx("button", { title: "Import HTML", onClick: () => htmlInputRef.current?.click(), style: {
5074
- height: 32,
5075
- padding: "0 10px",
5076
- border: "1px solid var(--srte-input-border)",
5077
- borderRadius: 6,
5078
- background: "var(--srte-input-bg)",
5079
- color: "var(--srte-input-text)",
5080
- }, children: "HTML" }), _jsx("button", { title: "Import Markdown", onClick: () => mdInputRef.current?.click(), style: {
5081
- height: 32,
5082
- padding: "0 10px",
5083
- border: "1px solid var(--srte-input-border)",
5084
- borderRadius: 6,
5085
- background: "var(--srte-input-bg)",
5086
- color: "var(--srte-input-text)",
5087
- }, children: "MD" }), _jsx("button", { title: "Export HTML", onClick: exportHtml, style: {
5088
- height: 32,
5089
- padding: "0 10px",
5090
- border: "1px solid var(--srte-input-border)",
5091
- borderRadius: 6,
5092
- background: "var(--srte-input-bg)",
5093
- color: "var(--srte-input-text)",
5094
- }, children: "Export HTML" }), _jsx("button", { title: "Export Markdown", onClick: exportMarkdown, style: {
5095
- height: 32,
5096
- padding: "0 10px",
5379
+ padding: "0 8px",
5097
5380
  border: "1px solid var(--srte-input-border)",
5098
5381
  borderRadius: 6,
5099
5382
  background: "var(--srte-input-bg)",
5100
5383
  color: "var(--srte-input-text)",
5101
- }, children: "Export MD" }), _jsx("button", { title: "Export DOCX", onClick: exportDocx, style: {
5102
- height: 32,
5103
- padding: "0 10px",
5104
- border: "1px solid var(--srte-input-border)",
5105
- borderRadius: 6,
5106
- background: "var(--srte-input-bg)",
5107
- color: "var(--srte-input-text)",
5108
- }, children: "Export DOCX" }), _jsx("button", { title: "Export PDF", onClick: exportPdf, style: {
5384
+ opacity: loadingPdf || loadingDocx ? 0.6 : 1,
5385
+ }, children: [_jsx("option", { value: "", disabled: true, children: loadingPdf || loadingDocx ? "Importing…" : "Import…" }), _jsx("option", { value: "pdf", children: "PDF (.pdf)" }), _jsx("option", { value: "docx", children: "Word (.docx)" }), _jsx("option", { value: "html", children: "HTML (.html)" }), _jsx("option", { value: "markdown", children: "Markdown (.md)" })] }), _jsxs("select", { className: "srte-command-proxy", "aria-hidden": "true", tabIndex: -1, defaultValue: "", "aria-label": "Export document", title: "Export document", onChange: (event) => {
5386
+ const format = event.currentTarget.value;
5387
+ event.currentTarget.value = "";
5388
+ if (format === "html")
5389
+ exportHtml();
5390
+ else if (format === "markdown")
5391
+ exportMarkdown();
5392
+ else if (format === "docx")
5393
+ void exportDocx();
5394
+ else if (format === "pdf")
5395
+ exportPdf();
5396
+ }, style: {
5109
5397
  height: 32,
5110
- padding: "0 10px",
5398
+ padding: "0 8px",
5111
5399
  border: "1px solid var(--srte-input-border)",
5112
5400
  borderRadius: 6,
5113
5401
  background: "var(--srte-input-bg)",
5114
5402
  color: "var(--srte-input-text)",
5115
- }, children: "Export PDF" }), _jsxs("div", { style: {
5116
- display: "inline-flex",
5117
- gap: 4,
5118
- alignItems: "center",
5119
- marginLeft: 6,
5120
- }, children: [_jsx("span", { style: { fontSize: 12, opacity: 0.7 }, children: "Image align:" }), _jsx("button", { onClick: () => {
5121
- const img = selectedImage;
5122
- if (!img)
5123
- return;
5124
- img.style.display = "block";
5125
- img.style.margin = "0 auto";
5126
- img.style.float = "none";
5127
- scheduleImageOverlay();
5128
- handleInput();
5129
- }, title: "Center", style: {
5130
- height: 28,
5131
- minWidth: 28,
5132
- padding: "0 6px",
5133
- border: "1px solid var(--srte-input-border)",
5134
- borderRadius: 6,
5135
- background: "var(--srte-input-bg)",
5136
- color: "var(--srte-input-text)",
5137
- }, children: "\u2299" }), _jsx("button", { onClick: () => {
5138
- const img = selectedImage;
5139
- if (!img)
5140
- return;
5141
- img.style.display = "inline";
5142
- img.style.float = "left";
5143
- img.style.margin = "0 8px 8px 0";
5144
- scheduleImageOverlay();
5145
- handleInput();
5146
- }, title: "Float left", style: {
5147
- height: 28,
5148
- minWidth: 28,
5149
- padding: "0 6px",
5150
- border: "1px solid var(--srte-input-border)",
5151
- borderRadius: 6,
5152
- background: "var(--srte-input-bg)",
5153
- color: "var(--srte-input-text)",
5154
- }, children: "\u27F8" }), _jsx("button", { onClick: () => {
5155
- const img = selectedImage;
5156
- if (!img)
5157
- return;
5158
- img.style.display = "inline";
5159
- img.style.float = "right";
5160
- img.style.margin = "0 0 8px 8px";
5161
- scheduleImageOverlay();
5162
- handleInput();
5163
- }, title: "Float right", style: {
5164
- height: 28,
5165
- minWidth: 28,
5166
- padding: "0 6px",
5167
- border: "1px solid var(--srte-input-border)",
5168
- borderRadius: 6,
5169
- background: "var(--srte-input-bg)",
5170
- color: "var(--srte-input-text)",
5171
- }, children: "\u27F9" })] })] })), table && (_jsx("button", { title: "Insert table", onClick: () => setShowTableDialog(true), style: {
5172
- height: 32,
5173
- padding: "0 10px",
5174
- border: "1px solid var(--srte-input-border)",
5175
- borderRadius: 6,
5176
- background: "var(--srte-input-bg)",
5177
- color: "var(--srte-input-text)",
5178
- }, children: "\u2795 Table" })), _jsxs("div", { style: {
5179
- display: "inline-flex",
5180
- gap: 4,
5181
- alignItems: "center",
5182
- marginLeft: 6,
5183
- }, children: [_jsx("span", { style: { fontSize: 12, opacity: 0.7 }, children: "Move:" }), _jsx("button", { title: "Move selected block up", onClick: () => moveCurrentElement("up"), style: activeButtonStyle(false, { height: 28, minWidth: 28, padding: "0 6px" }), children: "\u2191" }), _jsx("button", { title: "Move selected block down", onClick: () => moveCurrentElement("down"), style: activeButtonStyle(false, { height: 28, minWidth: 28, padding: "0 6px" }), children: "\u2193" }), _jsx("button", { title: "Move selected block left", onClick: () => moveCurrentElement("left"), style: activeButtonStyle(false, { height: 28, minWidth: 28, padding: "0 6px" }), children: "\u2190" }), _jsx("button", { title: "Move selected block right", onClick: () => moveCurrentElement("right"), style: activeButtonStyle(false, { height: 28, minWidth: 28, padding: "0 6px" }), children: "\u2192" })] }), _jsx("button", { title: "Undo", onClick: () => exec("undo"), style: {
5184
- height: 32,
5185
- padding: "0 10px",
5186
- border: "1px solid var(--srte-input-border)",
5187
- borderRadius: 6,
5188
- background: "var(--srte-input-bg)",
5189
- color: "var(--srte-input-text)",
5190
- }, children: "\u238C Undo" }), _jsx("button", { title: "Redo", onClick: () => exec("redo"), style: {
5191
- height: 32,
5192
- padding: "0 10px",
5193
- border: "1px solid var(--srte-input-border)",
5194
- borderRadius: 6,
5195
- background: "var(--srte-input-bg)",
5196
- color: "var(--srte-input-text)",
5197
- }, children: "\u293E Redo" })] }), media && mediaManager && (_jsx(MediaManager, { open: showMediaManager, onClose: () => setShowMediaManager(false), adapter: mediaManager, onSelect: (item) => {
5403
+ }, children: [_jsx("option", { value: "", disabled: true, children: "Export\u2026" }), _jsx("option", { value: "html", children: "HTML (.html)" }), _jsx("option", { value: "markdown", children: "Markdown (.md)" }), _jsx("option", { value: "docx", children: "Word (.docx)" }), _jsx("option", { value: "pdf", children: "PDF (.pdf)" })] })] }), _jsx(ToolbarMenu, { label: "Text alignment", icon: currentAlignment === "center" ? "align-center" : currentAlignment === "right" ? "align-right" : currentAlignment === "justify" ? "justify" : "align-left", active: currentAlignment !== "left", children: ["left", "center", "right", "justify"].map((alignment) => (_jsx(MenuItem, { icon: alignment === "left" ? "align-left" : alignment === "center" ? "align-center" : alignment === "right" ? "align-right" : "justify", label: alignment === "justify" ? "Justify" : `Align ${alignment}`, title: alignment === "justify" ? "Justify" : `Align ${alignment}`, active: currentAlignment === alignment, onClick: () => applyTextAlignment(alignment) }, alignment))) }), _jsxs(ToolbarMenu, { label: "Move and indent", icon: "move", priority: 2, children: [_jsx(MenuItem, { icon: "up", label: "Move block up", title: "Move selected block up", onClick: () => moveCurrentElement("up") }), _jsx(MenuItem, { icon: "down", label: "Move block down", title: "Move selected block down", onClick: () => moveCurrentElement("down") }), _jsx("div", { className: "srte-menu-separator", role: "separator" }), _jsx(MenuItem, { icon: "outdent", label: "Decrease indent", title: "Move selected block left", onClick: () => moveCurrentElement("left") }), _jsx(MenuItem, { icon: "indent", label: "Increase indent", title: "Move selected block right", onClick: () => moveCurrentElement("right") })] }), _jsx("div", { className: "srte-mobile-more", children: _jsxs(ToolbarMenu, { label: "More editor actions", icon: "more", children: [table && _jsx(MenuItem, { icon: "table", label: "Insert table", onClick: () => setShowTableDialog(true) }), media && _jsx(MenuItem, { icon: "image", label: "Upload image", onClick: insertImage }), media && _jsx(MenuItem, { icon: "media", label: "Media", onClick: () => mediaManager ? setShowMediaManager(true) : insertImage() }), formula && _jsx(MenuItem, { icon: "formula", label: "Insert formula", onClick: () => setShowFormulaDialog(true) }), _jsx(MenuItem, { icon: "omega", label: "Special characters", onClick: () => setShowSpecialChars(true) }), _jsx(MenuItem, { icon: "up", label: "Move block up", onClick: () => moveCurrentElement("up") }), _jsx(MenuItem, { icon: "down", label: "Move block down", onClick: () => moveCurrentElement("down") }), _jsx(MenuItem, { icon: "outdent", label: "Decrease indent", onClick: () => moveCurrentElement("left") }), _jsx(MenuItem, { icon: "indent", label: "Increase indent", onClick: () => moveCurrentElement("right") }), _jsx("div", { className: "srte-menu-separator", role: "separator" }), _jsx(MenuItem, { icon: "import", label: "Import Word document", onClick: () => docxInputRef.current?.click() }), _jsx(MenuItem, { icon: "import", label: "Import HTML", onClick: () => htmlInputRef.current?.click() }), _jsx(MenuItem, { icon: "import", label: "Import Markdown", onClick: () => mdInputRef.current?.click() }), _jsx("div", { className: "srte-menu-separator", role: "separator" }), _jsx(MenuItem, { icon: "export", label: "Export PDF", onClick: exportPdf }), _jsx(MenuItem, { icon: "export", label: "Export Word document", onClick: () => void exportDocx() }), _jsx(MenuItem, { icon: "export", label: "Export HTML", onClick: exportHtml }), _jsx(MenuItem, { icon: "export", label: "Export Markdown", onClick: exportMarkdown })] }) })] }), media && mediaManager && (_jsx(MediaManager, { open: showMediaManager, onClose: () => setShowMediaManager(false), adapter: mediaManager, onSelect: (item) => {
5198
5404
  if (item?.url)
5199
5405
  insertImageAtSelection(item);
5200
5406
  } })), table && showTableDialog && (_jsx("div", { style: {
@@ -5654,7 +5860,8 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
5654
5860
  pushEditorHistory();
5655
5861
  const checked = item.dataset.checked !== "true";
5656
5862
  item.dataset.checked = checked ? "true" : "false";
5657
- t.textContent = checked ? "" : "";
5863
+ t.dataset.checked = checked ? "true" : "false";
5864
+ t.setAttribute("aria-pressed", checked ? "true" : "false");
5658
5865
  t.setAttribute("aria-label", checked ? "Mark incomplete" : "Mark complete");
5659
5866
  item.style.textDecoration =
5660
5867
  list.dataset.srteChecklistStrike === "true" && checked ? "line-through" : "";
@@ -5751,26 +5958,16 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
5751
5958
  }
5752
5959
  // Keep Tab for indentation in lists; otherwise insert 2 spaces
5753
5960
  if (e.key === "Tab") {
5754
- e.preventDefault();
5755
5961
  const selection = getSelectionRangeInEditor();
5756
- const selectedListItems = selection && !selection.collapsed
5757
- ? getSelectedListItems(getSelectedBlocks(selection))
5758
- : [];
5962
+ const selectedListItems = selection ? resolveSelectedListItems(selection) : [];
5759
5963
  if (selectedListItems.length > 0) {
5760
- if (!e.shiftKey) {
5761
- nestListSelection();
5762
- return;
5763
- }
5764
- pushEditorHistory();
5765
- exec("outdent");
5964
+ e.preventDefault();
5965
+ applyListDepthChange(selectedListItems, e.shiftKey ? "outdent" : "indent");
5766
5966
  return;
5767
5967
  }
5968
+ e.preventDefault();
5768
5969
  const currentBlock = getCurrentBlock();
5769
- if (currentBlock?.closest("li")) {
5770
- pushEditorHistory();
5771
- exec(e.shiftKey ? "outdent" : "indent");
5772
- }
5773
- else {
5970
+ if (!currentBlock?.closest("li")) {
5774
5971
  captureInputHistory("insertText");
5775
5972
  document.execCommand("insertText", false, " ");
5776
5973
  }