@overlap/rte 0.1.5 → 0.1.7

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.
Files changed (68) hide show
  1. package/README.md +52 -40
  2. package/dist/components/Editor.d.ts.map +1 -1
  3. package/dist/components/Icons.d.ts +11 -0
  4. package/dist/components/Icons.d.ts.map +1 -1
  5. package/dist/constants.d.ts +9 -0
  6. package/dist/constants.d.ts.map +1 -0
  7. package/dist/hooks/useCheckbox.d.ts +23 -0
  8. package/dist/hooks/useCheckbox.d.ts.map +1 -0
  9. package/dist/hooks/useEditorEvents.d.ts +18 -0
  10. package/dist/hooks/useEditorEvents.d.ts.map +1 -0
  11. package/dist/hooks/useEditorInit.d.ts +23 -0
  12. package/dist/hooks/useEditorInit.d.ts.map +1 -0
  13. package/dist/hooks/useEditorSelection.d.ts +7 -0
  14. package/dist/hooks/useEditorSelection.d.ts.map +1 -0
  15. package/dist/index.d.ts +77 -7
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.esm.js +2355 -772
  18. package/dist/index.esm.js.map +1 -1
  19. package/dist/index.js +2366 -770
  20. package/dist/index.js.map +1 -1
  21. package/dist/plugins/alignment.d.ts +9 -0
  22. package/dist/plugins/alignment.d.ts.map +1 -0
  23. package/dist/plugins/blockFormat.d.ts +2 -2
  24. package/dist/plugins/blockFormat.d.ts.map +1 -1
  25. package/dist/plugins/index.d.ts +4 -0
  26. package/dist/plugins/index.d.ts.map +1 -1
  27. package/dist/plugins/linkDialog.d.ts +17 -0
  28. package/dist/plugins/linkDialog.d.ts.map +1 -0
  29. package/dist/plugins/table.d.ts +7 -0
  30. package/dist/plugins/table.d.ts.map +1 -0
  31. package/dist/styles.css +767 -386
  32. package/dist/types.d.ts +2 -2
  33. package/dist/types.d.ts.map +1 -1
  34. package/dist/utils/checkbox.d.ts +27 -0
  35. package/dist/utils/checkbox.d.ts.map +1 -0
  36. package/dist/utils/content.d.ts +19 -2
  37. package/dist/utils/content.d.ts.map +1 -1
  38. package/dist/utils/dom.d.ts +31 -0
  39. package/dist/utils/dom.d.ts.map +1 -0
  40. package/dist/utils/table.d.ts +30 -0
  41. package/dist/utils/table.d.ts.map +1 -0
  42. package/package.json +2 -3
  43. package/src/components/Dropdown.tsx +0 -103
  44. package/src/components/Editor.css +0 -2
  45. package/src/components/Editor.tsx +0 -828
  46. package/src/components/FloatingToolbar.tsx +0 -214
  47. package/src/components/IconWrapper.tsx +0 -14
  48. package/src/components/Icons.tsx +0 -374
  49. package/src/components/Toolbar.tsx +0 -137
  50. package/src/components/index.ts +0 -3
  51. package/src/index.ts +0 -19
  52. package/src/plugins/base.tsx +0 -91
  53. package/src/plugins/blockFormat.tsx +0 -194
  54. package/src/plugins/clearFormatting.tsx +0 -31
  55. package/src/plugins/colors.tsx +0 -122
  56. package/src/plugins/fontSize.tsx +0 -81
  57. package/src/plugins/headings.tsx +0 -87
  58. package/src/plugins/image.tsx +0 -189
  59. package/src/plugins/index.tsx +0 -161
  60. package/src/plugins/listIndent.tsx +0 -90
  61. package/src/plugins/optional.tsx +0 -243
  62. package/src/styles.css +0 -638
  63. package/src/types.ts +0 -95
  64. package/src/utils/clearFormatting.ts +0 -244
  65. package/src/utils/content.ts +0 -290
  66. package/src/utils/history.ts +0 -59
  67. package/src/utils/listIndent.ts +0 -171
  68. package/src/utils/stateReflection.ts +0 -175
package/dist/index.esm.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
- import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react';
2
+ import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
3
3
 
4
4
  const BoldIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M15.6 10.79c.97-.67 1.65-1.77 1.65-2.79 0-2.26-1.75-4-4-4H7v14h7.04c2.09 0 3.71-1.7 3.71-3.79 0-1.52-.86-2.82-2.15-3.42zM10 6.5h3c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-3v-3zm3.5 9H10v-3h3.5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5z" }) }));
5
5
  const ItalicIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M10 4v3h2.21l-3.42 8H6v3h8v-3h-2.21l3.42-8H18V4z" }) }));
@@ -12,8 +12,9 @@ const QuoteIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { wid
12
12
  const BulletListIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5zm0 12c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zM7 19h14v-2H7v2zm0-6h14v-2H7v2zm0-8v2h14V5H7z" }) }));
13
13
  const NumberedListIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M2 17h2v.5H3v1h1v.5H2v1h3v-4H2v1zm1-9h1V4H2v1h1v3zm-1 3h1.8L2 13.1v.9h3v-1H3.2L5 11.9V11H2zm6-5v2h14V6H8zm0 14h14v-2H8v2zm0-6h14v-2H8v2z" }) }));
14
14
  const TextColorIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M2 20h20v4H2v-4zm3.49-3h2.42l1.27-3.58h5.64L16.09 17h2.42L13.25 3h-2.5L5.49 17zm4.22-5.61l2.03-5.79h.12l2.03 5.79H9.71z" }) }));
15
- const BackgroundColorIcon = ({ width = 18, height = 18, className, }) => (jsxs("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: [jsx("path", { d: "M17.5 4.5c-1.95 0-4.05.4-5.5 1.5-1.45-1.1-3.55-1.5-5.5-1.5-1.45 0-2.99.22-4.28.79C1.49 5.62 1 6.33 1 7.14v11.28c0 1.3 1.22 2.26 2.48 1.94.98-.25 2.02-.36 3.02-.36 1.56 0 3.22.26 4.56.92.6.3 1.28.3 1.88 0 1.34-.67 3-.92 4.56-.92 1 0 2.04.11 3.02.36C22.78 20.68 24 19.72 24 18.42V7.14c0-.81-.49-1.52-1.22-1.85-1.29-.57-2.83-.79-4.28-.79zM21 17.23c0 .63-.58 1.09-1.2.98-.75-.14-1.53-.2-2.3-.2-1.7 0-4.15.65-5.5 1.5V8c1.35-.85 3.8-1.5 5.5-1.5.77 0 1.55.06 2.3.2.62.11 1.2.58 1.2 1.18v9.35z" }), jsx("rect", { x: "4", y: "13", width: "16", height: "6", fill: "currentColor", opacity: "0.5" })] }));
15
+ const BackgroundColorIcon = ({ width = 18, height = 18, className, }) => (jsxs("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: className, children: [jsx("path", { d: "M14.5 2.5L21.5 9.5L13 18L6 11L14.5 2.5Z" }), jsx("path", { d: "M6 11L13 18" }), jsx("rect", { x: "2", y: "16", width: "8", height: "6", fill: "currentColor", opacity: "0.6", rx: "1" }), jsx("path", { d: "M21.5 9.5L19 7" })] }));
16
16
  const HeadingIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M5 4v3h5.5v12h3V7H19V4H5z" }) }));
17
+ const FormatIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M3 4h18v2H3V4zm0 4h12v2H3V8zm0 4h18v2H3v-2zm0 4h12v2H3v-2z" }) }));
17
18
  const FontSizeIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M9 4v3h5v12h3V7h5V4H9zm-6 8h3v8h3v-8h3V10H3z" }) }));
18
19
  const ImageIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z" }) }));
19
20
  const CloseIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" }) }));
@@ -21,6 +22,16 @@ const LoadingIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { w
21
22
  const UploadIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M9 16h6v-6h4l-7-7-7 7h4zm-4 2h14v2H5z" }) }));
22
23
  const IndentIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M3 21h18v-2H3v2zM3 8l4 4-4 4V8zm8 9h10v-2H11v2zM3 3v2h18V3H3zm8 6h10V7H11v2zm0 4h10v-2H11v2z" }) }));
23
24
  const OutdentIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M3 21h18v-2H3v2zM11 8l4 4-4 4V8zM3 3v2h18V3H3zm0 4h10v2H3V7zm0 4h10v2H3v-2zm0 4h18v2H3v-2z" }) }));
25
+ const CheckboxIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M19 3H5c-1.11 0-2 .89-2 2v14c0 1.11.89 2 2 2h14c1.11 0 2-.89 2-2V5c0-1.11-.89-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" }) }));
26
+ const StrikethroughIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M10 19h4v-3h-4v3zM5 4v3h5v3h4V7h5V4H5zM3 14h18v-2H3v2z" }) }));
27
+ const SubscriptIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M22 18h-2v1h3v1h-4v-2.5c0-.83.67-1.5 1.5-1.5h1.5v-1h-3v-1h2.5c.83 0 1.5.67 1.5 1.5v1c0 .83-.67 1.5-1.5 1.5zM5.88 18h2.66l3.4-5.42h.12l3.4 5.42h2.66l-4.65-7.27L17.81 4h-2.68l-3.07 4.99h-.12L8.87 4H6.19l4.32 6.73L5.88 18z" }) }));
28
+ const SuperscriptIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M22 7h-2v1h3v1h-4V6.5c0-.83.67-1.5 1.5-1.5h1.5V4h-3V3h2.5c.83 0 1.5.67 1.5 1.5v1c0 .83-.67 1.5-1.5 1.5zM5.88 20h2.66l3.4-5.42h.12l3.4 5.42h2.66l-4.65-7.27L17.81 6h-2.68l-3.07 4.99h-.12L8.87 6H6.19l4.32 6.73L5.88 20z" }) }));
29
+ const CodeIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z" }) }));
30
+ const AlignLeftIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M15 15H3v2h12v-2zm0-8H3v2h12V7zM3 13h18v-2H3v2zm0 8h18v-2H3v2zM3 3v2h18V3H3z" }) }));
31
+ const AlignCenterIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M7 15v2h10v-2H7zm-4 6h18v-2H3v2zm0-8h18v-2H3v2zm4-6v2h10V7H7zM3 3v2h18V3H3z" }) }));
32
+ const AlignRightIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M3 21h18v-2H3v2zm6-4h12v-2H9v2zm-6-4h18v-2H3v2zm6-4h12V7H9v2zM3 3v2h18V3H3z" }) }));
33
+ const AlignJustifyIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M3 21h18v-2H3v2zm0-4h18v-2H3v2zm0-4h18v-2H3v2zm0-4h18V7H3v2zm0-6v2h18V3H3z" }) }));
34
+ const TableIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M3 3v18h18V3H3zm8 16H5v-6h6v6zm0-8H5V5h6v6zm8 8h-6v-6h6v6zm0-8h-6V5h6v6z" }) }));
24
35
  const iconMap = {
25
36
  "mdi:format-bold": BoldIcon,
26
37
  "mdi:format-italic": ItalicIcon,
@@ -35,6 +46,7 @@ const iconMap = {
35
46
  "mdi:format-color-text": TextColorIcon,
36
47
  "mdi:format-color-fill": BackgroundColorIcon,
37
48
  "mdi:format-header-1": HeadingIcon,
49
+ "mdi:format-paragraph": FormatIcon,
38
50
  "mdi:format-size": FontSizeIcon,
39
51
  "mdi:image": ImageIcon,
40
52
  "mdi:close": CloseIcon,
@@ -42,6 +54,16 @@ const iconMap = {
42
54
  "mdi:upload": UploadIcon,
43
55
  "mdi:format-indent-increase": IndentIcon,
44
56
  "mdi:format-indent-decrease": OutdentIcon,
57
+ "mdi:checkbox-marked-outline": CheckboxIcon,
58
+ "mdi:format-strikethrough": StrikethroughIcon,
59
+ "mdi:format-subscript": SubscriptIcon,
60
+ "mdi:format-superscript": SuperscriptIcon,
61
+ "mdi:code-tags": CodeIcon,
62
+ "mdi:format-align-left": AlignLeftIcon,
63
+ "mdi:format-align-center": AlignCenterIcon,
64
+ "mdi:format-align-right": AlignRightIcon,
65
+ "mdi:format-align-justify": AlignJustifyIcon,
66
+ "mdi:table": TableIcon,
45
67
  };
46
68
  const Icon = ({ icon, width = 18, height = 18, className }) => {
47
69
  const IconComponent = iconMap[icon];
@@ -135,7 +157,91 @@ function createCommandPlugin(name, command, icon, label) {
135
157
  };
136
158
  }
137
159
 
138
- const defaultHeadings$2 = ["h1", "h2", "h3"];
160
+ /**
161
+ * Pure DOM utility functions.
162
+ * No React dependencies - only native browser APIs.
163
+ */
164
+ /**
165
+ * Checks if a UL element is a checkbox list.
166
+ * Detects: own format, Lexical format, and GitHub format.
167
+ */
168
+ function isCheckboxList(element) {
169
+ if (element.tagName !== "UL")
170
+ return false;
171
+ // Own format
172
+ if (element.classList.contains("rte-checkbox-list"))
173
+ return true;
174
+ // Lexical format (attribute)
175
+ if (element.getAttribute("__lexicallisttype") === "check")
176
+ return true;
177
+ // Lexical theme class (any class containing "checklist")
178
+ const hasChecklistClass = Array.from(element.classList).some((cls) => cls.toLowerCase().includes("checklist"));
179
+ if (hasChecklistClass)
180
+ return true;
181
+ // GitHub format
182
+ if (element.classList.contains("contains-task-list"))
183
+ return true;
184
+ return false;
185
+ }
186
+ /**
187
+ * Finds the closest checkbox list ancestor from an element.
188
+ * Works with all supported formats (own, Lexical, GitHub).
189
+ */
190
+ function findClosestCheckboxList(element) {
191
+ let current = element;
192
+ while (current) {
193
+ if (current.tagName === "UL" && isCheckboxList(current)) {
194
+ return current;
195
+ }
196
+ current = current.parentElement;
197
+ }
198
+ return null;
199
+ }
200
+ /**
201
+ * Checks if a list item has a nested list as its first child (not a leaf item).
202
+ */
203
+ function isNestedListItem(li) {
204
+ const firstChild = li.firstChild;
205
+ return (firstChild instanceof HTMLElement &&
206
+ (firstChild.tagName === "UL" || firstChild.tagName === "OL"));
207
+ }
208
+ /**
209
+ * Finds the closest list item from a node.
210
+ */
211
+ function findClosestListItem(node) {
212
+ const element = node.nodeType === Node.TEXT_NODE
213
+ ? node.parentElement
214
+ : node;
215
+ return element?.closest("li") || null;
216
+ }
217
+ /**
218
+ * Sets cursor position in a text node after an async DOM update.
219
+ */
220
+ function setCursorInTextNode(textNode, position, editor) {
221
+ requestAnimationFrame(() => {
222
+ requestAnimationFrame(() => {
223
+ try {
224
+ const range = document.createRange();
225
+ const maxPos = textNode.textContent?.length || 0;
226
+ const safePos = Math.min(Math.max(0, position), maxPos);
227
+ range.setStart(textNode, safePos);
228
+ range.collapse(true);
229
+ const selection = window.getSelection();
230
+ if (selection) {
231
+ selection.removeAllRanges();
232
+ selection.addRange(range);
233
+ if (editor)
234
+ editor.focus();
235
+ }
236
+ }
237
+ catch (_) {
238
+ // Silently fail - cursor positioning is best-effort
239
+ }
240
+ });
241
+ });
242
+ }
243
+
244
+ const defaultHeadings$2 = ["h1", "h2", "h3", "h4", "h5", "h6"];
139
245
  const headingLabels$1 = {
140
246
  h1: "Überschrift 1",
141
247
  h2: "Überschrift 2",
@@ -145,8 +251,8 @@ const headingLabels$1 = {
145
251
  h6: "Überschrift 6",
146
252
  };
147
253
  /**
148
- * Erstellt ein Block-Format-Plugin, das Headlines, Listen und Quote in einem Dropdown kombiniert
149
- * @param headings - Array von Heading-Levels (z.B. ["h1", "h2", "h3"])
254
+ * Creates a Block Format plugin that combines headings, lists, and quote in a dropdown.
255
+ * @param headings - Array of heading levels (e.g. ["h1", "h2", "h3"])
150
256
  */
151
257
  function createBlockFormatPlugin(headings = defaultHeadings$2) {
152
258
  const options = [
@@ -166,16 +272,64 @@ function createBlockFormatPlugin(headings = defaultHeadings$2) {
166
272
  label: "Nummerierte Liste",
167
273
  icon: "mdi:format-list-numbered",
168
274
  },
169
- { value: "blockquote", label: "Zitat", icon: "mdi:format-quote-close" },
275
+ {
276
+ value: "checkbox-list",
277
+ label: "Checkbox-Liste",
278
+ icon: "mdi:checkbox-marked-outline",
279
+ },
280
+ {
281
+ value: "blockquote",
282
+ label: "Zitat",
283
+ icon: "mdi:format-quote-close",
284
+ },
170
285
  ];
286
+ /** Detects the current block format at the cursor position. */
287
+ function detectCurrentFormat(editor) {
288
+ const selection = editor.getSelection();
289
+ if (!selection || selection.rangeCount === 0)
290
+ return undefined;
291
+ const range = selection.getRangeAt(0);
292
+ const container = range.commonAncestorContainer;
293
+ const element = container.nodeType === Node.TEXT_NODE
294
+ ? container.parentElement
295
+ : container;
296
+ if (!element)
297
+ return undefined;
298
+ const tagName = element.tagName.toLowerCase();
299
+ if (headings.includes(tagName))
300
+ return tagName;
301
+ if (element.closest("blockquote"))
302
+ return "blockquote";
303
+ if (findClosestCheckboxList(element))
304
+ return "checkbox-list";
305
+ if (element.closest("ul"))
306
+ return "ul";
307
+ if (element.closest("ol"))
308
+ return "ol";
309
+ if (tagName === "p")
310
+ return "p";
311
+ return undefined;
312
+ }
171
313
  return {
172
314
  name: "blockFormat",
173
315
  type: "block",
174
316
  renderButton: (props) => {
175
- // Aktuelles Format bestimmen
176
317
  const editor = props.editorAPI;
318
+ const onSelect = props.onSelect;
177
319
  let currentValue = props.currentValue;
178
320
  if (!currentValue && editor) {
321
+ currentValue = detectCurrentFormat(editor);
322
+ }
323
+ return (jsx(Dropdown, { icon: "mdi:format-paragraph", label: "Format", options: options, onSelect: (value) => {
324
+ if (onSelect)
325
+ onSelect(value);
326
+ }, currentValue: currentValue, disabled: props.disabled }));
327
+ },
328
+ getCurrentValue: (editor) => detectCurrentFormat(editor),
329
+ execute: (editor, value) => {
330
+ if (!value)
331
+ return;
332
+ if (value === "checkbox-list") {
179
333
  const selection = editor.getSelection();
180
334
  if (selection && selection.rangeCount > 0) {
181
335
  const range = selection.getRangeAt(0);
@@ -183,74 +337,26 @@ function createBlockFormatPlugin(headings = defaultHeadings$2) {
183
337
  const element = container.nodeType === Node.TEXT_NODE
184
338
  ? container.parentElement
185
339
  : container;
186
- if (element) {
187
- const tagName = element.tagName.toLowerCase();
188
- // Prüfe auf Heading
189
- if (headings.includes(tagName)) {
190
- currentValue = tagName;
191
- }
192
- // Prüfe auf Blockquote
193
- else if (element.closest("blockquote")) {
194
- currentValue = "blockquote";
195
- }
196
- // Prüfe auf Liste
197
- else if (element.closest("ul")) {
198
- currentValue = "ul";
199
- }
200
- else if (element.closest("ol")) {
201
- currentValue = "ol";
202
- }
203
- // Prüfe auf Paragraph
204
- else if (tagName === "p") {
205
- currentValue = "p";
206
- }
340
+ if (!element)
341
+ return;
342
+ const checkboxList = findClosestCheckboxList(element);
343
+ if (checkboxList) {
344
+ // Remove checkbox list: convert to normal list
345
+ checkboxList.classList.remove("rte-checkbox-list");
346
+ checkboxList
347
+ .querySelectorAll("li[role='checkbox']")
348
+ .forEach((li) => {
349
+ li.removeAttribute("role");
350
+ li.removeAttribute("tabIndex");
351
+ li.removeAttribute("aria-checked");
352
+ });
207
353
  }
208
- }
209
- }
210
- return (jsx(Dropdown, { icon: "mdi:format-header-1", label: "Format", options: options, onSelect: (value) => {
211
- // onSelect wird von der Toolbar übergeben und ruft handlePluginClick auf
212
- if (props.onSelect) {
213
- props.onSelect(value);
354
+ else {
355
+ editor.executeCommand("insertCheckboxList");
214
356
  }
215
- }, currentValue: currentValue, disabled: props.disabled }));
216
- },
217
- getCurrentValue: (editor) => {
218
- const selection = editor.getSelection();
219
- if (!selection || selection.rangeCount === 0)
220
- return undefined;
221
- const range = selection.getRangeAt(0);
222
- const container = range.commonAncestorContainer;
223
- const element = container.nodeType === Node.TEXT_NODE
224
- ? container.parentElement
225
- : container;
226
- if (!element)
227
- return undefined;
228
- const tagName = element.tagName.toLowerCase();
229
- // Prüfe auf Heading
230
- if (headings.includes(tagName)) {
231
- return tagName;
232
- }
233
- // Prüfe auf Blockquote
234
- if (element.closest("blockquote")) {
235
- return "blockquote";
236
- }
237
- // Prüfe auf Liste
238
- if (element.closest("ul")) {
239
- return "ul";
240
- }
241
- if (element.closest("ol")) {
242
- return "ol";
243
- }
244
- // Prüfe auf Paragraph
245
- if (tagName === "p") {
246
- return "p";
357
+ }
247
358
  }
248
- return undefined;
249
- },
250
- execute: (editor, value) => {
251
- if (!value)
252
- return;
253
- if (value === "ul") {
359
+ else if (value === "ul") {
254
360
  editor.executeCommand("insertUnorderedList");
255
361
  }
256
362
  else if (value === "ol") {
@@ -290,6 +396,7 @@ function createBlockFormatPlugin(headings = defaultHeadings$2) {
290
396
  const tagName = element.tagName.toLowerCase();
291
397
  return (headings.includes(tagName) ||
292
398
  element.closest("blockquote") !== null ||
399
+ findClosestCheckboxList(element) !== null ||
293
400
  element.closest("ul") !== null ||
294
401
  element.closest("ol") !== null);
295
402
  },
@@ -313,13 +420,69 @@ const clearFormattingPlugin = {
313
420
  },
314
421
  };
315
422
 
316
- const defaultHeadings$1 = ["h1", "h2", "h3"];
423
+ const defaultHeadings$1 = ["h1", "h2", "h3", "h4", "h5", "h6"];
317
424
  /**
318
425
  * Standard-Plugins
319
426
  */
320
427
  const boldPlugin = createInlinePlugin("bold", "bold", "mdi:format-bold", "Fett");
321
428
  const italicPlugin = createInlinePlugin("italic", "italic", "mdi:format-italic", "Kursiv");
322
429
  const underlinePlugin = createInlinePlugin("underline", "underline", "mdi:format-underline", "Unterstrichen");
430
+ const strikethroughPlugin = createInlinePlugin("strikethrough", "strikeThrough", "mdi:format-strikethrough", "Durchgestrichen");
431
+ const subscriptPlugin = createInlinePlugin("subscript", "subscript", "mdi:format-subscript", "Tiefgestellt");
432
+ const superscriptPlugin = createInlinePlugin("superscript", "superscript", "mdi:format-superscript", "Hochgestellt");
433
+ const codeInlinePlugin = {
434
+ name: "codeInline",
435
+ type: "inline",
436
+ renderButton: (props) => (jsx("button", { type: "button", onClick: props.onClick, disabled: props.disabled, className: `rte-toolbar-button ${props.isActive ? "rte-toolbar-button-active" : ""}`, title: "Code", "aria-label": "Code", children: jsx(IconWrapper, { icon: "mdi:code-tags", width: 18, height: 18 }) })),
437
+ execute: (editor) => {
438
+ const selection = editor.getSelection();
439
+ if (!selection || selection.rangeCount === 0)
440
+ return;
441
+ const range = selection.getRangeAt(0);
442
+ const container = range.commonAncestorContainer;
443
+ const element = container.nodeType === Node.TEXT_NODE
444
+ ? container.parentElement
445
+ : container;
446
+ const existingCode = element?.closest("code");
447
+ if (existingCode) {
448
+ // Unwrap code
449
+ const parent = existingCode.parentNode;
450
+ if (parent) {
451
+ while (existingCode.firstChild) {
452
+ parent.insertBefore(existingCode.firstChild, existingCode);
453
+ }
454
+ parent.removeChild(existingCode);
455
+ }
456
+ }
457
+ else if (!range.collapsed) {
458
+ // Wrap in code
459
+ const code = document.createElement("code");
460
+ try {
461
+ range.surroundContents(code);
462
+ }
463
+ catch {
464
+ // If surroundContents fails (partial selection), use extractContents
465
+ const fragment = range.extractContents();
466
+ code.appendChild(fragment);
467
+ range.insertNode(code);
468
+ }
469
+ }
470
+ },
471
+ isActive: () => {
472
+ if (typeof document === "undefined")
473
+ return false;
474
+ const selection = document.getSelection();
475
+ if (!selection || selection.rangeCount === 0)
476
+ return false;
477
+ const range = selection.getRangeAt(0);
478
+ const container = range.commonAncestorContainer;
479
+ const element = container.nodeType === Node.TEXT_NODE
480
+ ? container.parentElement
481
+ : container;
482
+ return element?.closest("code") !== null;
483
+ },
484
+ canExecute: () => true,
485
+ };
323
486
  const undoPlugin = createCommandPlugin("undo", "undo", "mdi:undo", "Rückgängig");
324
487
  const redoPlugin = createCommandPlugin("redo", "redo", "mdi:redo", "Wiederholen");
325
488
  /**
@@ -387,10 +550,10 @@ const defaultBlockFormatPlugin = createBlockFormatPlugin(defaultHeadings$1);
387
550
  const defaultPlugins = [
388
551
  undoPlugin,
389
552
  redoPlugin,
553
+ defaultBlockFormatPlugin,
390
554
  boldPlugin,
391
555
  italicPlugin,
392
556
  underlinePlugin,
393
- defaultBlockFormatPlugin,
394
557
  clearFormattingPlugin,
395
558
  _indentPlugin,
396
559
  _outdentPlugin,
@@ -779,6 +942,138 @@ function createImagePlugin(onImageUpload) {
779
942
  };
780
943
  }
781
944
 
945
+ /**
946
+ * Pure checkbox utility functions for checkbox lists.
947
+ * Supports detection of own format, Lexical format, and GitHub format.
948
+ * No React dependencies.
949
+ */
950
+ /**
951
+ * Updates a list item's checkbox attributes (role, aria-checked, tabindex).
952
+ */
953
+ function updateListItemChecked(li, checked) {
954
+ const parent = li.parentElement;
955
+ if (!parent ||
956
+ parent.tagName !== "UL" ||
957
+ !isCheckboxList(parent) ||
958
+ isNestedListItem(li)) {
959
+ li.removeAttribute("role");
960
+ li.removeAttribute("tabIndex");
961
+ li.removeAttribute("aria-checked");
962
+ return;
963
+ }
964
+ li.setAttribute("role", "checkbox");
965
+ li.setAttribute("tabIndex", "-1");
966
+ li.setAttribute("aria-checked", checked ? "true" : "false");
967
+ }
968
+ /**
969
+ * Toggles the checked state of a checkbox list item.
970
+ */
971
+ function toggleListItemChecked(li) {
972
+ const currentChecked = li.getAttribute("aria-checked") === "true";
973
+ li.setAttribute("aria-checked", currentChecked ? "false" : "true");
974
+ if (li.getAttribute("role") !== "checkbox") {
975
+ li.setAttribute("role", "checkbox");
976
+ li.setAttribute("tabIndex", "-1");
977
+ }
978
+ }
979
+ /**
980
+ * Ensures all checkbox list items in the editor have correct attributes.
981
+ * Normalizes foreign formats (Lexical, GitHub) to internal format.
982
+ */
983
+ function ensureAllCheckboxes(editor) {
984
+ const allULs = editor.querySelectorAll("ul");
985
+ allULs.forEach((ul) => {
986
+ if (!isCheckboxList(ul))
987
+ return;
988
+ // Normalize: ensure our class is always present
989
+ if (!ul.classList.contains("rte-checkbox-list")) {
990
+ ul.classList.add("rte-checkbox-list");
991
+ }
992
+ // Handle GitHub format: convert <input type="checkbox"> to aria-checked
993
+ ul.querySelectorAll(":scope > li").forEach((li) => {
994
+ const input = li.querySelector('input[type="checkbox"]');
995
+ if (input) {
996
+ const checked = input.checked || input.hasAttribute("checked");
997
+ if (li.getAttribute("aria-checked") === null) {
998
+ li.setAttribute("aria-checked", checked ? "true" : "false");
999
+ }
1000
+ input.remove();
1001
+ }
1002
+ });
1003
+ const listItems = ul.querySelectorAll(":scope > li");
1004
+ listItems.forEach((li) => {
1005
+ const htmlLi = li;
1006
+ if (!isNestedListItem(htmlLi)) {
1007
+ const ariaChecked = htmlLi.getAttribute("aria-checked");
1008
+ const checked = ariaChecked === "true";
1009
+ updateListItemChecked(htmlLi, checked);
1010
+ }
1011
+ else {
1012
+ htmlLi.removeAttribute("role");
1013
+ htmlLi.removeAttribute("tabIndex");
1014
+ htmlLi.removeAttribute("aria-checked");
1015
+ }
1016
+ });
1017
+ });
1018
+ }
1019
+ /**
1020
+ * Returns the currently focused checkbox list item, or null.
1021
+ */
1022
+ function getActiveCheckListItem() {
1023
+ const activeElement = document.activeElement;
1024
+ if (activeElement &&
1025
+ activeElement.tagName === "LI" &&
1026
+ activeElement.parentElement &&
1027
+ isCheckboxList(activeElement.parentElement)) {
1028
+ return activeElement;
1029
+ }
1030
+ return null;
1031
+ }
1032
+ /**
1033
+ * Finds the next or previous checkbox list item sibling.
1034
+ */
1035
+ function findCheckListItemSibling(li, backward) {
1036
+ let sibling = backward
1037
+ ? li.previousElementSibling
1038
+ : li.nextElementSibling;
1039
+ let parent = li;
1040
+ // Walk up the tree to find a non-null sibling
1041
+ while (sibling == null && parent) {
1042
+ const parentList = parent.parentElement;
1043
+ if (parentList && parentList.tagName === "UL") {
1044
+ const grandParent = parentList.parentElement;
1045
+ if (grandParent && grandParent.tagName === "LI") {
1046
+ parent = grandParent;
1047
+ sibling = backward
1048
+ ? parent.previousElementSibling
1049
+ : parent.nextElementSibling;
1050
+ }
1051
+ else {
1052
+ break;
1053
+ }
1054
+ }
1055
+ else {
1056
+ break;
1057
+ }
1058
+ }
1059
+ // Walk down the tree to find the first non-nested list item
1060
+ while (sibling && sibling.tagName === "LI") {
1061
+ const child = backward
1062
+ ? sibling.lastElementChild
1063
+ : sibling.firstElementChild;
1064
+ if (child &&
1065
+ (child.tagName === "UL" || child.tagName === "OL")) {
1066
+ sibling = backward
1067
+ ? child.lastElementChild
1068
+ : child.firstElementChild;
1069
+ }
1070
+ else {
1071
+ return sibling;
1072
+ }
1073
+ }
1074
+ return null;
1075
+ }
1076
+
782
1077
  /**
783
1078
  * Entfernt alle Formatierungen aus der aktuellen Selection
784
1079
  */
@@ -972,143 +1267,301 @@ function clearLinks(selection) {
972
1267
  });
973
1268
  }
974
1269
 
1270
+ /**
1271
+ * Converts a DOM element (editor root) to EditorContent JSON.
1272
+ * Supports own format, Lexical HTML, and GitHub HTML.
1273
+ */
975
1274
  function domToContent(element) {
976
1275
  const blocks = [];
1276
+ // Normalize all checkbox lists before conversion
1277
+ ensureAllCheckboxes(element);
977
1278
  function processNode(node) {
978
1279
  if (node.nodeType === Node.TEXT_NODE) {
979
1280
  const text = node.textContent;
980
1281
  if (text === null || text === undefined)
981
1282
  return null;
1283
+ // In checkbox lists, skip whitespace-only text nodes
1284
+ // if the LI has other meaningful text
1285
+ const isOnlyWhitespace = /^[\s\u200B]*$/.test(text);
1286
+ const isInCheckboxList = node.parentElement?.closest("ul.rte-checkbox-list") !== null;
1287
+ if (isOnlyWhitespace && isInCheckboxList) {
1288
+ const li = node.parentElement?.closest("li");
1289
+ if (li) {
1290
+ const allText = li.textContent || "";
1291
+ const textWithoutWhitespace = allText.replace(/[\s\u200B]/g, "");
1292
+ if (textWithoutWhitespace.length > 0) {
1293
+ return null;
1294
+ }
1295
+ }
1296
+ }
982
1297
  return { type: "text", text };
983
1298
  }
984
- if (node.nodeType === Node.ELEMENT_NODE) {
985
- const el = node;
986
- const tagName = el.tagName.toLowerCase();
987
- if (tagName === "img") {
988
- const attributes = {};
989
- const src = el.getAttribute("src");
990
- const alt = el.getAttribute("alt");
991
- if (src)
992
- attributes.src = src;
993
- if (alt)
994
- attributes.alt = alt;
995
- return {
996
- type: "image",
997
- attributes: Object.keys(attributes).length > 0
998
- ? attributes
999
- : undefined,
1000
- };
1001
- }
1002
- if ([
1003
- "p",
1004
- "div",
1005
- "h1",
1006
- "h2",
1007
- "h3",
1008
- "h4",
1009
- "h5",
1010
- "h6",
1011
- "blockquote",
1012
- "ul",
1013
- "ol",
1014
- "li",
1015
- ].includes(tagName)) {
1016
- const children = [];
1017
- Array.from(el.childNodes).forEach((child) => {
1018
- const processed = processNode(child);
1019
- if (processed)
1020
- children.push(processed);
1021
- });
1022
- const attributes = {};
1023
- if (tagName === "a") {
1024
- const href = el.getAttribute("href");
1025
- if (href)
1026
- attributes.href = href;
1299
+ if (node.nodeType !== Node.ELEMENT_NODE) {
1300
+ return null;
1301
+ }
1302
+ const el = node;
1303
+ const tagName = el.tagName.toLowerCase();
1304
+ // Skip checkbox input elements (GitHub format remnants)
1305
+ if (tagName === "input" && el.getAttribute("type") === "checkbox") {
1306
+ return null;
1307
+ }
1308
+ // Handle <br> as empty text
1309
+ if (tagName === "br") {
1310
+ return null;
1311
+ }
1312
+ if (tagName === "img") {
1313
+ const attributes = {};
1314
+ const src = el.getAttribute("src");
1315
+ const alt = el.getAttribute("alt");
1316
+ const attachmentId = el.getAttribute("data-attachment-id");
1317
+ if (src)
1318
+ attributes.src = src;
1319
+ if (alt)
1320
+ attributes.alt = alt;
1321
+ if (attachmentId)
1322
+ attributes["data-attachment-id"] = attachmentId;
1323
+ return {
1324
+ type: "image",
1325
+ attributes: Object.keys(attributes).length > 0
1326
+ ? attributes
1327
+ : undefined,
1328
+ };
1329
+ }
1330
+ // Table elements
1331
+ if (["table", "thead", "tbody", "tr", "td", "th"].includes(tagName)) {
1332
+ const children = [];
1333
+ const attributes = {};
1334
+ // Preserve class
1335
+ const cls = el.getAttribute("class");
1336
+ if (cls)
1337
+ attributes.class = cls;
1338
+ // Table cell attributes
1339
+ if (tagName === "td" || tagName === "th") {
1340
+ const colspan = el.getAttribute("colspan");
1341
+ const rowspan = el.getAttribute("rowspan");
1342
+ if (colspan && colspan !== "1")
1343
+ attributes.colspan = colspan;
1344
+ if (rowspan && rowspan !== "1")
1345
+ attributes.rowspan = rowspan;
1346
+ // Preserve text-align on cells
1347
+ const textAlign = el.style.textAlign;
1348
+ if (textAlign && textAlign !== "left" && textAlign !== "start") {
1349
+ attributes.textAlign = textAlign;
1027
1350
  }
1028
- return {
1029
- type: tagName,
1030
- children: children.length > 0 ? children : [],
1031
- attributes: Object.keys(attributes).length > 0
1032
- ? attributes
1033
- : undefined,
1034
- };
1351
+ // Preserve background-color
1352
+ const bgColor = el.style.backgroundColor;
1353
+ if (bgColor)
1354
+ attributes.backgroundColor = bgColor;
1035
1355
  }
1036
- if (["strong", "b", "em", "i", "u", "a", "span"].includes(tagName)) {
1037
- const children = [];
1038
- Array.from(el.childNodes).forEach((child) => {
1039
- const processed = processNode(child);
1040
- if (processed)
1041
- children.push(processed);
1042
- });
1043
- const attributes = {};
1044
- Array.from(el.attributes).forEach((attr) => {
1045
- attributes[attr.name] = attr.value;
1046
- });
1047
- if (tagName === "a") {
1048
- const href = el.getAttribute("href");
1049
- if (href)
1050
- attributes.href = href;
1051
- return {
1052
- type: "link",
1053
- children: children.length > 0 ? children : undefined,
1054
- attributes: Object.keys(attributes).length > 0
1055
- ? attributes
1056
- : undefined,
1057
- };
1058
- }
1059
- if (tagName === "span") {
1060
- const style = el.getAttribute("style");
1061
- if (style) {
1062
- style.split(";").forEach((rule) => {
1063
- const [key, value] = rule
1064
- .split(":")
1065
- .map((s) => s.trim());
1066
- if (key && value) {
1067
- if (key === "font-size") {
1068
- attributes.fontSize = value;
1069
- }
1070
- else if (key === "color") {
1071
- attributes.color = value;
1072
- }
1073
- else if (key === "background-color") {
1074
- attributes.backgroundColor = value;
1075
- }
1076
- }
1077
- });
1356
+ Array.from(el.childNodes).forEach((child) => {
1357
+ const processed = processNode(child);
1358
+ if (processed)
1359
+ children.push(processed);
1360
+ });
1361
+ return {
1362
+ type: tagName,
1363
+ children: children.length > 0 ? children : [],
1364
+ attributes: Object.keys(attributes).length > 0
1365
+ ? attributes
1366
+ : undefined,
1367
+ };
1368
+ }
1369
+ // Block elements
1370
+ if ([
1371
+ "p",
1372
+ "div",
1373
+ "h1",
1374
+ "h2",
1375
+ "h3",
1376
+ "h4",
1377
+ "h5",
1378
+ "h6",
1379
+ "blockquote",
1380
+ "ul",
1381
+ "ol",
1382
+ "li",
1383
+ ].includes(tagName)) {
1384
+ const children = [];
1385
+ const attributes = {};
1386
+ // Preserve text-align on block elements
1387
+ const textAlign = el.style.textAlign;
1388
+ if (textAlign && textAlign !== "left" && textAlign !== "start" && textAlign !== "") {
1389
+ attributes.textAlign = textAlign;
1390
+ }
1391
+ // Detect checkbox lists (own + Lexical + GitHub formats)
1392
+ if (tagName === "ul" && isCheckboxList(el)) {
1393
+ attributes.class = "rte-checkbox-list";
1394
+ }
1395
+ // Detect checkbox list items via aria-checked
1396
+ if (tagName === "li") {
1397
+ const parentUl = el.closest("ul");
1398
+ if (parentUl &&
1399
+ isCheckboxList(parentUl) &&
1400
+ el.getAttribute("role") === "checkbox") {
1401
+ const ariaChecked = el.getAttribute("aria-checked");
1402
+ if (ariaChecked !== null) {
1403
+ attributes.checkboxChecked = ariaChecked;
1078
1404
  }
1079
1405
  }
1080
- return {
1081
- type: tagName === "strong" || tagName === "b"
1082
- ? "bold"
1083
- : tagName === "em" || tagName === "i"
1084
- ? "italic"
1085
- : tagName === "u"
1086
- ? "underline"
1087
- : tagName,
1088
- children: children.length > 0 ? children : undefined,
1089
- attributes: Object.keys(attributes).length > 0
1090
- ? attributes
1091
- : undefined,
1092
- };
1093
1406
  }
1407
+ // Process children recursively
1408
+ Array.from(el.childNodes).forEach((child) => {
1409
+ // Skip checkbox input nodes
1410
+ if (child.nodeType === Node.ELEMENT_NODE &&
1411
+ child.tagName.toLowerCase() === "input" &&
1412
+ child.getAttribute("type") === "checkbox") {
1413
+ return;
1414
+ }
1415
+ const processed = processNode(child);
1416
+ if (processed) {
1417
+ children.push(processed);
1418
+ }
1419
+ });
1420
+ return {
1421
+ type: tagName,
1422
+ children: children.length > 0 ? children : [],
1423
+ attributes: Object.keys(attributes).length > 0
1424
+ ? attributes
1425
+ : undefined,
1426
+ };
1427
+ }
1428
+ // Inline elements
1429
+ if ([
1430
+ "strong",
1431
+ "b",
1432
+ "em",
1433
+ "i",
1434
+ "u",
1435
+ "s",
1436
+ "del",
1437
+ "strike",
1438
+ "a",
1439
+ "span",
1440
+ "sub",
1441
+ "sup",
1442
+ "code",
1443
+ ].includes(tagName)) {
1094
1444
  const children = [];
1095
1445
  Array.from(el.childNodes).forEach((child) => {
1096
1446
  const processed = processNode(child);
1097
1447
  if (processed)
1098
1448
  children.push(processed);
1099
1449
  });
1100
- if (children.length > 0) {
1450
+ const attributes = {};
1451
+ Array.from(el.attributes).forEach((attr) => {
1452
+ attributes[attr.name] = attr.value;
1453
+ });
1454
+ // Links — capture all relevant attributes
1455
+ if (tagName === "a") {
1456
+ const linkAttrs = {};
1457
+ const href = el.getAttribute("href");
1458
+ if (href)
1459
+ linkAttrs.href = href;
1460
+ const target = el.getAttribute("target");
1461
+ if (target)
1462
+ linkAttrs.target = target;
1463
+ const rel = el.getAttribute("rel");
1464
+ if (rel)
1465
+ linkAttrs.rel = rel;
1466
+ const title = el.getAttribute("title");
1467
+ if (title)
1468
+ linkAttrs.title = title;
1469
+ const pageRef = el.getAttribute("data-page-ref");
1470
+ if (pageRef)
1471
+ linkAttrs["data-page-ref"] = pageRef;
1472
+ const urlExtra = el.getAttribute("data-url-extra");
1473
+ if (urlExtra)
1474
+ linkAttrs["data-url-extra"] = urlExtra;
1101
1475
  return {
1102
- type: tagName,
1103
- children,
1476
+ type: "link",
1477
+ children: children.length > 0 ? children : undefined,
1478
+ attributes: Object.keys(linkAttrs).length > 0
1479
+ ? linkAttrs
1480
+ : undefined,
1104
1481
  };
1105
1482
  }
1483
+ // Spans: handle Lexical's white-space: pre-wrap wrapper
1484
+ if (tagName === "span") {
1485
+ const style = el.getAttribute("style") || "";
1486
+ const hasSemanticStyle = style.includes("font-size") ||
1487
+ style.includes("color") ||
1488
+ style.includes("background-color");
1489
+ // Transparent wrapper (e.g. Lexical's <span style="white-space: pre-wrap;">)
1490
+ // Return text content directly
1491
+ if (!hasSemanticStyle) {
1492
+ if (el.childNodes.length === 1 &&
1493
+ el.firstChild?.nodeType === Node.TEXT_NODE) {
1494
+ return {
1495
+ type: "text",
1496
+ text: el.firstChild.textContent || "",
1497
+ };
1498
+ }
1499
+ // Multiple children: flatten (return each child as-is)
1500
+ if (children.length === 1) {
1501
+ return children[0];
1502
+ }
1503
+ // If multiple children, keep as span but without extra attributes
1504
+ if (children.length > 1) {
1505
+ return {
1506
+ type: "span",
1507
+ children,
1508
+ };
1509
+ }
1510
+ return null;
1511
+ }
1512
+ // Semantic span: extract meaningful style properties
1513
+ if (style) {
1514
+ style.split(";").forEach((rule) => {
1515
+ const [key, value] = rule
1516
+ .split(":")
1517
+ .map((s) => s.trim());
1518
+ if (key && value) {
1519
+ if (key === "font-size") {
1520
+ attributes.fontSize = value;
1521
+ }
1522
+ else if (key === "color") {
1523
+ attributes.color = value;
1524
+ }
1525
+ else if (key === "background-color") {
1526
+ attributes.backgroundColor = value;
1527
+ }
1528
+ }
1529
+ });
1530
+ }
1531
+ }
1532
+ // Map tag names to semantic types
1533
+ const type = tagName === "strong" || tagName === "b"
1534
+ ? "bold"
1535
+ : tagName === "em" || tagName === "i"
1536
+ ? "italic"
1537
+ : tagName === "u"
1538
+ ? "underline"
1539
+ : tagName === "s" ||
1540
+ tagName === "del" ||
1541
+ tagName === "strike"
1542
+ ? "strikethrough"
1543
+ : tagName === "sub"
1544
+ ? "subscript"
1545
+ : tagName === "sup"
1546
+ ? "superscript"
1547
+ : tagName === "code"
1548
+ ? "code"
1549
+ : tagName;
1550
+ return {
1551
+ type,
1552
+ children: children.length > 0 ? children : undefined,
1553
+ attributes: Object.keys(attributes).length > 0
1554
+ ? attributes
1555
+ : undefined,
1556
+ };
1106
1557
  }
1107
1558
  return null;
1108
1559
  }
1560
+ // Process all child nodes of the editor element
1109
1561
  Array.from(element.childNodes).forEach((node) => {
1110
1562
  const processed = processNode(node);
1111
1563
  if (processed) {
1564
+ // Wrap bare text nodes in a paragraph
1112
1565
  if (processed.type === "text") {
1113
1566
  blocks.push({ type: "p", children: [processed] });
1114
1567
  }
@@ -1122,6 +1575,9 @@ function domToContent(element) {
1122
1575
  }
1123
1576
  return { blocks };
1124
1577
  }
1578
+ /**
1579
+ * Converts EditorContent JSON to DOM and appends to the container.
1580
+ */
1125
1581
  function contentToDOM(content, container, customLinkComponent, customHeadingRenderer) {
1126
1582
  container.innerHTML = "";
1127
1583
  function createNode(node) {
@@ -1149,7 +1605,11 @@ function contentToDOM(content, container, customLinkComponent, customHeadingRend
1149
1605
  bold: "strong",
1150
1606
  italic: "em",
1151
1607
  underline: "u",
1608
+ strikethrough: "s",
1152
1609
  link: "a",
1610
+ subscript: "sub",
1611
+ superscript: "sup",
1612
+ code: "code",
1153
1613
  };
1154
1614
  let tagName = tagMap[node.type] || node.type;
1155
1615
  if (node.type === "link" && customLinkComponent) {
@@ -1161,53 +1621,103 @@ function contentToDOM(content, container, customLinkComponent, customHeadingRend
1161
1621
  const element = document.createElement(tagName);
1162
1622
  if (node.attributes) {
1163
1623
  Object.entries(node.attributes).forEach(([key, value]) => {
1164
- if (key === "fontSize" ||
1165
- key === "color" ||
1166
- key === "backgroundColor") {
1167
- const currentStyle = element.getAttribute("style") || "";
1168
- if (key === "fontSize") {
1169
- element.setAttribute("style", `${currentStyle}font-size: ${value};`.trim());
1170
- }
1171
- else if (key === "color") {
1172
- element.setAttribute("style", `${currentStyle}color: ${value};`.trim());
1173
- }
1174
- else if (key === "backgroundColor") {
1175
- element.setAttribute("style", `${currentStyle}background-color: ${value};`.trim());
1176
- }
1624
+ if (key === "fontSize") {
1625
+ element.style.fontSize = value;
1177
1626
  }
1627
+ else if (key === "color") {
1628
+ element.style.color = value;
1629
+ }
1630
+ else if (key === "backgroundColor") {
1631
+ element.style.backgroundColor = value;
1632
+ }
1633
+ else if (key === "textAlign") {
1634
+ element.style.textAlign = value;
1635
+ }
1636
+ else if (key === "href" && tagName === "a") {
1637
+ element.setAttribute("href", value);
1638
+ }
1639
+ else if (key === "target" && tagName === "a") {
1640
+ element.setAttribute("target", value);
1641
+ }
1642
+ else if (key === "rel" && tagName === "a") {
1643
+ element.setAttribute("rel", value);
1644
+ }
1645
+ else if (key === "title" && tagName === "a") {
1646
+ element.setAttribute("title", value);
1647
+ }
1648
+ else if (key === "data-page-ref" && tagName === "a") {
1649
+ element.setAttribute("data-page-ref", value);
1650
+ }
1651
+ else if (key === "data-url-extra" && tagName === "a") {
1652
+ element.setAttribute("data-url-extra", value);
1653
+ }
1654
+ else if (key === "data-attachment-id") {
1655
+ element.setAttribute("data-attachment-id", value);
1656
+ }
1657
+ else if (key === "colspan") {
1658
+ element.setAttribute("colspan", value);
1659
+ }
1660
+ else if (key === "rowspan") {
1661
+ element.setAttribute("rowspan", value);
1662
+ }
1663
+ else if (key === "class") {
1664
+ element.className = value;
1665
+ }
1666
+ else if (key === "checkboxChecked") ;
1178
1667
  else {
1179
1668
  element.setAttribute(key, value);
1180
1669
  }
1181
1670
  });
1182
1671
  }
1672
+ // Ensure tables have the rte-table class
1673
+ if (node.type === "table" && !element.classList.contains("rte-table")) {
1674
+ element.classList.add("rte-table");
1675
+ }
1183
1676
  if (node.children) {
1184
1677
  node.children.forEach((child) => {
1185
- const childNode = createNode(child);
1186
- element.appendChild(childNode);
1678
+ element.appendChild(createNode(child));
1187
1679
  });
1188
1680
  }
1681
+ // Set checkbox attributes on list items
1682
+ if (node.type === "li" &&
1683
+ node.attributes?.checkboxChecked !== undefined) {
1684
+ element.setAttribute("role", "checkbox");
1685
+ element.setAttribute("tabIndex", "-1");
1686
+ element.setAttribute("aria-checked", node.attributes.checkboxChecked);
1687
+ }
1189
1688
  return element;
1190
1689
  }
1191
1690
  content.blocks.forEach((block) => {
1192
- const blockNode = createNode(block);
1193
- container.appendChild(blockNode);
1691
+ container.appendChild(createNode(block));
1194
1692
  });
1693
+ // Normalize all checkbox lists after DOM creation
1694
+ ensureAllCheckboxes(container);
1195
1695
  }
1196
- function createEmptyContent() {
1696
+ /**
1697
+ * Creates empty editor content with a single paragraph.
1698
+ */
1699
+ function createEmptyContent() {
1197
1700
  return {
1198
1701
  blocks: [{ type: "p", children: [] }],
1199
1702
  };
1200
1703
  }
1201
- function htmlToContent(htmlString) {
1202
- const tempDiv = document.createElement("div");
1203
- tempDiv.innerHTML = htmlString.trim();
1204
- return domToContent(tempDiv);
1205
- }
1704
+ /**
1705
+ * Converts EditorContent to an HTML string.
1706
+ */
1206
1707
  function contentToHTML(content) {
1207
1708
  const tempDiv = document.createElement("div");
1208
1709
  contentToDOM(content, tempDiv);
1209
1710
  return tempDiv.innerHTML;
1210
1711
  }
1712
+ /**
1713
+ * Converts an HTML string to EditorContent.
1714
+ * Supports Lexical, GitHub, and standard HTML formats.
1715
+ */
1716
+ function htmlToContent(htmlString) {
1717
+ const tempDiv = document.createElement("div");
1718
+ tempDiv.innerHTML = htmlString;
1719
+ return domToContent(tempDiv);
1720
+ }
1211
1721
 
1212
1722
  class HistoryManager {
1213
1723
  constructor() {
@@ -1417,321 +1927,974 @@ function outdentListItem(selection) {
1417
1927
  return true;
1418
1928
  }
1419
1929
 
1420
- const Toolbar = ({ plugins, editorAPI, className, }) => {
1421
- const [updateTrigger, setUpdateTrigger] = useState(0);
1422
- const [isClient, setIsClient] = useState(false);
1930
+ /** Debounce time for pushing to history after input (ms) */
1931
+ const HISTORY_DEBOUNCE_MS = 300;
1932
+ /** Width of the clickable checkbox area in pixels */
1933
+ const CHECKBOX_CLICK_ZONE_PX = 40;
1934
+
1935
+ /**
1936
+ * Hook that manages all checkbox list interactions.
1937
+ * Consolidates click handling, keyboard navigation, and checkbox insertion.
1938
+ * Uses event delegation (single listener on editor root) for all checkbox events.
1939
+ */
1940
+ function useCheckbox({ editorRef, isUpdatingRef, pushToHistory, notifyChange, getDomContent, }) {
1941
+ // --- Event Listeners (click + pointerdown) ---
1423
1942
  useEffect(() => {
1424
- setIsClient(true);
1425
- const handleSelectionChange = () => {
1426
- setUpdateTrigger((prev) => prev + 1);
1427
- };
1428
- const handleMouseUp = () => {
1429
- setTimeout(handleSelectionChange, 10);
1943
+ const editor = editorRef.current;
1944
+ if (!editor)
1945
+ return;
1946
+ const handleClick = (event) => {
1947
+ const clickTarget = event.target;
1948
+ if (!(clickTarget instanceof HTMLElement))
1949
+ return;
1950
+ const listItem = clickTarget.tagName === "LI"
1951
+ ? clickTarget
1952
+ : clickTarget.closest("li");
1953
+ if (!listItem)
1954
+ return;
1955
+ const parentNode = listItem.parentElement;
1956
+ if (!parentNode || !isCheckboxList(parentNode))
1957
+ return;
1958
+ if (listItem.getAttribute("role") !== "checkbox")
1959
+ return;
1960
+ if (isNestedListItem(listItem))
1961
+ return;
1962
+ const rect = listItem.getBoundingClientRect();
1963
+ const clientX = event.clientX;
1964
+ const isInCheckboxArea = listItem.dir === "rtl"
1965
+ ? clientX >= rect.right - CHECKBOX_CLICK_ZONE_PX
1966
+ : clientX <= rect.left + CHECKBOX_CLICK_ZONE_PX;
1967
+ if (isInCheckboxArea) {
1968
+ event.preventDefault();
1969
+ event.stopPropagation();
1970
+ toggleListItemChecked(listItem);
1971
+ // Trigger change event
1972
+ const changeEvent = new Event("input", { bubbles: true });
1973
+ editor.dispatchEvent(changeEvent);
1974
+ }
1430
1975
  };
1431
- const handleKeyUp = () => {
1432
- setTimeout(handleSelectionChange, 10);
1976
+ const handlePointerDown = (event) => {
1977
+ const target = event.target;
1978
+ if (!(target instanceof HTMLElement))
1979
+ return;
1980
+ const listItem = target.tagName === "LI"
1981
+ ? target
1982
+ : target.closest("li");
1983
+ if (!listItem)
1984
+ return;
1985
+ if (isNestedListItem(listItem))
1986
+ return;
1987
+ const parentNode = listItem.parentElement;
1988
+ if (!parentNode || !isCheckboxList(parentNode))
1989
+ return;
1990
+ const rect = listItem.getBoundingClientRect();
1991
+ const clientX = event.clientX;
1992
+ const isInCheckboxArea = listItem.dir === "rtl"
1993
+ ? clientX >= rect.right - CHECKBOX_CLICK_ZONE_PX
1994
+ : clientX <= rect.left + CHECKBOX_CLICK_ZONE_PX;
1995
+ if (isInCheckboxArea) {
1996
+ event.preventDefault();
1997
+ }
1433
1998
  };
1434
- if (typeof document !== 'undefined') {
1435
- document.addEventListener("selectionchange", handleSelectionChange);
1436
- document.addEventListener("mouseup", handleMouseUp);
1437
- document.addEventListener("keyup", handleKeyUp);
1438
- }
1999
+ editor.addEventListener("click", handleClick, true);
2000
+ editor.addEventListener("pointerdown", handlePointerDown, true);
1439
2001
  return () => {
1440
- if (typeof document !== 'undefined') {
1441
- document.removeEventListener("selectionchange", handleSelectionChange);
1442
- document.removeEventListener("mouseup", handleMouseUp);
1443
- document.removeEventListener("keyup", handleKeyUp);
1444
- }
2002
+ editor.removeEventListener("click", handleClick, true);
2003
+ editor.removeEventListener("pointerdown", handlePointerDown, true);
1445
2004
  };
1446
- }, []);
1447
- const handlePluginClick = (plugin, value) => {
1448
- if (plugin.canExecute?.(editorAPI) !== false) {
1449
- if (plugin.execute) {
1450
- plugin.execute(editorAPI, value);
2005
+ }, [editorRef]);
2006
+ // --- Keyboard handlers (called from useEditorEvents) ---
2007
+ /**
2008
+ * Handles checkbox-specific keyboard events.
2009
+ * Returns true if the event was handled and should not propagate.
2010
+ */
2011
+ const handleCheckboxKeyDown = useCallback((e) => {
2012
+ // Arrow up/down: navigate between checkbox items
2013
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
2014
+ const activeItem = getActiveCheckListItem();
2015
+ if (activeItem) {
2016
+ const backward = e.key === "ArrowUp";
2017
+ const nextItem = findCheckListItemSibling(activeItem, backward);
2018
+ if (nextItem) {
2019
+ e.preventDefault();
2020
+ nextItem.focus();
2021
+ return true;
2022
+ }
1451
2023
  }
1452
- else if (plugin.command && value !== undefined) {
1453
- editorAPI.executeCommand(plugin.command, value);
2024
+ }
2025
+ // Space: toggle checkbox
2026
+ if (e.key === " ") {
2027
+ const activeItem = getActiveCheckListItem();
2028
+ if (activeItem) {
2029
+ e.preventDefault();
2030
+ toggleListItemChecked(activeItem);
2031
+ const editorEl = activeItem.closest(".rte-editor");
2032
+ if (editorEl) {
2033
+ editorEl.dispatchEvent(new Event("input", { bubbles: true }));
2034
+ }
2035
+ return true;
1454
2036
  }
1455
- else if (plugin.command) {
1456
- editorAPI.executeCommand(plugin.command);
2037
+ }
2038
+ // Escape: blur from checkbox item
2039
+ if (e.key === "Escape") {
2040
+ const activeItem = getActiveCheckListItem();
2041
+ if (activeItem) {
2042
+ const editorEl = activeItem.closest(".rte-editor");
2043
+ if (editorEl instanceof HTMLElement) {
2044
+ editorEl.focus();
2045
+ }
2046
+ return true;
1457
2047
  }
1458
- setTimeout(() => setUpdateTrigger((prev) => prev + 1), 50);
1459
2048
  }
1460
- };
1461
- const leftPlugins = plugins.filter((p) => p.name !== "clearFormatting");
1462
- const clearFormattingPlugin = plugins.find((p) => p.name === "clearFormatting");
1463
- return (jsxs("div", { className: `rte-toolbar rte-toolbar-sticky ${className || ""}`, children: [jsx("div", { className: "rte-toolbar-left", children: leftPlugins.map((plugin) => {
1464
- if (!plugin.renderButton)
1465
- return null;
1466
- const isActive = isClient && plugin.isActive
1467
- ? plugin.isActive(editorAPI)
1468
- : false;
1469
- const canExecute = isClient && plugin.canExecute
1470
- ? plugin.canExecute(editorAPI)
1471
- : true;
1472
- const currentValue = isClient && plugin.getCurrentValue
1473
- ? plugin.getCurrentValue(editorAPI)
1474
- : undefined;
1475
- const buttonProps = {
1476
- isActive,
1477
- onClick: () => handlePluginClick(plugin),
1478
- disabled: !canExecute,
1479
- onSelect: (value) => handlePluginClick(plugin, value),
1480
- editorAPI,
1481
- currentValue,
1482
- };
1483
- return (jsx(React.Fragment, { children: plugin.renderButton(buttonProps) }, plugin.name));
1484
- }) }), clearFormattingPlugin && clearFormattingPlugin.renderButton && (jsxs("div", { className: "rte-toolbar-right", children: [jsx("div", { className: "rte-toolbar-divider" }), (() => {
1485
- const isActive = isClient && clearFormattingPlugin.isActive
1486
- ? clearFormattingPlugin.isActive(editorAPI)
1487
- : false;
1488
- const canExecute = isClient && clearFormattingPlugin.canExecute
1489
- ? clearFormattingPlugin.canExecute(editorAPI)
1490
- : true;
1491
- const buttonProps = {
1492
- isActive,
1493
- onClick: () => handlePluginClick(clearFormattingPlugin),
1494
- disabled: !canExecute,
1495
- editorAPI,
1496
- };
1497
- return (jsx(React.Fragment, { children: clearFormattingPlugin.renderButton(buttonProps) }, clearFormattingPlugin.name));
1498
- })()] }))] }));
1499
- };
1500
-
1501
- const Editor = ({ initialContent, onChange, plugins: providedPlugins, placeholder = "Text eingeben...", className, toolbarClassName, editorClassName, fontSizes, colors, headings, customLinkComponent, customHeadingRenderer, customRenderer, onEditorAPIReady, theme, onImageUpload, }) => {
1502
- const plugins = useMemo(() => {
1503
- const allPlugins = [...(providedPlugins || defaultPlugins)];
1504
- if (fontSizes && fontSizes.length > 0) {
1505
- allPlugins.push(createFontSizePlugin(fontSizes));
2049
+ // Arrow left: focus list item when cursor is at the start
2050
+ if (e.key === "ArrowLeft") {
2051
+ const selection = window.getSelection();
2052
+ if (!selection || selection.rangeCount === 0)
2053
+ return false;
2054
+ const range = selection.getRangeAt(0);
2055
+ const container = range.commonAncestorContainer;
2056
+ const listItem = findClosestListItem(container);
2057
+ if (!listItem)
2058
+ return false;
2059
+ const parent = listItem.parentElement;
2060
+ if (parent &&
2061
+ isCheckboxList(parent) &&
2062
+ range.collapsed &&
2063
+ range.startOffset === 0) {
2064
+ if (document.activeElement !== listItem) {
2065
+ listItem.focus();
2066
+ e.preventDefault();
2067
+ return true;
2068
+ }
2069
+ }
1506
2070
  }
1507
- if (colors && colors.length > 0) {
1508
- allPlugins.push(createTextColorPlugin(colors));
1509
- allPlugins.push(createBackgroundColorPlugin(colors));
2071
+ return false;
2072
+ }, []);
2073
+ /**
2074
+ * Handles Enter key in checkbox lists.
2075
+ * Creates a new unchecked item after the current one.
2076
+ * Returns true if the event was handled.
2077
+ */
2078
+ const handleCheckboxEnter = useCallback((e) => {
2079
+ if (e.key !== "Enter")
2080
+ return false;
2081
+ const selection = window.getSelection();
2082
+ if (!selection || selection.rangeCount === 0)
2083
+ return false;
2084
+ const range = selection.getRangeAt(0);
2085
+ const container = range.commonAncestorContainer;
2086
+ const listItem = findClosestListItem(container);
2087
+ if (!listItem)
2088
+ return false;
2089
+ const checkboxList = findClosestCheckboxList(listItem);
2090
+ if (!checkboxList)
2091
+ return false;
2092
+ e.preventDefault();
2093
+ const newLi = document.createElement("li");
2094
+ updateListItemChecked(newLi, false);
2095
+ const textNode = document.createTextNode(" ");
2096
+ newLi.appendChild(textNode);
2097
+ if (listItem.nextSibling) {
2098
+ checkboxList.insertBefore(newLi, listItem.nextSibling);
1510
2099
  }
1511
- // BlockFormat Plugin ist bereits in defaultPlugins enthalten
1512
- // Wenn custom headings angegeben sind, ersetze das Standard-Plugin
1513
- if (headings && headings.length > 0) {
1514
- // Entferne das Standard-BlockFormat-Plugin
1515
- const blockFormatIndex = allPlugins.findIndex((p) => p.name === "blockFormat");
1516
- if (blockFormatIndex !== -1) {
1517
- allPlugins.splice(blockFormatIndex, 1);
1518
- }
1519
- // Füge das Plugin mit custom Headlines hinzu
1520
- allPlugins.push(createBlockFormatPlugin(headings));
2100
+ else {
2101
+ checkboxList.appendChild(newLi);
1521
2102
  }
1522
- allPlugins.push(createImagePlugin(onImageUpload));
1523
- return allPlugins;
1524
- }, [providedPlugins, fontSizes, colors, headings, onImageUpload]);
1525
- const editorRef = useRef(null);
1526
- const historyRef = useRef(new HistoryManager());
1527
- const isUpdatingRef = useRef(false);
1528
- const notifyChange = useCallback((content) => {
1529
- if (onChange && !isUpdatingRef.current) {
1530
- onChange(content);
2103
+ const editor = editorRef.current;
2104
+ if (editor)
2105
+ ensureAllCheckboxes(editor);
2106
+ const newRange = document.createRange();
2107
+ newRange.setStart(textNode, 0);
2108
+ newRange.collapse(true);
2109
+ selection.removeAllRanges();
2110
+ selection.addRange(newRange);
2111
+ if (editor) {
2112
+ const content = getDomContent();
2113
+ pushToHistory(content);
2114
+ notifyChange(content);
1531
2115
  }
1532
- }, [onChange]);
1533
- const restoreSelection = useCallback((editor) => {
1534
- if (typeof window === "undefined" || typeof document === "undefined")
1535
- return;
1536
- const range = document.createRange();
2116
+ return true;
2117
+ }, [editorRef, getDomContent, pushToHistory, notifyChange]);
2118
+ /**
2119
+ * Inserts a new checkbox list at the current cursor position,
2120
+ * or removes it if already inside one.
2121
+ * Returns true on success.
2122
+ */
2123
+ const insertCheckboxList = useCallback((editor) => {
2124
+ isUpdatingRef.current = true;
1537
2125
  const selection = window.getSelection();
1538
- if (editor.firstChild) {
1539
- range.setStart(editor.firstChild, 0);
1540
- range.collapse(true);
1541
- selection?.removeAllRanges();
1542
- selection?.addRange(range);
2126
+ if (!selection || selection.rangeCount === 0) {
2127
+ isUpdatingRef.current = false;
2128
+ return false;
1543
2129
  }
1544
- }, []);
1545
- const editorAPI = useMemo(() => {
1546
- const executeCommand = (command, value) => {
1547
- const editor = editorRef.current;
1548
- if (!editor)
1549
- return false;
1550
- if (command !== "undo" &&
1551
- command !== "redo" &&
1552
- command !== "insertImage") {
1553
- const currentContent = domToContent(editor);
1554
- historyRef.current.push(currentContent);
1555
- }
1556
- if (command === "undo") {
1557
- const content = historyRef.current.undo();
1558
- if (content && editor) {
1559
- isUpdatingRef.current = true;
1560
- contentToDOM(content, editor, customLinkComponent);
1561
- restoreSelection(editor);
1562
- isUpdatingRef.current = false;
1563
- notifyChange(content);
1564
- }
1565
- return true;
1566
- }
1567
- if (command === "redo") {
1568
- const content = historyRef.current.redo();
1569
- if (content && editor) {
1570
- isUpdatingRef.current = true;
1571
- contentToDOM(content, editor, customLinkComponent);
1572
- restoreSelection(editor);
1573
- isUpdatingRef.current = false;
1574
- notifyChange(content);
1575
- }
1576
- return true;
2130
+ const range = selection.getRangeAt(0);
2131
+ const container = range.commonAncestorContainer;
2132
+ const element = container.nodeType === Node.TEXT_NODE
2133
+ ? container.parentElement
2134
+ : container;
2135
+ if (!element) {
2136
+ isUpdatingRef.current = false;
2137
+ return false;
2138
+ }
2139
+ // Already in a checkbox list? Remove it.
2140
+ const existingList = findClosestCheckboxList(element);
2141
+ if (existingList) {
2142
+ existingList.classList.remove("rte-checkbox-list");
2143
+ existingList
2144
+ .querySelectorAll("li[role='checkbox']")
2145
+ .forEach((li) => {
2146
+ li.removeAttribute("role");
2147
+ li.removeAttribute("tabIndex");
2148
+ li.removeAttribute("aria-checked");
2149
+ });
2150
+ isUpdatingRef.current = false;
2151
+ return true;
2152
+ }
2153
+ // Create new checkbox list
2154
+ const ul = document.createElement("ul");
2155
+ ul.classList.add("rte-checkbox-list");
2156
+ const li = document.createElement("li");
2157
+ updateListItemChecked(li, false);
2158
+ const textNode = document.createTextNode(" ");
2159
+ li.appendChild(textNode);
2160
+ ul.appendChild(li);
2161
+ // Find block element to replace
2162
+ const blockElement = element.closest("p, div, h1, h2, h3, h4, h5, h6, blockquote");
2163
+ const isValidBlockElement = blockElement &&
2164
+ blockElement !== editor &&
2165
+ editor.contains(blockElement) &&
2166
+ blockElement.parentElement;
2167
+ if (isValidBlockElement) {
2168
+ const textContent = blockElement.textContent || "";
2169
+ blockElement.parentElement.replaceChild(ul, blockElement);
2170
+ const finalTextNode = li.firstChild;
2171
+ if (finalTextNode) {
2172
+ finalTextNode.textContent = textContent || " ";
2173
+ const cursorPos = textContent ? textContent.length : 0;
2174
+ setCursorInTextNode(finalTextNode, cursorPos, editor);
1577
2175
  }
1578
- if (command === "insertImage" && value) {
1579
- let selection = window.getSelection();
1580
- if (!selection)
1581
- return false;
1582
- if (document.activeElement !== editor) {
1583
- editor.focus();
1584
- }
1585
- if (selection.rangeCount === 0) {
1586
- const range = document.createRange();
1587
- if (editor.childNodes.length > 0) {
1588
- const lastChild = editor.childNodes[editor.childNodes.length - 1];
1589
- range.setStartAfter(lastChild);
1590
- range.collapse(true);
1591
- }
1592
- else {
1593
- const img = document.createElement("img");
1594
- img.setAttribute("src", value);
1595
- img.setAttribute("alt", "");
1596
- img.style.maxWidth = "100%";
1597
- img.style.height = "auto";
1598
- img.style.display = "block";
1599
- img.style.margin = "16px 0";
1600
- editor.appendChild(img);
1601
- const newRange = document.createRange();
1602
- newRange.setStartAfter(img);
1603
- newRange.collapse(true);
1604
- selection.removeAllRanges();
1605
- selection.addRange(newRange);
1606
- isUpdatingRef.current = true;
1607
- setTimeout(() => {
1608
- if (editor) {
1609
- const currentContent = domToContent(editor);
1610
- historyRef.current.push(currentContent);
1611
- isUpdatingRef.current = false;
1612
- notifyChange(currentContent);
1613
- }
1614
- }, 0);
1615
- return true;
1616
- }
1617
- selection.removeAllRanges();
1618
- selection.addRange(range);
1619
- }
1620
- if (selection.rangeCount === 0)
1621
- return false;
1622
- const range = selection.getRangeAt(0);
1623
- const container = range.commonAncestorContainer;
1624
- let parentElement = null;
1625
- if (container.nodeType === Node.TEXT_NODE) {
1626
- parentElement = container.parentElement;
1627
- }
1628
- else if (container.nodeType === Node.ELEMENT_NODE) {
1629
- parentElement = container;
1630
- }
1631
- const img = document.createElement("img");
1632
- img.setAttribute("src", value);
1633
- img.setAttribute("alt", "");
1634
- img.style.maxWidth = "100%";
1635
- img.style.height = "auto";
1636
- img.style.display = "block";
1637
- img.style.margin = "16px 0";
1638
- if (parentElement &&
1639
- parentElement !== editor &&
1640
- (parentElement.tagName === "P" ||
1641
- parentElement.tagName === "DIV" ||
1642
- parentElement.tagName === "H1" ||
1643
- parentElement.tagName === "H2" ||
1644
- parentElement.tagName === "H3" ||
1645
- parentElement.tagName === "H4" ||
1646
- parentElement.tagName === "H5" ||
1647
- parentElement.tagName === "H6")) {
1648
- if (parentElement.nextSibling) {
1649
- editor.insertBefore(img, parentElement.nextSibling);
1650
- }
1651
- else {
1652
- editor.appendChild(img);
1653
- }
1654
- }
1655
- else {
1656
- try {
1657
- range.insertNode(img);
1658
- }
1659
- catch (e) {
1660
- editor.appendChild(img);
1661
- }
2176
+ }
2177
+ else {
2178
+ try {
2179
+ range.deleteContents();
2180
+ range.insertNode(ul);
2181
+ const finalTextNode = li.firstChild;
2182
+ if (finalTextNode) {
2183
+ setCursorInTextNode(finalTextNode, 0, editor);
1662
2184
  }
1663
- const newRange = document.createRange();
1664
- newRange.setStartAfter(img);
1665
- newRange.collapse(true);
1666
- selection.removeAllRanges();
1667
- selection.addRange(newRange);
1668
- isUpdatingRef.current = true;
1669
- setTimeout(() => {
1670
- if (editor) {
1671
- const currentContent = domToContent(editor);
1672
- historyRef.current.push(currentContent);
1673
- isUpdatingRef.current = false;
1674
- notifyChange(currentContent);
1675
- }
1676
- }, 0);
1677
- return true;
1678
- }
1679
- const selection = window.getSelection();
1680
- let savedRange = null;
1681
- if (selection && selection.rangeCount > 0) {
1682
- savedRange = selection.getRangeAt(0).cloneRange();
1683
- }
1684
- if (document.activeElement !== editor) {
1685
- editor.focus();
1686
2185
  }
1687
- if (!selection || selection.rangeCount === 0) {
1688
- const range = document.createRange();
1689
- if (editor.childNodes.length > 0) {
1690
- const lastChild = editor.childNodes[editor.childNodes.length - 1];
1691
- if (lastChild.nodeType === Node.TEXT_NODE) {
1692
- range.setStart(lastChild, lastChild.textContent?.length || 0);
1693
- range.setEnd(lastChild, lastChild.textContent?.length || 0);
1694
- }
1695
- else {
1696
- range.selectNodeContents(lastChild);
1697
- range.collapse(false);
1698
- }
1699
- }
1700
- else {
1701
- const p = document.createElement("p");
1702
- editor.appendChild(p);
1703
- const textNode = document.createTextNode("");
1704
- p.appendChild(textNode);
1705
- range.setStart(textNode, 0);
1706
- range.setEnd(textNode, 0);
2186
+ catch (_) {
2187
+ editor.appendChild(ul);
2188
+ const finalTextNode = li.firstChild;
2189
+ if (finalTextNode) {
2190
+ setCursorInTextNode(finalTextNode, 0, editor);
1707
2191
  }
1708
- selection?.removeAllRanges();
1709
- selection?.addRange(range);
1710
2192
  }
1711
- else if (savedRange) {
1712
- selection.removeAllRanges();
1713
- selection.addRange(savedRange);
1714
- }
1715
- document.execCommand(command, false, value);
1716
- setTimeout(() => {
1717
- if (editor && !isUpdatingRef.current) {
1718
- const content = domToContent(editor);
1719
- notifyChange(content);
1720
- }
1721
- }, 0);
1722
- return true;
1723
- };
1724
- return {
1725
- executeCommand,
1726
- getSelection: () => {
1727
- if (typeof window === "undefined")
1728
- return null;
2193
+ }
2194
+ // After insertion: ensure attributes and save to history
2195
+ setTimeout(() => {
2196
+ if (!editor)
2197
+ return;
2198
+ ensureAllCheckboxes(editor);
2199
+ const content = getDomContent();
2200
+ pushToHistory(content);
2201
+ isUpdatingRef.current = false;
2202
+ notifyChange(content);
2203
+ }, 100);
2204
+ return true;
2205
+ }, [isUpdatingRef, getDomContent, pushToHistory, notifyChange]);
2206
+ return {
2207
+ ensureAllCheckboxes,
2208
+ insertCheckboxList,
2209
+ handleCheckboxKeyDown,
2210
+ handleCheckboxEnter,
2211
+ updateListItemChecked,
2212
+ };
2213
+ }
2214
+
2215
+ /**
2216
+ * Table DOM manipulation utilities for contentEditable.
2217
+ * Pure DOM functions — no React dependency.
2218
+ */
2219
+ /* ── Helpers ──────────────────────────────────────────────────────────── */
2220
+ /** Get the <td>/<th> cell that contains the current selection. */
2221
+ function getActiveCell() {
2222
+ const sel = document.getSelection();
2223
+ if (!sel || sel.rangeCount === 0)
2224
+ return null;
2225
+ const range = sel.getRangeAt(0);
2226
+ const container = range.commonAncestorContainer;
2227
+ const el = container.nodeType === Node.TEXT_NODE
2228
+ ? container.parentElement
2229
+ : container;
2230
+ return el?.closest("td, th");
2231
+ }
2232
+ /** Get the <table> that contains the current selection. */
2233
+ function getActiveTable() {
2234
+ const cell = getActiveCell();
2235
+ return cell?.closest("table") ?? null;
2236
+ }
2237
+ /** Get the <tr> that contains the current selection. */
2238
+ function getActiveRow() {
2239
+ const cell = getActiveCell();
2240
+ return cell?.closest("tr") ?? null;
2241
+ }
2242
+ /** Place the cursor inside a cell (at the start). */
2243
+ function focusCell(cell) {
2244
+ const range = document.createRange();
2245
+ const sel = document.getSelection();
2246
+ if (!sel)
2247
+ return;
2248
+ if (cell.firstChild) {
2249
+ range.setStart(cell.firstChild, 0);
2250
+ }
2251
+ else {
2252
+ // Empty cell — add a <br> so the cursor has something to land on
2253
+ const br = document.createElement("br");
2254
+ cell.appendChild(br);
2255
+ range.setStart(cell, 0);
2256
+ }
2257
+ range.collapse(true);
2258
+ sel.removeAllRanges();
2259
+ sel.addRange(range);
2260
+ }
2261
+ /** Count the number of columns in a table (from the first row). */
2262
+ function getColumnCount(table) {
2263
+ const firstRow = table.querySelector("tr");
2264
+ return firstRow ? firstRow.cells.length : 0;
2265
+ }
2266
+ /* ── Create ───────────────────────────────────────────────────────────── */
2267
+ /** Create a new table element with the given dimensions. */
2268
+ function createTable(rows, cols, withHeader = false) {
2269
+ const table = document.createElement("table");
2270
+ table.classList.add("rte-table");
2271
+ if (withHeader) {
2272
+ const thead = document.createElement("thead");
2273
+ const tr = document.createElement("tr");
2274
+ for (let c = 0; c < cols; c++) {
2275
+ const th = document.createElement("th");
2276
+ th.innerHTML = "<br>";
2277
+ tr.appendChild(th);
2278
+ }
2279
+ thead.appendChild(tr);
2280
+ table.appendChild(thead);
2281
+ }
2282
+ const tbody = document.createElement("tbody");
2283
+ const dataRows = withHeader ? rows - 1 : rows;
2284
+ for (let r = 0; r < dataRows; r++) {
2285
+ const tr = document.createElement("tr");
2286
+ for (let c = 0; c < cols; c++) {
2287
+ const td = document.createElement("td");
2288
+ td.innerHTML = "<br>";
2289
+ tr.appendChild(td);
2290
+ }
2291
+ tbody.appendChild(tr);
2292
+ }
2293
+ table.appendChild(tbody);
2294
+ return table;
2295
+ }
2296
+ /* ── Row operations ───────────────────────────────────────────────────── */
2297
+ /** Insert a row above or below the currently selected row. */
2298
+ function insertRow(position) {
2299
+ const row = getActiveRow();
2300
+ const table = getActiveTable();
2301
+ if (!row || !table)
2302
+ return;
2303
+ const cols = row.cells.length;
2304
+ const newRow = document.createElement("tr");
2305
+ for (let c = 0; c < cols; c++) {
2306
+ const td = document.createElement("td");
2307
+ td.innerHTML = "<br>";
2308
+ newRow.appendChild(td);
2309
+ }
2310
+ if (position === "above") {
2311
+ row.parentNode?.insertBefore(newRow, row);
2312
+ }
2313
+ else {
2314
+ row.parentNode?.insertBefore(newRow, row.nextSibling);
2315
+ }
2316
+ focusCell(newRow.cells[0]);
2317
+ }
2318
+ /** Delete the row that contains the current selection. */
2319
+ function deleteRow() {
2320
+ const row = getActiveRow();
2321
+ const table = getActiveTable();
2322
+ if (!row || !table)
2323
+ return;
2324
+ const allRows = table.querySelectorAll("tr");
2325
+ if (allRows.length <= 1) {
2326
+ // Last row — remove the whole table
2327
+ deleteTable();
2328
+ return;
2329
+ }
2330
+ // Focus adjacent row before deleting
2331
+ const nextRow = (row.nextElementSibling ||
2332
+ row.previousElementSibling);
2333
+ row.remove();
2334
+ if (nextRow?.cells[0]) {
2335
+ focusCell(nextRow.cells[0]);
2336
+ }
2337
+ }
2338
+ /* ── Column operations ────────────────────────────────────────────────── */
2339
+ /** Insert a column to the left or right of the currently selected cell. */
2340
+ function insertColumn(position) {
2341
+ const cell = getActiveCell();
2342
+ const table = getActiveTable();
2343
+ if (!cell || !table)
2344
+ return;
2345
+ const cellIndex = cell.cellIndex;
2346
+ const rows = table.querySelectorAll("tr");
2347
+ rows.forEach((row) => {
2348
+ const tag = row.parentElement?.tagName === "THEAD" ? "th" : "td";
2349
+ const newCell = document.createElement(tag);
2350
+ newCell.innerHTML = "<br>";
2351
+ const refCell = row.cells[cellIndex];
2352
+ if (!refCell) {
2353
+ row.appendChild(newCell);
2354
+ }
2355
+ else if (position === "left") {
2356
+ row.insertBefore(newCell, refCell);
2357
+ }
2358
+ else {
2359
+ row.insertBefore(newCell, refCell.nextSibling);
2360
+ }
2361
+ });
2362
+ }
2363
+ /** Delete the column that contains the currently selected cell. */
2364
+ function deleteColumn() {
2365
+ const cell = getActiveCell();
2366
+ const table = getActiveTable();
2367
+ if (!cell || !table)
2368
+ return;
2369
+ const cellIndex = cell.cellIndex;
2370
+ const colCount = getColumnCount(table);
2371
+ if (colCount <= 1) {
2372
+ // Last column — remove the whole table
2373
+ deleteTable();
2374
+ return;
2375
+ }
2376
+ const rows = table.querySelectorAll("tr");
2377
+ rows.forEach((row) => {
2378
+ if (row.cells[cellIndex]) {
2379
+ row.cells[cellIndex].remove();
2380
+ }
2381
+ });
2382
+ // Focus an adjacent cell
2383
+ const activeRow = getActiveRow();
2384
+ if (activeRow) {
2385
+ const idx = Math.min(cellIndex, activeRow.cells.length - 1);
2386
+ if (activeRow.cells[idx])
2387
+ focusCell(activeRow.cells[idx]);
2388
+ }
2389
+ }
2390
+ /* ── Table delete ─────────────────────────────────────────────────────── */
2391
+ /** Remove the entire table and place cursor after it. */
2392
+ function deleteTable() {
2393
+ const table = getActiveTable();
2394
+ if (!table)
2395
+ return;
2396
+ const parent = table.parentNode;
2397
+ const nextSibling = table.nextSibling;
2398
+ // Create a paragraph to place the cursor
2399
+ const p = document.createElement("p");
2400
+ p.innerHTML = "<br>";
2401
+ if (nextSibling) {
2402
+ parent?.insertBefore(p, nextSibling);
2403
+ }
2404
+ else {
2405
+ parent?.appendChild(p);
2406
+ }
2407
+ table.remove();
2408
+ // Focus the new paragraph
2409
+ const range = document.createRange();
2410
+ const sel = document.getSelection();
2411
+ if (sel && p.firstChild) {
2412
+ range.setStart(p, 0);
2413
+ range.collapse(true);
2414
+ sel.removeAllRanges();
2415
+ sel.addRange(range);
2416
+ }
2417
+ }
2418
+ /* ── Tab navigation ───────────────────────────────────────────────────── */
2419
+ /**
2420
+ * Move focus to the next or previous table cell.
2421
+ * Returns true if navigation happened (caller should preventDefault).
2422
+ */
2423
+ function navigateTableCell(direction) {
2424
+ const cell = getActiveCell();
2425
+ if (!cell)
2426
+ return false;
2427
+ const row = cell.closest("tr");
2428
+ const table = cell.closest("table");
2429
+ if (!row || !table)
2430
+ return false;
2431
+ const cellIndex = cell.cellIndex;
2432
+ const allRows = Array.from(table.querySelectorAll("tr"));
2433
+ const rowIndex = allRows.indexOf(row);
2434
+ let targetCell = null;
2435
+ if (direction === "next") {
2436
+ if (cellIndex < row.cells.length - 1) {
2437
+ targetCell = row.cells[cellIndex + 1];
2438
+ }
2439
+ else if (rowIndex < allRows.length - 1) {
2440
+ targetCell = allRows[rowIndex + 1].cells[0];
2441
+ }
2442
+ else {
2443
+ // Last cell of last row — add a new row
2444
+ insertRow("below");
2445
+ return true;
2446
+ }
2447
+ }
2448
+ else {
2449
+ if (cellIndex > 0) {
2450
+ targetCell = row.cells[cellIndex - 1];
2451
+ }
2452
+ else if (rowIndex > 0) {
2453
+ const prevRow = allRows[rowIndex - 1];
2454
+ targetCell = prevRow.cells[prevRow.cells.length - 1];
2455
+ }
2456
+ }
2457
+ if (targetCell) {
2458
+ focusCell(targetCell);
2459
+ return true;
2460
+ }
2461
+ return false;
2462
+ }
2463
+
2464
+ /**
2465
+ * Hook that sets up input, keyup, and keydown event listeners on the editor.
2466
+ */
2467
+ function useEditorEvents({ editorRef, historyRef, isUpdatingRef, notifyChange, handleCheckboxKeyDown, handleCheckboxEnter, undo, redo, }) {
2468
+ useEffect(() => {
2469
+ const editor = editorRef.current;
2470
+ if (!editor)
2471
+ return;
2472
+ let inputTimeout = null;
2473
+ const handleInput = () => {
2474
+ setTimeout(() => {
2475
+ const content = domToContent(editor);
2476
+ notifyChange(content);
2477
+ if (inputTimeout)
2478
+ clearTimeout(inputTimeout);
2479
+ inputTimeout = setTimeout(() => {
2480
+ historyRef.current.push(content);
2481
+ inputTimeout = null;
2482
+ }, HISTORY_DEBOUNCE_MS);
2483
+ }, 0);
2484
+ };
2485
+ const handleKeyDown = (e) => {
2486
+ const isModifierPressed = e.metaKey || e.ctrlKey;
2487
+ // Checkbox Enter: create new checkbox item
2488
+ if (handleCheckboxEnter(e))
2489
+ return;
2490
+ // Checkbox keyboard navigation
2491
+ if (handleCheckboxKeyDown(e))
2492
+ return;
2493
+ // Tab: navigate table cells OR indent/outdent in lists
2494
+ if (e.key === "Tab" && !isModifierPressed && !e.altKey) {
2495
+ // Table tab navigation takes priority
2496
+ if (getActiveCell()) {
2497
+ e.preventDefault();
2498
+ e.stopPropagation();
2499
+ e.stopImmediatePropagation();
2500
+ navigateTableCell(e.shiftKey ? "prev" : "next");
2501
+ setTimeout(() => {
2502
+ if (editor) {
2503
+ const content = domToContent(editor);
2504
+ notifyChange(content);
2505
+ }
2506
+ }, 0);
2507
+ return;
2508
+ }
2509
+ e.preventDefault();
2510
+ e.stopPropagation();
2511
+ e.stopImmediatePropagation();
2512
+ const selection = window.getSelection();
2513
+ if (!selection || selection.rangeCount === 0)
2514
+ return;
2515
+ const range = selection.getRangeAt(0);
2516
+ const container = range.commonAncestorContainer;
2517
+ if (!editor.contains(container))
2518
+ return;
2519
+ const listItem = container.nodeType === Node.TEXT_NODE
2520
+ ? container.parentElement?.closest("li")
2521
+ : container.closest("li");
2522
+ if (listItem && editor.contains(listItem)) {
2523
+ const currentContent = domToContent(editor);
2524
+ historyRef.current.push(currentContent);
2525
+ if (e.shiftKey) {
2526
+ outdentListItem(selection);
2527
+ }
2528
+ else {
2529
+ indentListItem(selection);
2530
+ }
2531
+ setTimeout(() => {
2532
+ if (editor) {
2533
+ const content = domToContent(editor);
2534
+ notifyChange(content);
2535
+ }
2536
+ }, 0);
2537
+ return;
2538
+ }
2539
+ }
2540
+ // Cmd/Ctrl+K: trigger link button click (if present in toolbar)
2541
+ if (isModifierPressed && e.key === "k") {
2542
+ e.preventDefault();
2543
+ e.stopPropagation();
2544
+ // Find and click the link button in the toolbar
2545
+ const editorContainer = editor.closest(".rte-container");
2546
+ if (editorContainer) {
2547
+ const linkBtn = editorContainer.querySelector('button[aria-label="Link"], button[aria-label="Link einfügen"]');
2548
+ if (linkBtn)
2549
+ linkBtn.click();
2550
+ }
2551
+ return;
2552
+ }
2553
+ // Undo/Redo shortcuts
2554
+ if (isModifierPressed && e.key === "z" && !e.shiftKey) {
2555
+ e.preventDefault();
2556
+ e.stopPropagation();
2557
+ undo();
2558
+ }
2559
+ else if (isModifierPressed &&
2560
+ (e.key === "y" || (e.key === "z" && e.shiftKey))) {
2561
+ e.preventDefault();
2562
+ e.stopPropagation();
2563
+ redo();
2564
+ }
2565
+ };
2566
+ editor.addEventListener("input", handleInput);
2567
+ editor.addEventListener("keyup", handleInput);
2568
+ editor.addEventListener("keydown", handleKeyDown, true);
2569
+ return () => {
2570
+ editor.removeEventListener("input", handleInput);
2571
+ editor.removeEventListener("keyup", handleInput);
2572
+ editor.removeEventListener("keydown", handleKeyDown, true);
2573
+ if (inputTimeout)
2574
+ clearTimeout(inputTimeout);
2575
+ };
2576
+ }, [
2577
+ editorRef,
2578
+ historyRef,
2579
+ isUpdatingRef,
2580
+ notifyChange,
2581
+ handleCheckboxKeyDown,
2582
+ handleCheckboxEnter,
2583
+ undo,
2584
+ redo,
2585
+ ]);
2586
+ }
2587
+
2588
+ /**
2589
+ * Hook that initializes the editor with initial content and sets up the MutationObserver.
2590
+ * Runs once on mount.
2591
+ */
2592
+ function useEditorInit({ editorRef, historyRef, isUpdatingRef, initialContent, customLinkComponent, customHeadingRenderer, }) {
2593
+ const isInitializedRef = useRef(false);
2594
+ useEffect(() => {
2595
+ const editor = editorRef.current;
2596
+ if (!editor || isInitializedRef.current)
2597
+ return;
2598
+ // Load initial content
2599
+ const content = initialContent || createEmptyContent();
2600
+ isUpdatingRef.current = true;
2601
+ contentToDOM(content, editor, customLinkComponent);
2602
+ historyRef.current.push(content);
2603
+ isUpdatingRef.current = false;
2604
+ isInitializedRef.current = true;
2605
+ // Normalize all checkbox lists (own + Lexical + GitHub formats)
2606
+ ensureAllCheckboxes(editor);
2607
+ // MutationObserver: ensure new checkbox list items get correct attributes
2608
+ const observer = new MutationObserver((mutations) => {
2609
+ if (isUpdatingRef.current)
2610
+ return;
2611
+ let needsUpdate = false;
2612
+ for (const mutation of mutations) {
2613
+ for (let i = 0; i < mutation.addedNodes.length; i++) {
2614
+ const node = mutation.addedNodes[i];
2615
+ if (node.nodeType !== Node.ELEMENT_NODE)
2616
+ continue;
2617
+ const el = node;
2618
+ if (el.tagName === "LI" &&
2619
+ el.closest("ul.rte-checkbox-list")) {
2620
+ needsUpdate = true;
2621
+ break;
2622
+ }
2623
+ if (el.tagName === "UL" &&
2624
+ el.classList.contains("rte-checkbox-list")) {
2625
+ needsUpdate = true;
2626
+ break;
2627
+ }
2628
+ if (el.querySelector("ul.rte-checkbox-list li")) {
2629
+ needsUpdate = true;
2630
+ break;
2631
+ }
2632
+ }
2633
+ if (needsUpdate)
2634
+ break;
2635
+ }
2636
+ if (needsUpdate) {
2637
+ setTimeout(() => ensureAllCheckboxes(editor), 0);
2638
+ }
2639
+ });
2640
+ observer.observe(editor, { childList: true, subtree: true });
2641
+ return () => {
2642
+ observer.disconnect();
2643
+ };
2644
+ // Only run once on mount
2645
+ // eslint-disable-next-line react-hooks/exhaustive-deps
2646
+ }, []);
2647
+ }
2648
+
2649
+ /**
2650
+ * Hook for editor selection management.
2651
+ */
2652
+ function useEditorSelection() {
2653
+ const restoreSelection = useCallback((editor) => {
2654
+ if (typeof window === "undefined" ||
2655
+ typeof document === "undefined")
2656
+ return;
2657
+ const range = document.createRange();
2658
+ const selection = window.getSelection();
2659
+ if (editor.firstChild) {
2660
+ range.setStart(editor.firstChild, 0);
2661
+ range.collapse(true);
2662
+ selection?.removeAllRanges();
2663
+ selection?.addRange(range);
2664
+ }
2665
+ }, []);
2666
+ return { restoreSelection };
2667
+ }
2668
+
2669
+ const Toolbar = ({ plugins, editorAPI, className, }) => {
2670
+ const [updateTrigger, setUpdateTrigger] = useState(0);
2671
+ const [isClient, setIsClient] = useState(false);
2672
+ useEffect(() => {
2673
+ setIsClient(true);
2674
+ const handleSelectionChange = () => {
2675
+ setUpdateTrigger((prev) => prev + 1);
2676
+ };
2677
+ const handleMouseUp = () => {
2678
+ setTimeout(handleSelectionChange, 10);
2679
+ };
2680
+ const handleKeyUp = () => {
2681
+ setTimeout(handleSelectionChange, 10);
2682
+ };
2683
+ if (typeof document !== 'undefined') {
2684
+ document.addEventListener("selectionchange", handleSelectionChange);
2685
+ document.addEventListener("mouseup", handleMouseUp);
2686
+ document.addEventListener("keyup", handleKeyUp);
2687
+ }
2688
+ return () => {
2689
+ if (typeof document !== 'undefined') {
2690
+ document.removeEventListener("selectionchange", handleSelectionChange);
2691
+ document.removeEventListener("mouseup", handleMouseUp);
2692
+ document.removeEventListener("keyup", handleKeyUp);
2693
+ }
2694
+ };
2695
+ }, []);
2696
+ const handlePluginClick = (plugin, value) => {
2697
+ if (plugin.canExecute?.(editorAPI) !== false) {
2698
+ if (plugin.execute) {
2699
+ plugin.execute(editorAPI, value);
2700
+ }
2701
+ else if (plugin.command && value !== undefined) {
2702
+ editorAPI.executeCommand(plugin.command, value);
2703
+ }
2704
+ else if (plugin.command) {
2705
+ editorAPI.executeCommand(plugin.command);
2706
+ }
2707
+ setTimeout(() => setUpdateTrigger((prev) => prev + 1), 50);
2708
+ }
2709
+ };
2710
+ const leftPlugins = plugins.filter((p) => p.name !== "clearFormatting");
2711
+ const clearFormattingPlugin = plugins.find((p) => p.name === "clearFormatting");
2712
+ return (jsxs("div", { className: `rte-toolbar rte-toolbar-sticky ${className || ""}`, children: [jsx("div", { className: "rte-toolbar-left", children: leftPlugins.map((plugin) => {
2713
+ if (!plugin.renderButton)
2714
+ return null;
2715
+ const isActive = isClient && plugin.isActive
2716
+ ? plugin.isActive(editorAPI)
2717
+ : false;
2718
+ const canExecute = isClient && plugin.canExecute
2719
+ ? plugin.canExecute(editorAPI)
2720
+ : true;
2721
+ const currentValue = isClient && plugin.getCurrentValue
2722
+ ? plugin.getCurrentValue(editorAPI)
2723
+ : undefined;
2724
+ const buttonProps = {
2725
+ isActive,
2726
+ onClick: () => handlePluginClick(plugin),
2727
+ disabled: !canExecute,
2728
+ onSelect: (value) => handlePluginClick(plugin, value),
2729
+ editorAPI,
2730
+ currentValue,
2731
+ };
2732
+ return (jsx(React.Fragment, { children: plugin.renderButton(buttonProps) }, plugin.name));
2733
+ }) }), clearFormattingPlugin && clearFormattingPlugin.renderButton && (jsxs("div", { className: "rte-toolbar-right", children: [jsx("div", { className: "rte-toolbar-divider" }), (() => {
2734
+ const isActive = isClient && clearFormattingPlugin.isActive
2735
+ ? clearFormattingPlugin.isActive(editorAPI)
2736
+ : false;
2737
+ const canExecute = isClient && clearFormattingPlugin.canExecute
2738
+ ? clearFormattingPlugin.canExecute(editorAPI)
2739
+ : true;
2740
+ const buttonProps = {
2741
+ isActive,
2742
+ onClick: () => handlePluginClick(clearFormattingPlugin),
2743
+ disabled: !canExecute,
2744
+ editorAPI,
2745
+ };
2746
+ return (jsx(React.Fragment, { children: clearFormattingPlugin.renderButton(buttonProps) }, clearFormattingPlugin.name));
2747
+ })()] }))] }));
2748
+ };
2749
+
2750
+ const Editor = ({ initialContent, onChange, plugins: providedPlugins, placeholder = "Text eingeben...", className, toolbarClassName, editorClassName, fontSizes, colors, headings, customLinkComponent, customHeadingRenderer, customRenderer, onEditorAPIReady, theme, onImageUpload, }) => {
2751
+ // --- Shared Refs ---
2752
+ const editorRef = useRef(null);
2753
+ const historyRef = useRef(new HistoryManager());
2754
+ const isUpdatingRef = useRef(false);
2755
+ // --- Plugins ---
2756
+ const plugins = useMemo(() => {
2757
+ const allPlugins = [...(providedPlugins || defaultPlugins)];
2758
+ if (headings && headings.length > 0) {
2759
+ const blockFormatIndex = allPlugins.findIndex((p) => p.name === "blockFormat");
2760
+ if (blockFormatIndex !== -1) {
2761
+ allPlugins[blockFormatIndex] =
2762
+ createBlockFormatPlugin(headings);
2763
+ }
2764
+ else {
2765
+ const redoIndex = allPlugins.findIndex((p) => p.name === "redo");
2766
+ if (redoIndex !== -1) {
2767
+ allPlugins.splice(redoIndex + 1, 0, createBlockFormatPlugin(headings));
2768
+ }
2769
+ else {
2770
+ allPlugins.push(createBlockFormatPlugin(headings));
2771
+ }
2772
+ }
2773
+ }
2774
+ if (fontSizes && fontSizes.length > 0) {
2775
+ const blockFormatIndex = allPlugins.findIndex((p) => p.name === "blockFormat");
2776
+ if (blockFormatIndex !== -1) {
2777
+ allPlugins.splice(blockFormatIndex + 1, 0, createFontSizePlugin(fontSizes));
2778
+ }
2779
+ else {
2780
+ allPlugins.push(createFontSizePlugin(fontSizes));
2781
+ }
2782
+ }
2783
+ if (colors && colors.length > 0) {
2784
+ allPlugins.push(createTextColorPlugin(colors));
2785
+ allPlugins.push(createBackgroundColorPlugin(colors));
2786
+ }
2787
+ allPlugins.push(createImagePlugin(onImageUpload));
2788
+ return allPlugins;
2789
+ }, [providedPlugins, fontSizes, colors, headings, onImageUpload]);
2790
+ // --- Callbacks ---
2791
+ const notifyChange = useCallback((content) => {
2792
+ if (onChange)
2793
+ onChange(content);
2794
+ }, [onChange]);
2795
+ const getDomContent = useCallback(() => {
2796
+ const editor = editorRef.current;
2797
+ if (!editor)
2798
+ return createEmptyContent();
2799
+ return domToContent(editor);
2800
+ }, []);
2801
+ const pushToHistory = useCallback((content) => {
2802
+ historyRef.current.push(content);
2803
+ }, []);
2804
+ // --- Hooks ---
2805
+ const { restoreSelection } = useEditorSelection();
2806
+ const checkbox = useCheckbox({
2807
+ editorRef,
2808
+ isUpdatingRef,
2809
+ pushToHistory,
2810
+ notifyChange,
2811
+ getDomContent,
2812
+ });
2813
+ // --- Undo / Redo ---
2814
+ const undo = useCallback(() => {
2815
+ const content = historyRef.current.undo();
2816
+ const editor = editorRef.current;
2817
+ if (content && editor) {
2818
+ isUpdatingRef.current = true;
2819
+ contentToDOM(content, editor, customLinkComponent);
2820
+ restoreSelection(editor);
2821
+ isUpdatingRef.current = false;
2822
+ notifyChange(content);
2823
+ }
2824
+ }, [
2825
+ customLinkComponent,
2826
+ customHeadingRenderer,
2827
+ restoreSelection,
2828
+ notifyChange,
2829
+ ]);
2830
+ const redo = useCallback(() => {
2831
+ const content = historyRef.current.redo();
2832
+ const editor = editorRef.current;
2833
+ if (content && editor) {
2834
+ isUpdatingRef.current = true;
2835
+ contentToDOM(content, editor, customLinkComponent);
2836
+ restoreSelection(editor);
2837
+ isUpdatingRef.current = false;
2838
+ notifyChange(content);
2839
+ }
2840
+ }, [
2841
+ customLinkComponent,
2842
+ customHeadingRenderer,
2843
+ restoreSelection,
2844
+ notifyChange,
2845
+ ]);
2846
+ // --- Editor API ---
2847
+ const editorAPI = useMemo(() => {
2848
+ const executeCommand = (command, value) => {
2849
+ const editor = editorRef.current;
2850
+ if (!editor)
2851
+ return false;
2852
+ // Save history before non-history commands
2853
+ if (command !== "undo" &&
2854
+ command !== "redo" &&
2855
+ command !== "insertImage" &&
2856
+ command !== "insertCheckboxList") {
2857
+ const currentContent = domToContent(editor);
2858
+ historyRef.current.push(currentContent);
2859
+ }
2860
+ if (command === "undo") {
2861
+ undo();
2862
+ return true;
2863
+ }
2864
+ if (command === "redo") {
2865
+ redo();
2866
+ return true;
2867
+ }
2868
+ if (command === "insertCheckboxList") {
2869
+ return checkbox.insertCheckboxList(editor);
2870
+ }
2871
+ if (command === "insertImage" && value) {
2872
+ return handleInsertImage(editor, value, isUpdatingRef, historyRef, notifyChange);
2873
+ }
2874
+ // General commands via document.execCommand
2875
+ ensureEditorFocused(editor);
2876
+ document.execCommand(command, false, value);
2877
+ setTimeout(() => {
2878
+ if (editor && !isUpdatingRef.current) {
2879
+ ensureAllCheckboxes(editor);
2880
+ const content = domToContent(editor);
2881
+ notifyChange(content);
2882
+ }
2883
+ }, 0);
2884
+ return true;
2885
+ };
2886
+ return {
2887
+ executeCommand,
2888
+ getSelection: () => {
2889
+ if (typeof window === "undefined")
2890
+ return null;
1729
2891
  return window.getSelection();
1730
2892
  },
1731
2893
  getContent: () => {
1732
2894
  const editor = editorRef.current;
1733
2895
  if (!editor)
1734
2896
  return createEmptyContent();
2897
+ ensureAllCheckboxes(editor);
1735
2898
  return domToContent(editor);
1736
2899
  },
1737
2900
  setContent: (content) => {
@@ -1751,23 +2914,20 @@ const Editor = ({ initialContent, onChange, plugins: providedPlugins, placeholde
1751
2914
  const range = selection.getRangeAt(0);
1752
2915
  const block = document.createElement(type);
1753
2916
  if (attributes) {
1754
- Object.entries(attributes).forEach(([key, value]) => {
1755
- block.setAttribute(key, value);
2917
+ Object.entries(attributes).forEach(([key, val]) => {
2918
+ block.setAttribute(key, val);
1756
2919
  });
1757
2920
  }
1758
2921
  range.insertNode(block);
1759
- const textNode = document.createTextNode("\u200B"); // Zero-width space
2922
+ const textNode = document.createTextNode("\u200B");
1760
2923
  block.appendChild(textNode);
1761
- // Cursor setzen
1762
2924
  range.setStartAfter(textNode);
1763
2925
  range.collapse(true);
1764
2926
  selection.removeAllRanges();
1765
2927
  selection.addRange(range);
1766
2928
  const editor = editorRef.current;
1767
- if (editor) {
1768
- const content = domToContent(editor);
1769
- notifyChange(content);
1770
- }
2929
+ if (editor)
2930
+ notifyChange(domToContent(editor));
1771
2931
  },
1772
2932
  insertInline: (type, attributes) => {
1773
2933
  const selection = window.getSelection();
@@ -1776,282 +2936,120 @@ const Editor = ({ initialContent, onChange, plugins: providedPlugins, placeholde
1776
2936
  const range = selection.getRangeAt(0);
1777
2937
  const inline = document.createElement(type);
1778
2938
  if (attributes) {
1779
- Object.entries(attributes).forEach(([key, value]) => {
1780
- inline.setAttribute(key, value);
2939
+ Object.entries(attributes).forEach(([key, val]) => {
2940
+ inline.setAttribute(key, val);
1781
2941
  });
1782
2942
  }
1783
2943
  try {
1784
2944
  range.surroundContents(inline);
1785
2945
  }
1786
- catch (e) {
1787
- // Falls surroundContents fehlschlägt, versuche es anders
2946
+ catch (_) {
1788
2947
  const contents = range.extractContents();
1789
2948
  inline.appendChild(contents);
1790
2949
  range.insertNode(inline);
1791
2950
  }
1792
- // Cursor setzen
1793
- range.setStartAfter(inline);
1794
- range.collapse(true);
1795
- selection.removeAllRanges();
1796
- selection.addRange(range);
1797
- const editor = editorRef.current;
1798
- if (editor) {
1799
- const content = domToContent(editor);
1800
- notifyChange(content);
1801
- }
1802
- },
1803
- undo: () => {
1804
- executeCommand("undo");
1805
- },
1806
- redo: () => {
1807
- executeCommand("redo");
1808
- },
1809
- canUndo: () => {
1810
- return historyRef.current.canUndo();
1811
- },
1812
- canRedo: () => {
1813
- return historyRef.current.canRedo();
1814
- },
1815
- importHtml: (htmlString) => {
1816
- const content = htmlToContent(htmlString);
1817
- const editor = editorRef.current;
1818
- if (editor) {
1819
- isUpdatingRef.current = true;
1820
- contentToDOM(content, editor, customLinkComponent);
1821
- historyRef.current.push(content);
1822
- isUpdatingRef.current = false;
1823
- notifyChange(content);
1824
- }
1825
- return content;
1826
- },
1827
- exportHtml: () => {
1828
- const editor = editorRef.current;
1829
- if (!editor)
1830
- return "";
1831
- const content = domToContent(editor);
1832
- return contentToHTML(content);
1833
- },
1834
- clearFormatting: () => {
1835
- const editor = editorRef.current;
1836
- if (!editor)
1837
- return;
1838
- const selection = window.getSelection();
1839
- if (selection && selection.rangeCount > 0) {
1840
- const currentContent = domToContent(editor);
1841
- historyRef.current.push(currentContent);
1842
- clearFormatting(selection);
1843
- setTimeout(() => {
1844
- if (editor) {
1845
- const content = domToContent(editor);
1846
- notifyChange(content);
1847
- }
1848
- }, 0);
1849
- }
1850
- },
1851
- clearTextColor: () => {
1852
- const editor = editorRef.current;
1853
- if (!editor)
1854
- return;
1855
- const selection = window.getSelection();
1856
- if (selection && selection.rangeCount > 0) {
1857
- const currentContent = domToContent(editor);
1858
- historyRef.current.push(currentContent);
1859
- clearTextColor(selection);
1860
- setTimeout(() => {
1861
- if (editor) {
1862
- const content = domToContent(editor);
1863
- notifyChange(content);
1864
- }
1865
- }, 0);
1866
- }
1867
- },
1868
- clearBackgroundColor: () => {
1869
- const editor = editorRef.current;
1870
- if (!editor)
1871
- return;
1872
- const selection = window.getSelection();
1873
- if (selection && selection.rangeCount > 0) {
1874
- const currentContent = domToContent(editor);
1875
- historyRef.current.push(currentContent);
1876
- clearBackgroundColor(selection);
1877
- setTimeout(() => {
1878
- if (editor) {
1879
- const content = domToContent(editor);
1880
- notifyChange(content);
1881
- }
1882
- }, 0);
1883
- }
1884
- },
1885
- clearFontSize: () => {
1886
- const editor = editorRef.current;
1887
- if (!editor)
1888
- return;
1889
- const selection = window.getSelection();
1890
- if (selection && selection.rangeCount > 0) {
1891
- const currentContent = domToContent(editor);
1892
- historyRef.current.push(currentContent);
1893
- clearFontSize(selection);
1894
- setTimeout(() => {
1895
- if (editor) {
1896
- const content = domToContent(editor);
1897
- notifyChange(content);
1898
- }
1899
- }, 0);
1900
- }
1901
- },
1902
- clearLinks: () => {
1903
- const editor = editorRef.current;
1904
- if (!editor)
1905
- return;
1906
- const selection = window.getSelection();
1907
- if (selection && selection.rangeCount > 0) {
1908
- const currentContent = domToContent(editor);
1909
- historyRef.current.push(currentContent);
1910
- clearLinks(selection);
1911
- setTimeout(() => {
1912
- if (editor) {
1913
- const content = domToContent(editor);
1914
- notifyChange(content);
1915
- }
1916
- }, 0);
1917
- }
1918
- },
1919
- indentListItem: () => {
1920
- const editor = editorRef.current;
1921
- if (!editor)
1922
- return;
1923
- const selection = window.getSelection();
1924
- if (selection && selection.rangeCount > 0) {
1925
- const currentContent = domToContent(editor);
1926
- historyRef.current.push(currentContent);
1927
- indentListItem(selection);
1928
- setTimeout(() => {
1929
- if (editor) {
1930
- const content = domToContent(editor);
1931
- notifyChange(content);
1932
- }
1933
- }, 0);
1934
- }
1935
- },
1936
- outdentListItem: () => {
1937
- const editor = editorRef.current;
1938
- if (!editor)
1939
- return;
1940
- const selection = window.getSelection();
1941
- if (selection && selection.rangeCount > 0) {
1942
- const currentContent = domToContent(editor);
1943
- historyRef.current.push(currentContent);
1944
- outdentListItem(selection);
1945
- setTimeout(() => {
1946
- if (editor) {
1947
- const content = domToContent(editor);
1948
- notifyChange(content);
1949
- }
1950
- }, 0);
1951
- }
1952
- },
1953
- };
1954
- }, [
1955
- notifyChange,
1956
- restoreSelection,
1957
- customLinkComponent,
1958
- customHeadingRenderer,
1959
- ]);
1960
- useEffect(() => {
1961
- if (onEditorAPIReady) {
1962
- onEditorAPIReady(editorAPI);
1963
- }
1964
- }, [editorAPI, onEditorAPIReady]);
1965
- const isInitializedRef = useRef(false);
1966
- useEffect(() => {
1967
- const editor = editorRef.current;
1968
- if (!editor || isInitializedRef.current)
1969
- return;
1970
- const content = initialContent || createEmptyContent();
1971
- isUpdatingRef.current = true;
1972
- contentToDOM(content, editor, customLinkComponent);
1973
- historyRef.current.push(content);
1974
- isUpdatingRef.current = false;
1975
- isInitializedRef.current = true;
1976
- let inputTimeout = null;
1977
- const handleInput = () => {
1978
- if (isUpdatingRef.current)
1979
- return;
1980
- const content = domToContent(editor);
1981
- notifyChange(content);
1982
- if (inputTimeout) {
1983
- clearTimeout(inputTimeout);
1984
- }
1985
- inputTimeout = setTimeout(() => {
1986
- historyRef.current.push(content);
1987
- inputTimeout = null;
1988
- }, 300);
1989
- };
1990
- const handleKeyDown = (e) => {
1991
- const isModifierPressed = e.metaKey || e.ctrlKey;
1992
- if (e.key === "Tab" && !isModifierPressed && !e.altKey) {
1993
- // Immer preventDefault aufrufen (wie Lexical), damit Tab den Fokus nicht aus dem Editor entfernt
1994
- e.preventDefault();
1995
- e.stopPropagation();
1996
- e.stopImmediatePropagation();
1997
- const selection = window.getSelection();
1998
- if (!selection || selection.rangeCount === 0) {
1999
- // Keine Selection: Tab verhindern, Fokus bleibt im Editor
2000
- return;
2001
- }
2002
- const range = selection.getRangeAt(0);
2003
- const container = range.commonAncestorContainer;
2004
- if (!editor.contains(container)) {
2005
- // Container nicht im Editor: Tab verhindern
2006
- return;
2007
- }
2008
- // Prüfe ob wir in einer Liste sind
2009
- const listItem = container.nodeType === Node.TEXT_NODE
2010
- ? container.parentElement?.closest("li")
2011
- : container.closest("li");
2012
- if (listItem && editor.contains(listItem)) {
2013
- // In Liste: Indent/Outdent durchführen
2014
- const currentContent = domToContent(editor);
2015
- historyRef.current.push(currentContent);
2016
- if (e.shiftKey) {
2017
- outdentListItem(selection);
2018
- }
2019
- else {
2020
- indentListItem(selection);
2021
- }
2022
- setTimeout(() => {
2023
- if (editor) {
2024
- const content = domToContent(editor);
2025
- notifyChange(content);
2026
- }
2027
- }, 0);
2028
- return;
2029
- }
2030
- // Nicht in Liste: Tab verhindern, aber kein Tab-Zeichen einfügen
2031
- // Der Fokus bleibt im Editor (durch preventDefault)
2032
- }
2033
- if (isModifierPressed && e.key === "z" && !e.shiftKey) {
2034
- e.preventDefault();
2035
- e.stopPropagation();
2036
- editorAPI.undo();
2037
- }
2038
- else if (isModifierPressed &&
2039
- (e.key === "y" || (e.key === "z" && e.shiftKey))) {
2040
- e.preventDefault();
2041
- e.stopPropagation();
2042
- editorAPI.redo();
2043
- }
2044
- };
2045
- editor.addEventListener("input", handleInput);
2046
- editor.addEventListener("keydown", handleKeyDown, true);
2047
- return () => {
2048
- editor.removeEventListener("input", handleInput);
2049
- editor.removeEventListener("keydown", handleKeyDown, true);
2050
- if (inputTimeout) {
2051
- clearTimeout(inputTimeout);
2052
- }
2951
+ range.setStartAfter(inline);
2952
+ range.collapse(true);
2953
+ selection.removeAllRanges();
2954
+ selection.addRange(range);
2955
+ const editor = editorRef.current;
2956
+ if (editor)
2957
+ notifyChange(domToContent(editor));
2958
+ },
2959
+ undo: () => undo(),
2960
+ redo: () => redo(),
2961
+ canUndo: () => historyRef.current.canUndo(),
2962
+ canRedo: () => historyRef.current.canRedo(),
2963
+ importHtml: (htmlString) => {
2964
+ const content = htmlToContent(htmlString);
2965
+ const editor = editorRef.current;
2966
+ if (editor) {
2967
+ isUpdatingRef.current = true;
2968
+ contentToDOM(content, editor, customLinkComponent);
2969
+ historyRef.current.push(content);
2970
+ isUpdatingRef.current = false;
2971
+ notifyChange(content);
2972
+ }
2973
+ return content;
2974
+ },
2975
+ exportHtml: () => {
2976
+ const editor = editorRef.current;
2977
+ if (!editor)
2978
+ return "";
2979
+ return contentToHTML(domToContent(editor));
2980
+ },
2981
+ clearFormatting: () => {
2982
+ executeWithHistory((selection) => clearFormatting(selection));
2983
+ },
2984
+ clearTextColor: () => {
2985
+ executeWithHistory((selection) => clearTextColor(selection));
2986
+ },
2987
+ clearBackgroundColor: () => {
2988
+ executeWithHistory((selection) => clearBackgroundColor(selection));
2989
+ },
2990
+ clearFontSize: () => {
2991
+ executeWithHistory((selection) => clearFontSize(selection));
2992
+ },
2993
+ clearLinks: () => {
2994
+ executeWithHistory((selection) => clearLinks(selection));
2995
+ },
2996
+ indentListItem: () => {
2997
+ executeWithHistory((selection) => indentListItem(selection));
2998
+ },
2999
+ outdentListItem: () => {
3000
+ executeWithHistory((selection) => outdentListItem(selection));
3001
+ },
2053
3002
  };
2054
- }, [editorAPI, notifyChange]);
3003
+ /** Helper: push history, execute operation, then notify change. */
3004
+ function executeWithHistory(operation) {
3005
+ const editor = editorRef.current;
3006
+ if (!editor)
3007
+ return;
3008
+ const selection = window.getSelection();
3009
+ if (!selection || selection.rangeCount === 0)
3010
+ return;
3011
+ const currentContent = domToContent(editor);
3012
+ historyRef.current.push(currentContent);
3013
+ operation(selection);
3014
+ setTimeout(() => {
3015
+ if (editor)
3016
+ notifyChange(domToContent(editor));
3017
+ }, 0);
3018
+ }
3019
+ }, [
3020
+ undo,
3021
+ redo,
3022
+ checkbox,
3023
+ notifyChange,
3024
+ customLinkComponent,
3025
+ customHeadingRenderer,
3026
+ ]);
3027
+ // --- Initialize editor ---
3028
+ useEditorInit({
3029
+ editorRef,
3030
+ historyRef,
3031
+ isUpdatingRef,
3032
+ initialContent,
3033
+ customLinkComponent,
3034
+ customHeadingRenderer,
3035
+ });
3036
+ // --- Set up event listeners ---
3037
+ useEditorEvents({
3038
+ editorRef,
3039
+ historyRef,
3040
+ isUpdatingRef,
3041
+ notifyChange,
3042
+ handleCheckboxKeyDown: checkbox.handleCheckboxKeyDown,
3043
+ handleCheckboxEnter: checkbox.handleCheckboxEnter,
3044
+ undo,
3045
+ redo,
3046
+ });
3047
+ // --- Expose editor API ---
3048
+ useEffect(() => {
3049
+ if (onEditorAPIReady)
3050
+ onEditorAPIReady(editorAPI);
3051
+ }, [editorAPI, onEditorAPIReady]);
3052
+ // --- Paste handler ---
2055
3053
  const handlePaste = (e) => {
2056
3054
  e.preventDefault();
2057
3055
  const html = e.clipboardData.getData("text/html");
@@ -2079,11 +3077,10 @@ const Editor = ({ initialContent, onChange, plugins: providedPlugins, placeholde
2079
3077
  }
2080
3078
  selection.removeAllRanges();
2081
3079
  selection.addRange(range);
2082
- const content = domToContent(editor);
2083
- notifyChange(content);
3080
+ notifyChange(domToContent(editor));
2084
3081
  }
2085
3082
  }
2086
- catch (error) {
3083
+ catch (_) {
2087
3084
  document.execCommand("insertText", false, text);
2088
3085
  }
2089
3086
  }
@@ -2091,6 +3088,7 @@ const Editor = ({ initialContent, onChange, plugins: providedPlugins, placeholde
2091
3088
  document.execCommand("insertText", false, text);
2092
3089
  }
2093
3090
  };
3091
+ // --- Theme styles ---
2094
3092
  const containerStyle = theme
2095
3093
  ? {
2096
3094
  ...(theme.borderColor &&
@@ -2121,6 +3119,131 @@ const Editor = ({ initialContent, onChange, plugins: providedPlugins, placeholde
2121
3119
  : {};
2122
3120
  return (jsxs("div", { className: `rte-container ${className || ""}`, style: containerStyle, children: [jsx(Toolbar, { plugins: plugins, editorAPI: editorAPI, className: toolbarClassName }), jsx("div", { ref: editorRef, contentEditable: true, className: `rte-editor ${editorClassName || ""}`, "data-placeholder": placeholder, onPaste: handlePaste, suppressContentEditableWarning: true })] }));
2123
3121
  };
3122
+ // --- Helper: Insert Image ---
3123
+ function handleInsertImage(editor, value, isUpdatingRef, historyRef, notifyChange) {
3124
+ let selection = window.getSelection();
3125
+ if (!selection)
3126
+ return false;
3127
+ if (document.activeElement !== editor) {
3128
+ editor.focus();
3129
+ }
3130
+ if (selection.rangeCount === 0) {
3131
+ const range = document.createRange();
3132
+ if (editor.childNodes.length > 0) {
3133
+ const lastChild = editor.childNodes[editor.childNodes.length - 1];
3134
+ range.setStartAfter(lastChild);
3135
+ range.collapse(true);
3136
+ }
3137
+ else {
3138
+ const img = createImageElement(value);
3139
+ editor.appendChild(img);
3140
+ const newRange = document.createRange();
3141
+ newRange.setStartAfter(img);
3142
+ newRange.collapse(true);
3143
+ selection.removeAllRanges();
3144
+ selection.addRange(newRange);
3145
+ saveAndNotify(editor, isUpdatingRef, historyRef, notifyChange);
3146
+ return true;
3147
+ }
3148
+ selection.removeAllRanges();
3149
+ selection.addRange(range);
3150
+ }
3151
+ if (selection.rangeCount === 0)
3152
+ return false;
3153
+ const range = selection.getRangeAt(0);
3154
+ const container = range.commonAncestorContainer;
3155
+ let parentElement = null;
3156
+ if (container.nodeType === Node.TEXT_NODE) {
3157
+ parentElement = container.parentElement;
3158
+ }
3159
+ else if (container.nodeType === Node.ELEMENT_NODE) {
3160
+ parentElement = container;
3161
+ }
3162
+ const img = createImageElement(value);
3163
+ if (parentElement &&
3164
+ parentElement !== editor &&
3165
+ /^(P|DIV|H[1-6])$/.test(parentElement.tagName)) {
3166
+ if (parentElement.nextSibling) {
3167
+ editor.insertBefore(img, parentElement.nextSibling);
3168
+ }
3169
+ else {
3170
+ editor.appendChild(img);
3171
+ }
3172
+ }
3173
+ else {
3174
+ try {
3175
+ range.insertNode(img);
3176
+ }
3177
+ catch (_) {
3178
+ editor.appendChild(img);
3179
+ }
3180
+ }
3181
+ const newRange = document.createRange();
3182
+ newRange.setStartAfter(img);
3183
+ newRange.collapse(true);
3184
+ selection.removeAllRanges();
3185
+ selection.addRange(newRange);
3186
+ saveAndNotify(editor, isUpdatingRef, historyRef, notifyChange);
3187
+ return true;
3188
+ }
3189
+ function createImageElement(src) {
3190
+ const img = document.createElement("img");
3191
+ img.setAttribute("src", src);
3192
+ img.setAttribute("alt", "");
3193
+ img.style.maxWidth = "100%";
3194
+ img.style.height = "auto";
3195
+ img.style.display = "block";
3196
+ img.style.margin = "16px 0";
3197
+ return img;
3198
+ }
3199
+ function saveAndNotify(editor, isUpdatingRef, historyRef, notifyChange) {
3200
+ isUpdatingRef.current = true;
3201
+ setTimeout(() => {
3202
+ const content = domToContent(editor);
3203
+ historyRef.current.push(content);
3204
+ isUpdatingRef.current = false;
3205
+ notifyChange(content);
3206
+ }, 0);
3207
+ }
3208
+ /** Ensures the editor is focused and has a valid selection. */
3209
+ function ensureEditorFocused(editor) {
3210
+ const selection = window.getSelection();
3211
+ let savedRange = null;
3212
+ if (selection && selection.rangeCount > 0) {
3213
+ savedRange = selection.getRangeAt(0).cloneRange();
3214
+ }
3215
+ if (document.activeElement !== editor) {
3216
+ editor.focus();
3217
+ }
3218
+ if (!selection || selection.rangeCount === 0) {
3219
+ const range = document.createRange();
3220
+ if (editor.childNodes.length > 0) {
3221
+ const lastChild = editor.childNodes[editor.childNodes.length - 1];
3222
+ if (lastChild.nodeType === Node.TEXT_NODE) {
3223
+ range.setStart(lastChild, lastChild.textContent?.length || 0);
3224
+ range.setEnd(lastChild, lastChild.textContent?.length || 0);
3225
+ }
3226
+ else {
3227
+ range.selectNodeContents(lastChild);
3228
+ range.collapse(false);
3229
+ }
3230
+ }
3231
+ else {
3232
+ const p = document.createElement("p");
3233
+ editor.appendChild(p);
3234
+ const textNode = document.createTextNode("");
3235
+ p.appendChild(textNode);
3236
+ range.setStart(textNode, 0);
3237
+ range.setEnd(textNode, 0);
3238
+ }
3239
+ selection?.removeAllRanges();
3240
+ selection?.addRange(range);
3241
+ }
3242
+ else if (savedRange) {
3243
+ selection.removeAllRanges();
3244
+ selection.addRange(savedRange);
3245
+ }
3246
+ }
2124
3247
 
2125
3248
  const defaultHeadings = ["h1", "h2", "h3"];
2126
3249
  const headingLabels = {
@@ -2349,5 +3472,465 @@ const orderedListPlugin = {
2349
3472
  },
2350
3473
  };
2351
3474
 
2352
- export { Dropdown, Editor, HistoryManager, Toolbar, blockquotePlugin, boldPlugin, clearFormattingPlugin, contentToDOM, contentToHTML, createBackgroundColorPlugin, createBlockFormatPlugin, createEmptyContent, createFontSizePlugin, createHeadingsPlugin, createImagePlugin, createLinkPlugin, createTextColorPlugin, Editor as default, defaultPlugins, domToContent, getCurrentBackgroundColor, getCurrentFontSize, getCurrentHeading, getCurrentTextColor, htmlToContent, indentListItem, indentListItemPlugin, italicPlugin, linkPlugin, orderedListPlugin, outdentListItem, outdentListItemPlugin, redoPlugin, underlinePlugin, undoPlugin, unorderedListPlugin };
3475
+ const alignmentLabels = {
3476
+ left: "Links",
3477
+ center: "Zentriert",
3478
+ right: "Rechts",
3479
+ justify: "Blocksatz",
3480
+ };
3481
+ const alignmentIcons = {
3482
+ left: "mdi:format-align-left",
3483
+ center: "mdi:format-align-center",
3484
+ right: "mdi:format-align-right",
3485
+ justify: "mdi:format-align-justify",
3486
+ };
3487
+ const alignmentCommands = {
3488
+ left: "justifyLeft",
3489
+ center: "justifyCenter",
3490
+ right: "justifyRight",
3491
+ justify: "justifyFull",
3492
+ };
3493
+ /**
3494
+ * Detects current text alignment at cursor position.
3495
+ */
3496
+ function detectCurrentAlignment() {
3497
+ if (typeof document === "undefined")
3498
+ return undefined;
3499
+ const selection = document.getSelection();
3500
+ if (!selection || selection.rangeCount === 0)
3501
+ return undefined;
3502
+ const range = selection.getRangeAt(0);
3503
+ const container = range.commonAncestorContainer;
3504
+ const element = container.nodeType === Node.TEXT_NODE
3505
+ ? container.parentElement
3506
+ : container;
3507
+ if (!element)
3508
+ return undefined;
3509
+ // Walk up to find the block-level element
3510
+ let block = element;
3511
+ const blockTags = new Set([
3512
+ "P",
3513
+ "DIV",
3514
+ "H1",
3515
+ "H2",
3516
+ "H3",
3517
+ "H4",
3518
+ "H5",
3519
+ "H6",
3520
+ "LI",
3521
+ "BLOCKQUOTE",
3522
+ "TD",
3523
+ "TH",
3524
+ ]);
3525
+ while (block && !blockTags.has(block.tagName)) {
3526
+ block = block.parentElement;
3527
+ }
3528
+ if (!block)
3529
+ return undefined;
3530
+ const align = block.style.textAlign || window.getComputedStyle(block).textAlign;
3531
+ if (align === "center")
3532
+ return "center";
3533
+ if (align === "right")
3534
+ return "right";
3535
+ if (align === "justify")
3536
+ return "justify";
3537
+ return "left";
3538
+ }
3539
+ /**
3540
+ * Creates an alignment plugin with a dropdown.
3541
+ * @param alignments - Which alignments to offer, defaults to all four.
3542
+ */
3543
+ function createAlignmentPlugin(alignments = ["left", "center", "right", "justify"]) {
3544
+ const options = alignments.map((a) => ({
3545
+ value: a,
3546
+ label: alignmentLabels[a] || a,
3547
+ icon: alignmentIcons[a],
3548
+ }));
3549
+ return {
3550
+ name: "alignment",
3551
+ type: "block",
3552
+ renderButton: (props) => {
3553
+ const onSelect = props.onSelect;
3554
+ const currentValue = props.currentValue ||
3555
+ detectCurrentAlignment() ||
3556
+ "left";
3557
+ return (jsx(Dropdown, { icon: alignmentIcons[currentValue] || "mdi:format-align-left", label: "Ausrichtung", options: options, onSelect: (value) => {
3558
+ if (onSelect)
3559
+ onSelect(value);
3560
+ }, currentValue: currentValue, disabled: props.disabled }));
3561
+ },
3562
+ getCurrentValue: () => detectCurrentAlignment(),
3563
+ execute: (_editor, value) => {
3564
+ if (!value)
3565
+ return;
3566
+ const command = alignmentCommands[value];
3567
+ if (command) {
3568
+ document.execCommand(command, false);
3569
+ }
3570
+ },
3571
+ isActive: () => {
3572
+ const align = detectCurrentAlignment();
3573
+ return align !== undefined && align !== "left";
3574
+ },
3575
+ canExecute: () => true,
3576
+ };
3577
+ }
3578
+ /** Pre-built alignment plugin with all four options */
3579
+ const alignmentPlugin = createAlignmentPlugin();
3580
+
3581
+ const InsertTableDialog = ({ onInsert, onClose, }) => {
3582
+ const [rows, setRows] = useState(3);
3583
+ const [cols, setCols] = useState(3);
3584
+ const dialogRef = useRef(null);
3585
+ useEffect(() => {
3586
+ const handler = (e) => {
3587
+ if (dialogRef.current &&
3588
+ !dialogRef.current.contains(e.target)) {
3589
+ onClose();
3590
+ }
3591
+ };
3592
+ document.addEventListener("mousedown", handler);
3593
+ return () => document.removeEventListener("mousedown", handler);
3594
+ }, [onClose]);
3595
+ return (jsxs("div", { className: "rte-table-insert-dialog", ref: dialogRef, children: [jsx("div", { className: "rte-table-insert-title", children: "Tabelle einf\u00FCgen" }), jsxs("div", { className: "rte-table-insert-fields", children: [jsxs("label", { className: "rte-table-insert-label", children: [jsx("span", { children: "Zeilen" }), jsx("input", { type: "number", min: 1, max: 20, value: rows, onChange: (e) => setRows(Math.max(1, Math.min(20, parseInt(e.target.value) || 1))), className: "rte-table-insert-input" })] }), jsxs("label", { className: "rte-table-insert-label", children: [jsx("span", { children: "Spalten" }), jsx("input", { type: "number", min: 1, max: 10, value: cols, onChange: (e) => setCols(Math.max(1, Math.min(10, parseInt(e.target.value) || 1))), className: "rte-table-insert-input" })] })] }), jsx("button", { type: "button", className: "rte-table-insert-btn", onClick: () => onInsert(rows, cols), children: "Einf\u00FCgen" })] }));
3596
+ };
3597
+ const TableContextMenu = ({ x, y, onClose, }) => {
3598
+ const ref = useRef(null);
3599
+ useEffect(() => {
3600
+ const handler = (e) => {
3601
+ if (ref.current && !ref.current.contains(e.target)) {
3602
+ onClose();
3603
+ }
3604
+ };
3605
+ document.addEventListener("mousedown", handler);
3606
+ return () => document.removeEventListener("mousedown", handler);
3607
+ }, [onClose]);
3608
+ const action = (fn) => {
3609
+ fn();
3610
+ onClose();
3611
+ };
3612
+ return (jsxs("div", { ref: ref, className: "rte-table-context-menu", style: { position: "fixed", left: x, top: y }, children: [jsx("button", { type: "button", onClick: () => action(() => insertRow("above")), children: "Zeile oben einf\u00FCgen" }), jsx("button", { type: "button", onClick: () => action(() => insertRow("below")), children: "Zeile unten einf\u00FCgen" }), jsx("div", { className: "rte-table-context-divider" }), jsx("button", { type: "button", onClick: () => action(() => insertColumn("left")), children: "Spalte links einf\u00FCgen" }), jsx("button", { type: "button", onClick: () => action(() => insertColumn("right")), children: "Spalte rechts einf\u00FCgen" }), jsx("div", { className: "rte-table-context-divider" }), jsx("button", { type: "button", className: "rte-table-context-danger", onClick: () => action(deleteRow), children: "Zeile l\u00F6schen" }), jsx("button", { type: "button", className: "rte-table-context-danger", onClick: () => action(deleteColumn), children: "Spalte l\u00F6schen" }), jsx("button", { type: "button", className: "rte-table-context-danger", onClick: () => action(deleteTable), children: "Tabelle l\u00F6schen" })] }));
3613
+ };
3614
+ const TableToolbarButton = (props) => {
3615
+ const [showDialog, setShowDialog] = useState(false);
3616
+ const handleInsert = useCallback((rows, cols) => {
3617
+ setShowDialog(false);
3618
+ if (!props.editorAPI)
3619
+ return;
3620
+ const sel = document.getSelection();
3621
+ if (!sel || sel.rangeCount === 0)
3622
+ return;
3623
+ const range = sel.getRangeAt(0);
3624
+ // Find the editor's contentEditable root
3625
+ const container = range.commonAncestorContainer;
3626
+ const editorEl = container.nodeType === Node.TEXT_NODE
3627
+ ? container.parentElement
3628
+ : container;
3629
+ const editorRoot = editorEl?.closest("[contenteditable]");
3630
+ if (!editorRoot)
3631
+ return;
3632
+ const table = createTable(rows, cols);
3633
+ // Insert after the current block element
3634
+ let block = editorEl;
3635
+ while (block &&
3636
+ block !== editorRoot &&
3637
+ block.parentElement !== editorRoot) {
3638
+ block = block.parentElement;
3639
+ }
3640
+ if (block && block !== editorRoot) {
3641
+ block.parentNode?.insertBefore(table, block.nextSibling);
3642
+ }
3643
+ else {
3644
+ editorRoot.appendChild(table);
3645
+ }
3646
+ // Add a paragraph after the table so the user can continue typing
3647
+ const p = document.createElement("p");
3648
+ p.innerHTML = "<br>";
3649
+ table.parentNode?.insertBefore(p, table.nextSibling);
3650
+ // Focus the first cell
3651
+ const firstCell = table.querySelector("td, th");
3652
+ if (firstCell) {
3653
+ const newRange = document.createRange();
3654
+ newRange.setStart(firstCell, 0);
3655
+ newRange.collapse(true);
3656
+ sel.removeAllRanges();
3657
+ sel.addRange(newRange);
3658
+ }
3659
+ }, [props.editorAPI]);
3660
+ return (jsxs("div", { style: { position: "relative" }, children: [jsx("button", { type: "button", onClick: () => setShowDialog(!showDialog), disabled: props.disabled, className: `rte-toolbar-button ${props.isActive ? "rte-toolbar-button-active" : ""}`, title: "Tabelle", "aria-label": "Tabelle", children: jsx(IconWrapper, { icon: "mdi:table", width: 18, height: 18 }) }), showDialog && (jsx(InsertTableDialog, { onInsert: handleInsert, onClose: () => setShowDialog(false) }))] }));
3661
+ };
3662
+ /* ══════════════════════════════════════════════════════════════════════════
3663
+ Table Plugin export
3664
+ ══════════════════════════════════════════════════════════════════════ */
3665
+ const tablePlugin = {
3666
+ name: "table",
3667
+ type: "command",
3668
+ renderButton: (props) => (jsx(TableToolbarButton, { ...props, editorAPI: props.editorAPI })),
3669
+ execute: () => {
3670
+ // Insertion is handled by the dialog component
3671
+ },
3672
+ isActive: () => getActiveTable() !== null,
3673
+ canExecute: () => true,
3674
+ };
3675
+ /* ══════════════════════════════════════════════════════════════════════════
3676
+ TableContextMenuProvider — wrap the editor to enable right-click menu
3677
+ ══════════════════════════════════════════════════════════════════════ */
3678
+ const TableContextMenuProvider = ({ children }) => {
3679
+ const [menu, setMenu] = useState(null);
3680
+ useEffect(() => {
3681
+ const handler = (e) => {
3682
+ const target = e.target;
3683
+ const cell = target.closest("td, th");
3684
+ const editorRoot = target.closest("[contenteditable]");
3685
+ if (cell && editorRoot) {
3686
+ e.preventDefault();
3687
+ setMenu({ x: e.clientX, y: e.clientY });
3688
+ }
3689
+ };
3690
+ document.addEventListener("contextmenu", handler);
3691
+ return () => document.removeEventListener("contextmenu", handler);
3692
+ }, []);
3693
+ return (jsxs(Fragment, { children: [children, menu && (jsx(TableContextMenu, { x: menu.x, y: menu.y, onClose: () => setMenu(null) }))] }));
3694
+ };
3695
+
3696
+ const EMPTY_LINK = {
3697
+ url: "",
3698
+ target: "_self",
3699
+ rel: "",
3700
+ title: "",
3701
+ pageRef: "",
3702
+ urlExtra: "",
3703
+ };
3704
+ const LinkDialog = ({ initialData, options, onSave, onRemove, onClose, isEditing, }) => {
3705
+ const [data, setData] = useState(initialData);
3706
+ const [showAdvanced, setShowAdvanced] = useState(false);
3707
+ const dialogRef = useRef(null);
3708
+ const urlInputRef = useRef(null);
3709
+ useEffect(() => {
3710
+ urlInputRef.current?.focus();
3711
+ }, []);
3712
+ useEffect(() => {
3713
+ const handler = (e) => {
3714
+ if (dialogRef.current &&
3715
+ !dialogRef.current.contains(e.target)) {
3716
+ onClose();
3717
+ }
3718
+ };
3719
+ // Delay to avoid closing immediately on the same click
3720
+ const timer = setTimeout(() => {
3721
+ document.addEventListener("mousedown", handler);
3722
+ }, 50);
3723
+ return () => {
3724
+ clearTimeout(timer);
3725
+ document.removeEventListener("mousedown", handler);
3726
+ };
3727
+ }, [onClose]);
3728
+ const handleKeyDown = (e) => {
3729
+ if (e.key === "Enter") {
3730
+ e.preventDefault();
3731
+ onSave(data);
3732
+ }
3733
+ else if (e.key === "Escape") {
3734
+ e.preventDefault();
3735
+ onClose();
3736
+ }
3737
+ };
3738
+ const set = (key, value) => setData((prev) => ({ ...prev, [key]: value }));
3739
+ const hasAdvancedOptions = options.enableRel ||
3740
+ options.enableTitle ||
3741
+ options.enableUrlExtra ||
3742
+ options.enablePageRef;
3743
+ return (jsxs("div", { ref: dialogRef, className: "rte-link-dialog", onKeyDown: handleKeyDown, children: [jsx("div", { className: "rte-link-dialog-title", children: isEditing ? "Link bearbeiten" : "Link einfügen" }), jsxs("div", { className: "rte-link-dialog-field", children: [jsx("label", { className: "rte-link-dialog-label", children: "URL" }), jsx("input", { ref: urlInputRef, type: "url", className: "rte-link-dialog-input", value: data.url, onChange: (e) => set("url", e.target.value), placeholder: "https://..." })] }), options.enableTarget && (jsxs("div", { className: "rte-link-dialog-field", children: [jsx("label", { className: "rte-link-dialog-label", children: "Ziel" }), jsxs("select", { className: "rte-link-dialog-select", value: data.target, onChange: (e) => set("target", e.target.value), children: [jsx("option", { value: "_self", children: "Gleiches Fenster" }), jsx("option", { value: "_blank", children: "Neues Fenster" })] })] })), hasAdvancedOptions && (jsxs(Fragment, { children: [jsx("button", { type: "button", className: "rte-link-dialog-toggle", onClick: () => setShowAdvanced(!showAdvanced), children: showAdvanced ? "Erweitert ausblenden" : "Erweitert anzeigen" }), showAdvanced && (jsxs("div", { className: "rte-link-dialog-advanced", children: [options.enablePageRef && (jsxs("div", { className: "rte-link-dialog-field", children: [jsx("label", { className: "rte-link-dialog-label", children: "Seitenreferenz" }), jsx("input", { type: "text", className: "rte-link-dialog-input", value: data.pageRef, onChange: (e) => set("pageRef", e.target.value), placeholder: "page-id" })] })), options.enableUrlExtra && (jsxs("div", { className: "rte-link-dialog-field", children: [jsx("label", { className: "rte-link-dialog-label", children: "URL Extra" }), jsx("input", { type: "text", className: "rte-link-dialog-input", value: data.urlExtra, onChange: (e) => set("urlExtra", e.target.value), placeholder: "?param=value oder #anchor" })] })), options.enableRel && (jsxs("div", { className: "rte-link-dialog-field", children: [jsx("label", { className: "rte-link-dialog-label", children: "Rel" }), jsx("input", { type: "text", className: "rte-link-dialog-input", value: data.rel, onChange: (e) => set("rel", e.target.value), placeholder: "noopener noreferrer" })] })), options.enableTitle && (jsxs("div", { className: "rte-link-dialog-field", children: [jsx("label", { className: "rte-link-dialog-label", children: "Titel" }), jsx("input", { type: "text", className: "rte-link-dialog-input", value: data.title, onChange: (e) => set("title", e.target.value), placeholder: "Link-Titel" })] }))] }))] })), jsxs("div", { className: "rte-link-dialog-actions", children: [jsx("button", { type: "button", className: "rte-link-dialog-btn rte-link-dialog-btn-primary", onClick: () => onSave(data), disabled: !data.url.trim(), children: isEditing ? "Speichern" : "Einfügen" }), isEditing && (jsx("button", { type: "button", className: "rte-link-dialog-btn rte-link-dialog-btn-danger", onClick: onRemove, children: "Entfernen" })), jsx("button", { type: "button", className: "rte-link-dialog-btn", onClick: onClose, children: "Abbrechen" })] })] }));
3744
+ };
3745
+ const LinkToolbarButton = (props) => {
3746
+ const [showDialog, setShowDialog] = useState(false);
3747
+ const [linkData, setLinkData] = useState(EMPTY_LINK);
3748
+ const [isEditing, setIsEditing] = useState(false);
3749
+ const savedRangeRef = useRef(null);
3750
+ const openDialog = useCallback(() => {
3751
+ const sel = document.getSelection();
3752
+ if (!sel || sel.rangeCount === 0)
3753
+ return;
3754
+ // Save the current selection
3755
+ savedRangeRef.current = sel.getRangeAt(0).cloneRange();
3756
+ const range = sel.getRangeAt(0);
3757
+ const container = range.commonAncestorContainer;
3758
+ const element = container.nodeType === Node.TEXT_NODE
3759
+ ? container.parentElement
3760
+ : container;
3761
+ const existingLink = element?.closest("a");
3762
+ if (existingLink) {
3763
+ setIsEditing(true);
3764
+ setLinkData({
3765
+ url: existingLink.getAttribute("href") || "",
3766
+ target: existingLink.getAttribute("target") || "_self",
3767
+ rel: existingLink.getAttribute("rel") || "",
3768
+ title: existingLink.getAttribute("title") || "",
3769
+ pageRef: existingLink.getAttribute("data-page-ref") || "",
3770
+ urlExtra: existingLink.getAttribute("data-url-extra") || "",
3771
+ });
3772
+ }
3773
+ else {
3774
+ setIsEditing(false);
3775
+ setLinkData(EMPTY_LINK);
3776
+ }
3777
+ setShowDialog(true);
3778
+ }, []);
3779
+ const restoreSelection = useCallback(() => {
3780
+ if (savedRangeRef.current) {
3781
+ const sel = document.getSelection();
3782
+ if (sel) {
3783
+ sel.removeAllRanges();
3784
+ sel.addRange(savedRangeRef.current);
3785
+ }
3786
+ }
3787
+ }, []);
3788
+ const handleSave = useCallback((data) => {
3789
+ setShowDialog(false);
3790
+ restoreSelection();
3791
+ const sel = document.getSelection();
3792
+ if (!sel || sel.rangeCount === 0)
3793
+ return;
3794
+ const range = sel.getRangeAt(0);
3795
+ const container = range.commonAncestorContainer;
3796
+ const element = container.nodeType === Node.TEXT_NODE
3797
+ ? container.parentElement
3798
+ : container;
3799
+ const existingLink = element?.closest("a");
3800
+ if (existingLink) {
3801
+ // Update existing link
3802
+ existingLink.setAttribute("href", data.url);
3803
+ if (data.target && data.target !== "_self") {
3804
+ existingLink.setAttribute("target", data.target);
3805
+ }
3806
+ else {
3807
+ existingLink.removeAttribute("target");
3808
+ }
3809
+ if (data.rel) {
3810
+ existingLink.setAttribute("rel", data.rel);
3811
+ }
3812
+ else {
3813
+ existingLink.removeAttribute("rel");
3814
+ }
3815
+ if (data.title) {
3816
+ existingLink.setAttribute("title", data.title);
3817
+ }
3818
+ else {
3819
+ existingLink.removeAttribute("title");
3820
+ }
3821
+ if (data.pageRef) {
3822
+ existingLink.setAttribute("data-page-ref", data.pageRef);
3823
+ }
3824
+ else {
3825
+ existingLink.removeAttribute("data-page-ref");
3826
+ }
3827
+ if (data.urlExtra) {
3828
+ existingLink.setAttribute("data-url-extra", data.urlExtra);
3829
+ }
3830
+ else {
3831
+ existingLink.removeAttribute("data-url-extra");
3832
+ }
3833
+ }
3834
+ else {
3835
+ // Create new link
3836
+ document.execCommand("createLink", false, data.url);
3837
+ // Now find the newly created link and set extra attributes
3838
+ const newSel = document.getSelection();
3839
+ if (newSel && newSel.rangeCount > 0) {
3840
+ const newRange = newSel.getRangeAt(0);
3841
+ const newContainer = newRange.commonAncestorContainer;
3842
+ const newElement = newContainer.nodeType === Node.TEXT_NODE
3843
+ ? newContainer.parentElement
3844
+ : newContainer;
3845
+ const newLink = newElement?.closest("a");
3846
+ if (newLink) {
3847
+ if (data.target && data.target !== "_self") {
3848
+ newLink.setAttribute("target", data.target);
3849
+ }
3850
+ if (data.rel) {
3851
+ newLink.setAttribute("rel", data.rel);
3852
+ }
3853
+ if (data.title) {
3854
+ newLink.setAttribute("title", data.title);
3855
+ }
3856
+ if (data.pageRef) {
3857
+ newLink.setAttribute("data-page-ref", data.pageRef);
3858
+ }
3859
+ if (data.urlExtra) {
3860
+ newLink.setAttribute("data-url-extra", data.urlExtra);
3861
+ }
3862
+ }
3863
+ }
3864
+ }
3865
+ }, [restoreSelection]);
3866
+ const handleRemove = useCallback(() => {
3867
+ setShowDialog(false);
3868
+ restoreSelection();
3869
+ const sel = document.getSelection();
3870
+ if (!sel || sel.rangeCount === 0)
3871
+ return;
3872
+ const range = sel.getRangeAt(0);
3873
+ const container = range.commonAncestorContainer;
3874
+ const element = container.nodeType === Node.TEXT_NODE
3875
+ ? container.parentElement
3876
+ : container;
3877
+ const existingLink = element?.closest("a");
3878
+ if (existingLink) {
3879
+ const parent = existingLink.parentNode;
3880
+ if (parent) {
3881
+ while (existingLink.firstChild) {
3882
+ parent.insertBefore(existingLink.firstChild, existingLink);
3883
+ }
3884
+ parent.removeChild(existingLink);
3885
+ }
3886
+ }
3887
+ }, [restoreSelection]);
3888
+ return (jsxs("div", { style: { position: "relative" }, children: [jsx("button", { type: "button", onClick: openDialog, disabled: props.disabled, className: `rte-toolbar-button ${props.isActive ? "rte-toolbar-button-active" : ""}`, title: "Link", "aria-label": "Link", children: jsx(IconWrapper, { icon: "mdi:link", width: 18, height: 18 }) }), showDialog && (jsx(LinkDialog, { initialData: linkData, options: props.options, onSave: handleSave, onRemove: handleRemove, onClose: () => setShowDialog(false), isEditing: isEditing }))] }));
3889
+ };
3890
+ /* ══════════════════════════════════════════════════════════════════════════
3891
+ Plugin factory + export
3892
+ ══════════════════════════════════════════════════════════════════════ */
3893
+ /**
3894
+ * Creates an advanced link plugin with a floating dialog.
3895
+ * Supports URL, target, rel, title, page reference, and URL extra.
3896
+ */
3897
+ function createAdvancedLinkPlugin(options = {}) {
3898
+ const opts = {
3899
+ enablePageRef: false,
3900
+ enableTarget: true,
3901
+ enableRel: true,
3902
+ enableTitle: true,
3903
+ enableUrlExtra: false,
3904
+ ...options,
3905
+ };
3906
+ return {
3907
+ name: "advancedLink",
3908
+ type: "inline",
3909
+ renderButton: (props) => (jsx(LinkToolbarButton, { ...props, editorAPI: props.editorAPI, options: opts })),
3910
+ execute: () => {
3911
+ // Handled by the dialog component
3912
+ },
3913
+ isActive: () => {
3914
+ if (typeof document === "undefined")
3915
+ return false;
3916
+ const sel = document.getSelection();
3917
+ if (!sel || sel.rangeCount === 0)
3918
+ return false;
3919
+ const range = sel.getRangeAt(0);
3920
+ const container = range.commonAncestorContainer;
3921
+ const element = container.nodeType === Node.TEXT_NODE
3922
+ ? container.parentElement
3923
+ : container;
3924
+ return element?.closest("a") !== null;
3925
+ },
3926
+ canExecute: () => {
3927
+ const sel = document.getSelection();
3928
+ return sel !== null && sel.rangeCount > 0;
3929
+ },
3930
+ };
3931
+ }
3932
+ /** Pre-built advanced link plugin with target + rel + title enabled */
3933
+ const advancedLinkPlugin = createAdvancedLinkPlugin();
3934
+
3935
+ export { Dropdown, Editor, HistoryManager, TableContextMenuProvider, Toolbar, advancedLinkPlugin, alignmentPlugin, blockquotePlugin, boldPlugin, clearFormattingPlugin, codeInlinePlugin, contentToDOM, contentToHTML, createAdvancedLinkPlugin, createAlignmentPlugin, createBackgroundColorPlugin, createBlockFormatPlugin, createEmptyContent, createFontSizePlugin, createHeadingsPlugin, createImagePlugin, createLinkPlugin, createTextColorPlugin, Editor as default, defaultPlugins, domToContent, ensureAllCheckboxes, findClosestCheckboxList, getCurrentBackgroundColor, getCurrentFontSize, getCurrentHeading, getCurrentTextColor, htmlToContent, indentListItem, indentListItemPlugin, isCheckboxList, italicPlugin, linkPlugin, orderedListPlugin, outdentListItem, outdentListItemPlugin, redoPlugin, strikethroughPlugin, subscriptPlugin, superscriptPlugin, tablePlugin, underlinePlugin, undoPlugin, unorderedListPlugin };
2353
3936
  //# sourceMappingURL=index.esm.js.map