@overlap/rte 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +269 -0
  2. package/dist/components/Dropdown.d.ts +19 -0
  3. package/dist/components/Dropdown.d.ts.map +1 -0
  4. package/dist/components/Editor.d.ts +4 -0
  5. package/dist/components/Editor.d.ts.map +1 -0
  6. package/dist/components/FloatingToolbar.d.ts +10 -0
  7. package/dist/components/FloatingToolbar.d.ts.map +1 -0
  8. package/dist/components/IconWrapper.d.ts +10 -0
  9. package/dist/components/IconWrapper.d.ts.map +1 -0
  10. package/dist/components/Icons.d.ts +32 -0
  11. package/dist/components/Icons.d.ts.map +1 -0
  12. package/dist/components/Toolbar.d.ts +10 -0
  13. package/dist/components/Toolbar.d.ts.map +1 -0
  14. package/dist/components/index.d.ts +3 -0
  15. package/dist/components/index.d.ts.map +1 -0
  16. package/dist/index.d.ts +208 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.esm.js +2080 -0
  19. package/dist/index.esm.js.map +1 -0
  20. package/dist/index.js +2116 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/plugins/base.d.ts +10 -0
  23. package/dist/plugins/base.d.ts.map +1 -0
  24. package/dist/plugins/clearFormatting.d.ts +6 -0
  25. package/dist/plugins/clearFormatting.d.ts.map +1 -0
  26. package/dist/plugins/colors.d.ts +4 -0
  27. package/dist/plugins/colors.d.ts.map +1 -0
  28. package/dist/plugins/fontSize.d.ts +3 -0
  29. package/dist/plugins/fontSize.d.ts.map +1 -0
  30. package/dist/plugins/headings.d.ts +3 -0
  31. package/dist/plugins/headings.d.ts.map +1 -0
  32. package/dist/plugins/image.d.ts +6 -0
  33. package/dist/plugins/image.d.ts.map +1 -0
  34. package/dist/plugins/index.d.ts +14 -0
  35. package/dist/plugins/index.d.ts.map +1 -0
  36. package/dist/plugins/optional.d.ts +19 -0
  37. package/dist/plugins/optional.d.ts.map +1 -0
  38. package/dist/styles.css +638 -0
  39. package/dist/types.d.ts +81 -0
  40. package/dist/types.d.ts.map +1 -0
  41. package/dist/utils/clearFormatting.d.ts +21 -0
  42. package/dist/utils/clearFormatting.d.ts.map +1 -0
  43. package/dist/utils/content.d.ts +12 -0
  44. package/dist/utils/content.d.ts.map +1 -0
  45. package/dist/utils/history.d.ts +14 -0
  46. package/dist/utils/history.d.ts.map +1 -0
  47. package/dist/utils/listIndent.d.ts +9 -0
  48. package/dist/utils/listIndent.d.ts.map +1 -0
  49. package/dist/utils/stateReflection.d.ts +18 -0
  50. package/dist/utils/stateReflection.d.ts.map +1 -0
  51. package/package.json +48 -0
  52. package/src/components/Dropdown.tsx +103 -0
  53. package/src/components/Editor.css +2 -0
  54. package/src/components/Editor.tsx +785 -0
  55. package/src/components/FloatingToolbar.tsx +214 -0
  56. package/src/components/IconWrapper.tsx +14 -0
  57. package/src/components/Icons.tsx +145 -0
  58. package/src/components/Toolbar.tsx +137 -0
  59. package/src/components/index.ts +3 -0
  60. package/src/index.ts +19 -0
  61. package/src/plugins/base.tsx +91 -0
  62. package/src/plugins/clearFormatting.tsx +31 -0
  63. package/src/plugins/colors.tsx +122 -0
  64. package/src/plugins/fontSize.tsx +81 -0
  65. package/src/plugins/headings.tsx +76 -0
  66. package/src/plugins/image.tsx +189 -0
  67. package/src/plugins/index.ts +54 -0
  68. package/src/plugins/optional.tsx +221 -0
  69. package/src/styles.css +638 -0
  70. package/src/types.ts +92 -0
  71. package/src/utils/clearFormatting.ts +244 -0
  72. package/src/utils/content.ts +290 -0
  73. package/src/utils/history.ts +59 -0
  74. package/src/utils/listIndent.ts +171 -0
  75. package/src/utils/stateReflection.ts +175 -0
package/dist/index.js ADDED
@@ -0,0 +1,2116 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+ var React = require('react');
7
+
8
+ const BoldIcon = ({ width = 18, height = 18, className }) => (jsxRuntime.jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsxRuntime.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" }) }));
9
+ const ItalicIcon = ({ width = 18, height = 18, className }) => (jsxRuntime.jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsxRuntime.jsx("path", { d: "M10 4v3h2.21l-3.42 8H6v3h8v-3h-2.21l3.42-8H18V4z" }) }));
10
+ const UnderlineIcon = ({ width = 18, height = 18, className }) => (jsxRuntime.jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsxRuntime.jsx("path", { d: "M12 17c3.31 0 6-2.69 6-6V3h-2.5v8c0 1.93-1.57 3.5-3.5 3.5S8.5 12.93 8.5 11V3H6v8c0 3.31 2.69 6 6 6zm-7 2v2h14v-2H5z" }) }));
11
+ const UndoIcon = ({ width = 18, height = 18, className }) => (jsxRuntime.jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsxRuntime.jsx("path", { d: "M12.5 8c-2.65 0-5.05.99-6.9 2.6L2 7v9h9l-3.62-3.62c1.39-1.16 3.16-1.88 5.12-1.88 3.54 0 6.55 2.31 7.6 5.5l2.37-.78C21.08 11.03 17.15 8 12.5 8z" }) }));
12
+ const RedoIcon = ({ width = 18, height = 18, className }) => (jsxRuntime.jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsxRuntime.jsx("path", { d: "M18.4 10.6C16.55 8.99 14.15 8 11.5 8c-4.65 0-8.58 3.03-9.96 7.22L3.9 16c1.05-3.19 4.05-5.5 7.6-5.5 1.95 0 3.73.72 5.12 1.88L13 16h9V7l-3.6 3.6z" }) }));
13
+ const ClearFormattingIcon = ({ width = 18, height = 18, className }) => (jsxRuntime.jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsxRuntime.jsx("path", { d: "M6 5v.18L8.82 8h2.4L7.73 4.36C7.26 3.85 6.61 3.5 6 3.5c-1.11 0-2 .89-2 2 0 .61.35 1.26.86 1.73L6 5zm14.27 2.5L18.73 9H21v1h-4.27L15 8.27l1.23-1.23c.5-.5 1.15-.73 1.77-.73 1.11 0 2 .89 2 2 0 .62-.23 1.27-.73 1.77L18.27 13H21v1h-5.27l-2-2H9.73l-2 2H3v-1h4.27l2-2H7v-1h2.73l2-2H12v-1h-2.27l2-2h4.54zM5 15h14v2H5v-2z" }) }));
14
+ const LinkIcon = ({ width = 18, height = 18, className }) => (jsxRuntime.jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsxRuntime.jsx("path", { d: "M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z" }) }));
15
+ const QuoteIcon = ({ width = 18, height = 18, className }) => (jsxRuntime.jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsxRuntime.jsx("path", { d: "M6 17h3l2-4V7H5v6h3zm8 0h3l2-4V7h-6v6h3z" }) }));
16
+ const BulletListIcon = ({ width = 18, height = 18, className }) => (jsxRuntime.jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsxRuntime.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" }) }));
17
+ const NumberedListIcon = ({ width = 18, height = 18, className }) => (jsxRuntime.jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsxRuntime.jsx("path", { d: "M2 17h2v.5H3v1h1v.5H2v1h3v-4H2v1zm1-9h1V4H2v1h1v3zm-1 3h1.8L2 13.1v.9h3v-1H3.2L5 11.9V11H2zm6-5v2h14V6H8zm0 14h14v-2H8v2zm0-6h14v-2H8v2z" }) }));
18
+ const TextColorIcon = ({ width = 18, height = 18, className }) => (jsxRuntime.jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsxRuntime.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" }) }));
19
+ const BackgroundColorIcon = ({ width = 18, height = 18, className }) => (jsxRuntime.jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsxRuntime.jsx("path", { d: "M20 6h-2.18c.11-.31.18-.65.18-1 0-1.66-1.34-3-3-3-1.05 0-1.96.54-2.5 1.35l-.5.67-.5-.68C10.96 2.54 10.05 2 9 2 7.34 2 6 3.34 6 5c0 .35.07.69.18 1H4c-1.11 0-1.99.89-1.99 2L2 19c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V8c0-1.11-.89-2-2-2zm-5-2c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zM9 4c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm11 15H4v-2h16v2zm0-5H4V8h5.08L7 10.83 8.62 12 11 8.76l1-1.36 1 1.36L15.38 12 17 10.83 14.92 8H20v6z" }) }));
20
+ const HeadingIcon = ({ width = 18, height = 18, className }) => (jsxRuntime.jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsxRuntime.jsx("path", { d: "M5 4v3h5.5v12h3V7H19V4H5z" }) }));
21
+ const FontSizeIcon = ({ width = 18, height = 18, className }) => (jsxRuntime.jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsxRuntime.jsx("path", { d: "M9 4v3h5v12h3V7h5V4H9zm-6 8h3v8h3v-8h3V10H3z" }) }));
22
+ const ImageIcon = ({ width = 18, height = 18, className }) => (jsxRuntime.jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsxRuntime.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" }) }));
23
+ const CloseIcon = ({ width = 18, height = 18, className }) => (jsxRuntime.jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsxRuntime.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" }) }));
24
+ const LoadingIcon = ({ width = 18, height = 18, className }) => (jsxRuntime.jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsxRuntime.jsx("path", { d: "M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z" }) }));
25
+ const UploadIcon = ({ width = 18, height = 18, className }) => (jsxRuntime.jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsxRuntime.jsx("path", { d: "M9 16h6v-6h4l-7-7-7 7h4zm-4 2h14v2H5z" }) }));
26
+ const iconMap = {
27
+ 'mdi:format-bold': BoldIcon,
28
+ 'mdi:format-italic': ItalicIcon,
29
+ 'mdi:format-underline': UnderlineIcon,
30
+ 'mdi:undo': UndoIcon,
31
+ 'mdi:redo': RedoIcon,
32
+ 'mdi:format-clear': ClearFormattingIcon,
33
+ 'mdi:link': LinkIcon,
34
+ 'mdi:format-quote-close': QuoteIcon,
35
+ 'mdi:format-list-bulleted': BulletListIcon,
36
+ 'mdi:format-list-numbered': NumberedListIcon,
37
+ 'mdi:format-color-text': TextColorIcon,
38
+ 'mdi:format-color-fill': BackgroundColorIcon,
39
+ 'mdi:format-header-1': HeadingIcon,
40
+ 'mdi:format-size': FontSizeIcon,
41
+ 'mdi:image': ImageIcon,
42
+ 'mdi:close': CloseIcon,
43
+ 'mdi:loading': LoadingIcon,
44
+ 'mdi:upload': UploadIcon,
45
+ };
46
+ const Icon = ({ icon, width = 18, height = 18, className }) => {
47
+ const IconComponent = iconMap[icon];
48
+ if (!IconComponent) {
49
+ return jsxRuntime.jsx("span", { style: { width, height, display: 'inline-block' } });
50
+ }
51
+ return jsxRuntime.jsx(IconComponent, { width: width, height: height, className: className });
52
+ };
53
+
54
+ const IconWrapper = ({ icon, width = 18, height = 18, className }) => {
55
+ return jsxRuntime.jsx(Icon, { icon: icon, width: width, height: height, className: className });
56
+ };
57
+
58
+ /**
59
+ * Basis-Plugin für Inline-Formatierungen
60
+ */
61
+ function createInlinePlugin(name, command, icon, label) {
62
+ return {
63
+ name,
64
+ type: 'inline',
65
+ command,
66
+ renderButton: (props) => (jsxRuntime.jsx("button", { type: "button", onClick: props.onClick, disabled: props.disabled, className: `rte-toolbar-button ${props.isActive ? 'rte-toolbar-button-active' : ''}`, title: label, "aria-label": label, children: jsxRuntime.jsx(IconWrapper, { icon: icon, width: 18, height: 18 }) })),
67
+ execute: (editor) => {
68
+ editor.executeCommand(command);
69
+ },
70
+ isActive: (editor) => {
71
+ if (typeof window === 'undefined' || typeof document === 'undefined')
72
+ return false;
73
+ const selection = editor.getSelection();
74
+ if (!selection || selection.rangeCount === 0)
75
+ return false;
76
+ const range = selection.getRangeAt(0);
77
+ const container = range.commonAncestorContainer;
78
+ const element = container.nodeType === Node.TEXT_NODE
79
+ ? container.parentElement
80
+ : container;
81
+ if (!element)
82
+ return false;
83
+ return document.queryCommandState(command);
84
+ },
85
+ canExecute: (editor) => {
86
+ // Formatierung sollte auch ohne Selection möglich sein
87
+ // (z.B. wenn Editor leer ist, wird beim Klick eine Selection erstellt)
88
+ return true;
89
+ },
90
+ };
91
+ }
92
+ /**
93
+ * Basis-Plugin für Commands
94
+ */
95
+ function createCommandPlugin(name, command, icon, label) {
96
+ return {
97
+ name,
98
+ type: 'command',
99
+ command,
100
+ renderButton: (props) => (jsxRuntime.jsx("button", { type: "button", onClick: props.onClick, disabled: props.disabled, className: "rte-toolbar-button", title: label, "aria-label": label, children: jsxRuntime.jsx(IconWrapper, { icon: icon, width: 18, height: 18 }) })),
101
+ execute: (editor) => {
102
+ editor.executeCommand(command);
103
+ },
104
+ canExecute: (editor) => {
105
+ if (command === 'undo')
106
+ return editor.canUndo();
107
+ if (command === 'redo')
108
+ return editor.canRedo();
109
+ return true;
110
+ },
111
+ };
112
+ }
113
+
114
+ /**
115
+ * Clear Formatting Plugin - Entfernt alle Formatierungen
116
+ */
117
+ const clearFormattingPlugin = {
118
+ name: 'clearFormatting',
119
+ type: 'command',
120
+ renderButton: (props) => (jsxRuntime.jsx("button", { type: "button", onClick: props.onClick, disabled: props.disabled, className: "rte-toolbar-button", title: "Formatierung entfernen", "aria-label": "Formatierung entfernen", children: jsxRuntime.jsx(IconWrapper, { icon: "mdi:format-clear", width: 18, height: 18 }) })),
121
+ execute: (editor) => {
122
+ editor.clearFormatting();
123
+ },
124
+ canExecute: (editor) => {
125
+ const selection = editor.getSelection();
126
+ return selection !== null && selection.rangeCount > 0 && !selection.isCollapsed;
127
+ },
128
+ };
129
+
130
+ /**
131
+ * Standard-Plugins
132
+ */
133
+ const boldPlugin = createInlinePlugin('bold', 'bold', 'mdi:format-bold', 'Fett');
134
+ const italicPlugin = createInlinePlugin('italic', 'italic', 'mdi:format-italic', 'Kursiv');
135
+ const underlinePlugin = createInlinePlugin('underline', 'underline', 'mdi:format-underline', 'Unterstrichen');
136
+ const undoPlugin = createCommandPlugin('undo', 'undo', 'mdi:undo', 'Rückgängig');
137
+ const redoPlugin = createCommandPlugin('redo', 'redo', 'mdi:redo', 'Wiederholen');
138
+ /**
139
+ * Standard-Plugin-Liste
140
+ */
141
+ const defaultPlugins = [
142
+ undoPlugin,
143
+ redoPlugin,
144
+ boldPlugin,
145
+ italicPlugin,
146
+ underlinePlugin,
147
+ clearFormattingPlugin,
148
+ ];
149
+
150
+ const Dropdown = ({ icon, label, options, onSelect, currentValue, disabled, }) => {
151
+ const [isOpen, setIsOpen] = React.useState(false);
152
+ const dropdownRef = React.useRef(null);
153
+ React.useEffect(() => {
154
+ const handleClickOutside = (event) => {
155
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
156
+ setIsOpen(false);
157
+ }
158
+ };
159
+ if (isOpen) {
160
+ document.addEventListener('mousedown', handleClickOutside);
161
+ }
162
+ return () => {
163
+ document.removeEventListener('mousedown', handleClickOutside);
164
+ };
165
+ }, [isOpen]);
166
+ const handleSelect = (value) => {
167
+ onSelect(value);
168
+ setIsOpen(false);
169
+ };
170
+ const currentOption = options.find(opt => opt.value === currentValue);
171
+ return (jsxRuntime.jsxs("div", { className: "rte-dropdown", ref: dropdownRef, children: [jsxRuntime.jsxs("button", { type: "button", onClick: () => !disabled && setIsOpen(!isOpen), disabled: disabled, className: `rte-toolbar-button rte-dropdown-button ${currentOption ? 'rte-dropdown-button-has-value' : ''}`, title: label, "aria-label": label, children: [jsxRuntime.jsx(Icon, { icon: icon, width: 18, height: 18 }), currentOption && (jsxRuntime.jsx("span", { className: "rte-dropdown-value", children: currentOption.label }))] }), isOpen && (jsxRuntime.jsx("div", { className: "rte-dropdown-menu", children: options.map((option) => (jsxRuntime.jsxs("button", { type: "button", className: `rte-dropdown-item ${currentValue === option.value ? 'rte-dropdown-item-active' : ''}`, onClick: () => handleSelect(option.value), children: [option.color && (jsxRuntime.jsx("span", { className: `rte-dropdown-color-preview ${currentValue === option.value ? 'active' : ''}`, style: { backgroundColor: option.color } })), option.preview && !option.headingPreview && (jsxRuntime.jsx("span", { className: "rte-dropdown-fontsize-preview", style: { fontSize: `${option.preview}px` }, children: "Aa" })), option.headingPreview && (jsxRuntime.jsx("span", { className: `rte-dropdown-heading-preview ${option.headingPreview}`, children: option.headingPreview === 'p' ? 'Normal' : option.headingPreview.toUpperCase() })), option.icon && jsxRuntime.jsx(Icon, { icon: option.icon, width: 16, height: 16 }), jsxRuntime.jsx("span", { style: { flex: 1, fontWeight: currentValue === option.value ? 600 : 400 }, children: option.label })] }, option.value))) }))] }));
172
+ };
173
+
174
+ /**
175
+ * Liest die aktuelle Font-Size aus dem DOM an der Cursor-Position
176
+ */
177
+ function getCurrentFontSize(editor) {
178
+ const selection = editor.getSelection();
179
+ if (!selection || selection.rangeCount === 0)
180
+ return undefined;
181
+ const range = selection.getRangeAt(0);
182
+ const container = range.commonAncestorContainer;
183
+ const element = container.nodeType === Node.TEXT_NODE
184
+ ? container.parentElement
185
+ : container;
186
+ if (!element)
187
+ return undefined;
188
+ // Suche nach dem nächsten Element mit fontSize
189
+ let current = element;
190
+ while (current && current !== document.body) {
191
+ const fontSize = window.getComputedStyle(current).fontSize;
192
+ if (fontSize && fontSize !== 'inherit' && fontSize !== 'initial') {
193
+ // Konvertiere "16px" zu "16"
194
+ const size = parseInt(fontSize, 10);
195
+ if (!isNaN(size)) {
196
+ return size.toString();
197
+ }
198
+ }
199
+ current = current.parentElement;
200
+ }
201
+ return undefined;
202
+ }
203
+ /**
204
+ * Liest die aktuelle Textfarbe aus dem DOM an der Cursor-Position
205
+ */
206
+ function getCurrentTextColor(editor) {
207
+ const selection = editor.getSelection();
208
+ if (!selection || selection.rangeCount === 0)
209
+ return undefined;
210
+ const range = selection.getRangeAt(0);
211
+ const container = range.commonAncestorContainer;
212
+ const element = container.nodeType === Node.TEXT_NODE
213
+ ? container.parentElement
214
+ : container;
215
+ if (!element)
216
+ return undefined;
217
+ // Suche nach dem nächsten Element mit color
218
+ let current = element;
219
+ while (current && current !== document.body) {
220
+ // Prüfe zuerst inline style (hat Priorität)
221
+ const inlineColor = current.style.color;
222
+ if (inlineColor && inlineColor.trim()) {
223
+ // Wenn bereits Hex, direkt zurückgeben
224
+ if (inlineColor.startsWith('#')) {
225
+ return inlineColor;
226
+ }
227
+ // Wenn RGB/RGBA, konvertieren
228
+ const rgbMatch = inlineColor.match(/\d+/g);
229
+ if (rgbMatch && rgbMatch.length >= 3) {
230
+ const hex = '#' + rgbMatch.slice(0, 3).map(x => {
231
+ const hex = parseInt(x, 10).toString(16);
232
+ return hex.length === 1 ? '0' + hex : hex;
233
+ }).join('');
234
+ return hex;
235
+ }
236
+ }
237
+ // Dann computed style
238
+ const color = window.getComputedStyle(current).color;
239
+ if (color && color !== 'inherit' && color !== 'initial' && color !== 'rgb(0, 0, 0)' && color !== 'rgba(0, 0, 0, 0)') {
240
+ // Konvertiere RGB zu Hex
241
+ const rgb = color.match(/\d+/g);
242
+ if (rgb && rgb.length >= 3) {
243
+ const hex = '#' + rgb.slice(0, 3).map(x => {
244
+ const hex = parseInt(x, 10).toString(16);
245
+ return hex.length === 1 ? '0' + hex : hex;
246
+ }).join('');
247
+ return hex;
248
+ }
249
+ }
250
+ current = current.parentElement;
251
+ }
252
+ return undefined;
253
+ }
254
+ /**
255
+ * Liest die aktuelle Hintergrundfarbe aus dem DOM an der Cursor-Position
256
+ */
257
+ function getCurrentBackgroundColor(editor) {
258
+ const selection = editor.getSelection();
259
+ if (!selection || selection.rangeCount === 0)
260
+ return undefined;
261
+ const range = selection.getRangeAt(0);
262
+ const container = range.commonAncestorContainer;
263
+ const element = container.nodeType === Node.TEXT_NODE
264
+ ? container.parentElement
265
+ : container;
266
+ if (!element)
267
+ return undefined;
268
+ // Suche nach dem nächsten Element mit backgroundColor
269
+ let current = element;
270
+ while (current && current !== document.body) {
271
+ // Prüfe zuerst inline style (hat Priorität)
272
+ const inlineBgColor = current.style.backgroundColor;
273
+ if (inlineBgColor && inlineBgColor.trim()) {
274
+ // Wenn bereits Hex, direkt zurückgeben
275
+ if (inlineBgColor.startsWith('#')) {
276
+ return inlineBgColor;
277
+ }
278
+ // Wenn RGB/RGBA, konvertieren
279
+ const rgbMatch = inlineBgColor.match(/\d+/g);
280
+ if (rgbMatch && rgbMatch.length >= 3) {
281
+ const hex = '#' + rgbMatch.slice(0, 3).map(x => {
282
+ const hex = parseInt(x, 10).toString(16);
283
+ return hex.length === 1 ? '0' + hex : hex;
284
+ }).join('');
285
+ return hex;
286
+ }
287
+ }
288
+ // Dann computed style
289
+ const bgColor = window.getComputedStyle(current).backgroundColor;
290
+ if (bgColor && bgColor !== 'inherit' && bgColor !== 'initial' && bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent') {
291
+ // Konvertiere RGB zu Hex
292
+ const rgb = bgColor.match(/\d+/g);
293
+ if (rgb && rgb.length >= 3) {
294
+ const hex = '#' + rgb.slice(0, 3).map(x => {
295
+ const hex = parseInt(x, 10).toString(16);
296
+ return hex.length === 1 ? '0' + hex : hex;
297
+ }).join('');
298
+ return hex;
299
+ }
300
+ }
301
+ current = current.parentElement;
302
+ }
303
+ return undefined;
304
+ }
305
+ /**
306
+ * Liest das aktuelle Heading-Level aus dem DOM an der Cursor-Position
307
+ */
308
+ function getCurrentHeading(editor, availableHeadings) {
309
+ const selection = editor.getSelection();
310
+ if (!selection || selection.rangeCount === 0)
311
+ return undefined;
312
+ const range = selection.getRangeAt(0);
313
+ const container = range.commonAncestorContainer;
314
+ const element = container.nodeType === Node.TEXT_NODE
315
+ ? container.parentElement
316
+ : container;
317
+ if (!element)
318
+ return undefined;
319
+ // Suche nach dem nächsten Block-Element
320
+ let current = element;
321
+ while (current && current !== document.body) {
322
+ const tagName = current.tagName.toLowerCase();
323
+ if (availableHeadings.includes(tagName)) {
324
+ return tagName;
325
+ }
326
+ if (tagName === 'p') {
327
+ return 'p';
328
+ }
329
+ current = current.parentElement;
330
+ }
331
+ return undefined;
332
+ }
333
+
334
+ const defaultColors = [
335
+ { value: '#000000', label: 'Schwarz', color: '#000000' },
336
+ { value: '#333333', label: 'Dunkelgrau', color: '#333333' },
337
+ { value: '#666666', label: 'Grau', color: '#666666' },
338
+ { value: '#ff0000', label: 'Rot', color: '#ff0000' },
339
+ { value: '#0000ff', label: 'Blau', color: '#0000ff' },
340
+ { value: '#00aa00', label: 'Grün', color: '#00aa00' },
341
+ { value: '#ffaa00', label: 'Orange', color: '#ffaa00' },
342
+ { value: '#aa00ff', label: 'Lila', color: '#aa00ff' },
343
+ ];
344
+ function createTextColorPlugin(colors = defaultColors.map(c => c.value)) {
345
+ // Finde Labels für bekannte Farben
346
+ const getColorLabel = (color) => {
347
+ const found = defaultColors.find(c => c.value.toLowerCase() === color.toLowerCase());
348
+ return found ? found.label : color;
349
+ };
350
+ const colorOptions = colors.map(color => ({
351
+ value: color,
352
+ label: getColorLabel(color),
353
+ color,
354
+ }));
355
+ return {
356
+ name: 'textColor',
357
+ type: 'inline',
358
+ renderButton: (props) => {
359
+ // Aktuelle Textfarbe aus State Reflection
360
+ const currentValue = props.currentValue || (props.editorAPI ? getCurrentTextColor(props.editorAPI) : undefined);
361
+ return (jsxRuntime.jsx(Dropdown, { icon: "mdi:format-color-text", label: "Textfarbe", options: colorOptions.map(opt => ({
362
+ value: opt.value,
363
+ label: opt.label,
364
+ color: opt.color,
365
+ })), onSelect: (value) => {
366
+ if (props.onSelect) {
367
+ props.onSelect(value);
368
+ }
369
+ else {
370
+ props.onClick();
371
+ }
372
+ }, currentValue: currentValue, disabled: props.disabled }));
373
+ },
374
+ execute: (editor, value) => {
375
+ if (value) {
376
+ editor.executeCommand('foreColor', value);
377
+ }
378
+ },
379
+ canExecute: () => true,
380
+ getCurrentValue: (editor) => {
381
+ return getCurrentTextColor(editor);
382
+ },
383
+ };
384
+ }
385
+ function createBackgroundColorPlugin(colors = defaultColors.map(c => c.value)) {
386
+ // Finde Labels für bekannte Farben
387
+ const getColorLabel = (color) => {
388
+ const found = defaultColors.find(c => c.value.toLowerCase() === color.toLowerCase());
389
+ return found ? found.label : color;
390
+ };
391
+ const colorOptions = colors.map(color => ({
392
+ value: color,
393
+ label: getColorLabel(color),
394
+ color,
395
+ }));
396
+ return {
397
+ name: 'backgroundColor',
398
+ type: 'inline',
399
+ renderButton: (props) => {
400
+ // Aktuelle Hintergrundfarbe aus State Reflection
401
+ const currentValue = props.currentValue || (props.editorAPI ? getCurrentBackgroundColor(props.editorAPI) : undefined);
402
+ return (jsxRuntime.jsx(Dropdown, { icon: "mdi:format-color-fill", label: "Hintergrundfarbe", options: colorOptions.map(opt => ({
403
+ value: opt.value,
404
+ label: opt.label,
405
+ color: opt.color,
406
+ })), onSelect: (value) => {
407
+ if (props.onSelect) {
408
+ props.onSelect(value);
409
+ }
410
+ else {
411
+ props.onClick();
412
+ }
413
+ }, currentValue: currentValue, disabled: props.disabled }));
414
+ },
415
+ execute: (editor, value) => {
416
+ if (value) {
417
+ editor.executeCommand('backColor', value);
418
+ }
419
+ },
420
+ canExecute: () => true,
421
+ getCurrentValue: (editor) => {
422
+ return getCurrentBackgroundColor(editor);
423
+ },
424
+ };
425
+ }
426
+
427
+ function createFontSizePlugin(fontSizes = [12, 14, 16, 18, 20, 24]) {
428
+ return {
429
+ name: 'fontSize',
430
+ type: 'inline',
431
+ renderButton: (props) => {
432
+ const sizes = props.fontSizes || fontSizes;
433
+ const options = sizes.map(size => ({
434
+ value: size.toString(),
435
+ label: `${size}px`,
436
+ preview: size.toString(),
437
+ }));
438
+ // Aktuelle Font-Size aus State Reflection
439
+ const currentValue = props.currentValue || (props.editorAPI ? getCurrentFontSize(props.editorAPI) : undefined);
440
+ return (jsxRuntime.jsx(Dropdown, { icon: "mdi:format-size", label: "Schriftgr\u00F6\u00DFe", options: options, onSelect: (value) => {
441
+ if (props.onSelect) {
442
+ props.onSelect(value);
443
+ }
444
+ else {
445
+ props.onClick();
446
+ }
447
+ }, currentValue: currentValue, disabled: props.disabled }));
448
+ },
449
+ getCurrentValue: (editor) => {
450
+ return getCurrentFontSize(editor);
451
+ },
452
+ execute: (editor, value) => {
453
+ if (value) {
454
+ // Setze inline style für präzise Größe
455
+ const selection = editor.getSelection();
456
+ if (selection && selection.rangeCount > 0) {
457
+ const range = selection.getRangeAt(0);
458
+ let element = null;
459
+ if (range.commonAncestorContainer.nodeType === Node.TEXT_NODE) {
460
+ element = range.commonAncestorContainer.parentElement;
461
+ }
462
+ else {
463
+ element = range.commonAncestorContainer;
464
+ }
465
+ if (element) {
466
+ // Erstelle oder aktualisiere span mit fontSize
467
+ const span = document.createElement('span');
468
+ span.style.fontSize = `${value}px`;
469
+ try {
470
+ range.surroundContents(span);
471
+ }
472
+ catch (e) {
473
+ // Falls surroundContents fehlschlägt
474
+ const contents = range.extractContents();
475
+ span.appendChild(contents);
476
+ range.insertNode(span);
477
+ }
478
+ // Cursor setzen
479
+ range.setStartAfter(span);
480
+ range.collapse(true);
481
+ selection.removeAllRanges();
482
+ selection.addRange(range);
483
+ }
484
+ }
485
+ }
486
+ },
487
+ canExecute: () => true,
488
+ };
489
+ }
490
+
491
+ const defaultHeadings = ['h1', 'h2', 'h3'];
492
+ const headingLabels = {
493
+ h1: 'Überschrift 1',
494
+ h2: 'Überschrift 2',
495
+ h3: 'Überschrift 3',
496
+ h4: 'Überschrift 4',
497
+ h5: 'Überschrift 5',
498
+ h6: 'Überschrift 6',
499
+ };
500
+ function createHeadingsPlugin(headings = defaultHeadings) {
501
+ const options = [
502
+ { value: 'p', label: 'Normal', headingPreview: 'p' },
503
+ ...headings.map(h => ({
504
+ value: h,
505
+ label: headingLabels[h] || h.toUpperCase(),
506
+ headingPreview: h,
507
+ })),
508
+ ];
509
+ return {
510
+ name: 'headings',
511
+ type: 'block',
512
+ renderButton: (props) => {
513
+ // Aktuelles Heading aus State Reflection
514
+ const currentValue = props.currentValue || (props.editorAPI ? getCurrentHeading(props.editorAPI, headings) : undefined);
515
+ return (jsxRuntime.jsx(Dropdown, { icon: "mdi:format-header-1", label: "\u00DCberschrift", options: options, onSelect: (value) => {
516
+ if (props.onSelect) {
517
+ props.onSelect(value);
518
+ }
519
+ else {
520
+ props.onClick();
521
+ }
522
+ }, currentValue: currentValue, disabled: props.disabled }));
523
+ },
524
+ getCurrentValue: (editor) => {
525
+ return getCurrentHeading(editor, headings);
526
+ },
527
+ execute: (editor, value) => {
528
+ const tag = value || 'p';
529
+ editor.executeCommand('formatBlock', `<${tag}>`);
530
+ },
531
+ isActive: (editor) => {
532
+ const selection = editor.getSelection();
533
+ if (!selection || selection.rangeCount === 0)
534
+ return false;
535
+ const range = selection.getRangeAt(0);
536
+ const container = range.commonAncestorContainer;
537
+ const element = container.nodeType === Node.TEXT_NODE
538
+ ? container.parentElement
539
+ : container;
540
+ if (!element)
541
+ return false;
542
+ const tagName = element.tagName.toLowerCase();
543
+ return headings.includes(tagName);
544
+ },
545
+ canExecute: () => true,
546
+ };
547
+ }
548
+
549
+ /**
550
+ * Image-Plugin mit URL-Eingabe und File-Upload
551
+ */
552
+ function createImagePlugin(onImageUpload) {
553
+ return {
554
+ name: 'image',
555
+ type: 'block',
556
+ renderButton: (props) => {
557
+ const [showModal, setShowModal] = React.useState(false);
558
+ const [imageUrl, setImageUrl] = React.useState('');
559
+ const [altText, setAltText] = React.useState('');
560
+ const [isUploading, setIsUploading] = React.useState(false);
561
+ const fileInputRef = React.useRef(null);
562
+ const handleInsertImage = () => {
563
+ if (!props.editorAPI)
564
+ return;
565
+ const src = imageUrl.trim();
566
+ if (!src) {
567
+ alert('Bitte geben Sie eine Bild-URL ein');
568
+ return;
569
+ }
570
+ // Verwende executeCommand, das alles korrekt handhabt
571
+ // Alt-Text wird später über das Datenmodell gespeichert
572
+ props.editorAPI.executeCommand('insertImage', src);
573
+ // Modal schließen
574
+ setShowModal(false);
575
+ setImageUrl('');
576
+ setAltText('');
577
+ };
578
+ const handleFileSelect = async (e) => {
579
+ const file = e.target.files?.[0];
580
+ if (!file || !onImageUpload)
581
+ return;
582
+ if (!file.type.startsWith('image/')) {
583
+ alert('Bitte wählen Sie eine Bilddatei aus');
584
+ return;
585
+ }
586
+ setIsUploading(true);
587
+ try {
588
+ const uploadedUrl = await onImageUpload(file);
589
+ setImageUrl(uploadedUrl);
590
+ }
591
+ catch (error) {
592
+ console.error('Image upload failed:', error);
593
+ alert('Fehler beim Hochladen des Bildes');
594
+ }
595
+ finally {
596
+ setIsUploading(false);
597
+ if (fileInputRef.current) {
598
+ fileInputRef.current.value = '';
599
+ }
600
+ }
601
+ };
602
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("button", { type: "button", onClick: () => setShowModal(true), disabled: props.disabled, className: "rte-toolbar-button", title: "Bild einf\u00FCgen", "aria-label": "Bild einf\u00FCgen", children: jsxRuntime.jsx(IconWrapper, { icon: "mdi:image", width: 18, height: 18 }) }), showModal && (jsxRuntime.jsx("div", { className: "rte-image-modal-overlay", onClick: (e) => {
603
+ if (e.target === e.currentTarget) {
604
+ setShowModal(false);
605
+ }
606
+ }, children: jsxRuntime.jsxs("div", { className: "rte-image-modal", children: [jsxRuntime.jsxs("div", { className: "rte-image-modal-header", children: [jsxRuntime.jsx("h3", { children: "Bild einf\u00FCgen" }), jsxRuntime.jsx("button", { type: "button", onClick: () => setShowModal(false), className: "rte-image-modal-close", "aria-label": "Schlie\u00DFen", children: jsxRuntime.jsx(IconWrapper, { icon: "mdi:close", width: 20, height: 20 }) })] }), jsxRuntime.jsxs("div", { className: "rte-image-modal-content", children: [onImageUpload && (jsxRuntime.jsx("div", { className: "rte-image-upload-section", children: jsxRuntime.jsxs("label", { className: "rte-image-upload-label", children: [jsxRuntime.jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", onChange: handleFileSelect, style: { display: 'none' } }), jsxRuntime.jsx("div", { className: "rte-image-upload-button", children: isUploading ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(IconWrapper, { icon: "mdi:loading", width: 24, height: 24, className: "rte-spin" }), jsxRuntime.jsx("span", { children: "Wird hochgeladen..." })] })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(IconWrapper, { icon: "mdi:upload", width: 24, height: 24 }), jsxRuntime.jsx("span", { children: "Datei ausw\u00E4hlen" })] })) })] }) })), jsxRuntime.jsx("div", { className: "rte-image-url-section", children: jsxRuntime.jsxs("label", { children: ["Bild-URL", jsxRuntime.jsx("input", { type: "url", value: imageUrl, onChange: (e) => setImageUrl(e.target.value), placeholder: "https://example.com/image.jpg", className: "rte-image-url-input" })] }) }), jsxRuntime.jsx("div", { className: "rte-image-alt-section", children: jsxRuntime.jsxs("label", { children: ["Alt-Text (optional)", jsxRuntime.jsx("input", { type: "text", value: altText, onChange: (e) => setAltText(e.target.value), placeholder: "Bildbeschreibung", className: "rte-image-alt-input" })] }) }), imageUrl && (jsxRuntime.jsx("div", { className: "rte-image-preview", children: jsxRuntime.jsx("img", { src: imageUrl, alt: altText || 'Preview' }) }))] }), jsxRuntime.jsxs("div", { className: "rte-image-modal-footer", children: [jsxRuntime.jsx("button", { type: "button", onClick: () => setShowModal(false), className: "rte-image-modal-cancel", children: "Abbrechen" }), jsxRuntime.jsx("button", { type: "button", onClick: handleInsertImage, disabled: !imageUrl.trim() || isUploading, className: "rte-image-modal-insert", children: "Einf\u00FCgen" })] })] }) }))] }));
607
+ },
608
+ execute: (editor) => {
609
+ // Wird über renderButton gehandhabt
610
+ },
611
+ canExecute: () => true,
612
+ };
613
+ }
614
+
615
+ /**
616
+ * Entfernt alle Formatierungen aus der aktuellen Selection
617
+ */
618
+ function clearFormatting(selection) {
619
+ if (!selection || selection.rangeCount === 0)
620
+ return;
621
+ const range = selection.getRangeAt(0);
622
+ // Wenn Selection leer ist, nichts zu tun
623
+ if (range.collapsed)
624
+ return;
625
+ try {
626
+ // Verwende document.execCommand für Standard-Formatierungen
627
+ document.execCommand('removeFormat', false);
628
+ document.execCommand('unlink', false);
629
+ // Entferne alle inline styles und Formatierungen manuell
630
+ const walker = document.createTreeWalker(range.commonAncestorContainer, NodeFilter.SHOW_ELEMENT, null);
631
+ const elements = [];
632
+ let node = walker.currentNode;
633
+ while (node) {
634
+ if (node.nodeType === Node.ELEMENT_NODE) {
635
+ const el = node;
636
+ if (range.intersectsNode(el)) {
637
+ const tagName = el.tagName.toLowerCase();
638
+ // Entferne Inline-Formatierungen (strong, em, u, span, a, etc.)
639
+ if (['strong', 'b', 'em', 'i', 'u', 'span', 'a', 'font'].includes(tagName)) {
640
+ // Ersetze durch Text-Node
641
+ const text = el.textContent || '';
642
+ const textNode = document.createTextNode(text);
643
+ el.parentNode?.replaceChild(textNode, el);
644
+ }
645
+ // Entferne alle Styles
646
+ if (el.style.length > 0) {
647
+ el.removeAttribute('style');
648
+ }
649
+ // Konvertiere Headings zu Paragraphs
650
+ if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
651
+ const p = document.createElement('p');
652
+ while (el.firstChild) {
653
+ p.appendChild(el.firstChild);
654
+ }
655
+ el.parentNode?.replaceChild(p, el);
656
+ }
657
+ }
658
+ }
659
+ node = walker.nextNode();
660
+ }
661
+ // Normalisiere: Entferne leere Formatierungs-Tags
662
+ const normalizeWalker = document.createTreeWalker(range.commonAncestorContainer, NodeFilter.SHOW_ELEMENT, null);
663
+ node = normalizeWalker.currentNode;
664
+ while (node) {
665
+ if (node.nodeType === Node.ELEMENT_NODE) {
666
+ const el = node;
667
+ const tagName = el.tagName.toLowerCase();
668
+ if (['strong', 'b', 'em', 'i', 'u', 'span'].includes(tagName) && !el.textContent?.trim()) {
669
+ el.parentNode?.removeChild(el);
670
+ }
671
+ }
672
+ node = normalizeWalker.nextNode();
673
+ }
674
+ }
675
+ catch (error) {
676
+ console.error('Error clearing formatting:', error);
677
+ // Fallback: Einfache Methode
678
+ document.execCommand('removeFormat', false);
679
+ document.execCommand('unlink', false);
680
+ }
681
+ }
682
+ /**
683
+ * Entfernt nur Textfarbe
684
+ */
685
+ function clearTextColor(selection) {
686
+ if (!selection || selection.rangeCount === 0)
687
+ return;
688
+ const range = selection.getRangeAt(0);
689
+ if (range.collapsed)
690
+ return;
691
+ // Finde alle Elemente mit color style
692
+ const walker = document.createTreeWalker(range.commonAncestorContainer, NodeFilter.SHOW_ELEMENT, null);
693
+ const elements = [];
694
+ let node = walker.currentNode;
695
+ while (node) {
696
+ if (node.nodeType === Node.ELEMENT_NODE) {
697
+ const el = node;
698
+ if (range.intersectsNode(el) && el.style.color) {
699
+ elements.push(el);
700
+ }
701
+ }
702
+ node = walker.nextNode();
703
+ }
704
+ elements.forEach(el => {
705
+ el.style.color = '';
706
+ if (!el.style.length) {
707
+ el.removeAttribute('style');
708
+ }
709
+ });
710
+ }
711
+ /**
712
+ * Entfernt nur Hintergrundfarbe
713
+ */
714
+ function clearBackgroundColor(selection) {
715
+ if (!selection || selection.rangeCount === 0)
716
+ return;
717
+ const range = selection.getRangeAt(0);
718
+ if (range.collapsed)
719
+ return;
720
+ // Finde alle Elemente mit backgroundColor style
721
+ const walker = document.createTreeWalker(range.commonAncestorContainer, NodeFilter.SHOW_ELEMENT, null);
722
+ const elements = [];
723
+ let node = walker.currentNode;
724
+ while (node) {
725
+ if (node.nodeType === Node.ELEMENT_NODE) {
726
+ const el = node;
727
+ if (range.intersectsNode(el) && el.style.backgroundColor) {
728
+ elements.push(el);
729
+ }
730
+ }
731
+ node = walker.nextNode();
732
+ }
733
+ elements.forEach(el => {
734
+ el.style.backgroundColor = '';
735
+ if (!el.style.length) {
736
+ el.removeAttribute('style');
737
+ }
738
+ });
739
+ }
740
+ /**
741
+ * Entfernt nur Font-Size
742
+ */
743
+ function clearFontSize(selection) {
744
+ if (!selection || selection.rangeCount === 0)
745
+ return;
746
+ const range = selection.getRangeAt(0);
747
+ if (range.collapsed)
748
+ return;
749
+ // Finde alle Elemente mit fontSize style
750
+ const walker = document.createTreeWalker(range.commonAncestorContainer, NodeFilter.SHOW_ELEMENT, null);
751
+ const elements = [];
752
+ let node = walker.currentNode;
753
+ while (node) {
754
+ if (node.nodeType === Node.ELEMENT_NODE) {
755
+ const el = node;
756
+ if (range.intersectsNode(el) && el.style.fontSize) {
757
+ elements.push(el);
758
+ }
759
+ }
760
+ node = walker.nextNode();
761
+ }
762
+ elements.forEach(el => {
763
+ el.style.fontSize = '';
764
+ if (!el.style.length) {
765
+ el.removeAttribute('style');
766
+ }
767
+ });
768
+ }
769
+ /**
770
+ * Entfernt Links (behält Text)
771
+ */
772
+ function clearLinks(selection) {
773
+ if (!selection || selection.rangeCount === 0)
774
+ return;
775
+ const range = selection.getRangeAt(0);
776
+ // Finde alle Links in der Selection
777
+ const walker = document.createTreeWalker(range.commonAncestorContainer, NodeFilter.SHOW_ELEMENT, {
778
+ acceptNode: (node) => {
779
+ if (node.nodeType === Node.ELEMENT_NODE) {
780
+ const el = node;
781
+ if (el.tagName.toLowerCase() === 'a' && range.intersectsNode(el)) {
782
+ return NodeFilter.FILTER_ACCEPT;
783
+ }
784
+ }
785
+ return NodeFilter.FILTER_SKIP;
786
+ }
787
+ });
788
+ const links = [];
789
+ let node = walker.currentNode;
790
+ while (node) {
791
+ if (node.nodeType === Node.ELEMENT_NODE && node.tagName.toLowerCase() === 'a') {
792
+ links.push(node);
793
+ }
794
+ node = walker.nextNode();
795
+ }
796
+ // Entferne Links (behält Text)
797
+ links.forEach(link => {
798
+ const parent = link.parentNode;
799
+ if (parent) {
800
+ while (link.firstChild) {
801
+ parent.insertBefore(link.firstChild, link);
802
+ }
803
+ parent.removeChild(link);
804
+ }
805
+ });
806
+ }
807
+
808
+ function domToContent(element) {
809
+ const blocks = [];
810
+ function processNode(node) {
811
+ if (node.nodeType === Node.TEXT_NODE) {
812
+ const text = node.textContent;
813
+ if (text === null || text === undefined)
814
+ return null;
815
+ return { type: "text", text };
816
+ }
817
+ if (node.nodeType === Node.ELEMENT_NODE) {
818
+ const el = node;
819
+ const tagName = el.tagName.toLowerCase();
820
+ if (tagName === "img") {
821
+ const attributes = {};
822
+ const src = el.getAttribute("src");
823
+ const alt = el.getAttribute("alt");
824
+ if (src)
825
+ attributes.src = src;
826
+ if (alt)
827
+ attributes.alt = alt;
828
+ return {
829
+ type: "image",
830
+ attributes: Object.keys(attributes).length > 0
831
+ ? attributes
832
+ : undefined,
833
+ };
834
+ }
835
+ if ([
836
+ "p",
837
+ "div",
838
+ "h1",
839
+ "h2",
840
+ "h3",
841
+ "h4",
842
+ "h5",
843
+ "h6",
844
+ "blockquote",
845
+ "ul",
846
+ "ol",
847
+ "li",
848
+ ].includes(tagName)) {
849
+ const children = [];
850
+ Array.from(el.childNodes).forEach((child) => {
851
+ const processed = processNode(child);
852
+ if (processed)
853
+ children.push(processed);
854
+ });
855
+ const attributes = {};
856
+ if (tagName === "a") {
857
+ const href = el.getAttribute("href");
858
+ if (href)
859
+ attributes.href = href;
860
+ }
861
+ return {
862
+ type: tagName,
863
+ children: children.length > 0 ? children : [],
864
+ attributes: Object.keys(attributes).length > 0
865
+ ? attributes
866
+ : undefined,
867
+ };
868
+ }
869
+ if (["strong", "b", "em", "i", "u", "a", "span"].includes(tagName)) {
870
+ const children = [];
871
+ Array.from(el.childNodes).forEach((child) => {
872
+ const processed = processNode(child);
873
+ if (processed)
874
+ children.push(processed);
875
+ });
876
+ const attributes = {};
877
+ Array.from(el.attributes).forEach((attr) => {
878
+ attributes[attr.name] = attr.value;
879
+ });
880
+ if (tagName === "a") {
881
+ const href = el.getAttribute("href");
882
+ if (href)
883
+ attributes.href = href;
884
+ return {
885
+ type: "link",
886
+ children: children.length > 0 ? children : undefined,
887
+ attributes: Object.keys(attributes).length > 0
888
+ ? attributes
889
+ : undefined,
890
+ };
891
+ }
892
+ if (tagName === "span") {
893
+ const style = el.getAttribute("style");
894
+ if (style) {
895
+ style.split(";").forEach((rule) => {
896
+ const [key, value] = rule
897
+ .split(":")
898
+ .map((s) => s.trim());
899
+ if (key && value) {
900
+ if (key === "font-size") {
901
+ attributes.fontSize = value;
902
+ }
903
+ else if (key === "color") {
904
+ attributes.color = value;
905
+ }
906
+ else if (key === "background-color") {
907
+ attributes.backgroundColor = value;
908
+ }
909
+ }
910
+ });
911
+ }
912
+ }
913
+ return {
914
+ type: tagName === "strong" || tagName === "b"
915
+ ? "bold"
916
+ : tagName === "em" || tagName === "i"
917
+ ? "italic"
918
+ : tagName === "u"
919
+ ? "underline"
920
+ : tagName,
921
+ children: children.length > 0 ? children : undefined,
922
+ attributes: Object.keys(attributes).length > 0
923
+ ? attributes
924
+ : undefined,
925
+ };
926
+ }
927
+ const children = [];
928
+ Array.from(el.childNodes).forEach((child) => {
929
+ const processed = processNode(child);
930
+ if (processed)
931
+ children.push(processed);
932
+ });
933
+ if (children.length > 0) {
934
+ return {
935
+ type: tagName,
936
+ children,
937
+ };
938
+ }
939
+ }
940
+ return null;
941
+ }
942
+ Array.from(element.childNodes).forEach((node) => {
943
+ const processed = processNode(node);
944
+ if (processed) {
945
+ if (processed.type === "text") {
946
+ blocks.push({ type: "p", children: [processed] });
947
+ }
948
+ else {
949
+ blocks.push(processed);
950
+ }
951
+ }
952
+ });
953
+ if (blocks.length === 0) {
954
+ blocks.push({ type: "p", children: [] });
955
+ }
956
+ return { blocks };
957
+ }
958
+ function contentToDOM(content, container, customLinkComponent, customHeadingRenderer) {
959
+ container.innerHTML = "";
960
+ function createNode(node) {
961
+ if (node.type === "text" && node.text !== undefined) {
962
+ return document.createTextNode(node.text);
963
+ }
964
+ if (node.type === "image") {
965
+ const img = document.createElement("img");
966
+ if (node.attributes) {
967
+ if (node.attributes.src)
968
+ img.setAttribute("src", node.attributes.src);
969
+ if (node.attributes.alt)
970
+ img.setAttribute("alt", node.attributes.alt);
971
+ if (node.attributes.uploading === "true") {
972
+ img.setAttribute("data-uploading", "true");
973
+ }
974
+ }
975
+ img.style.maxWidth = "100%";
976
+ img.style.height = "auto";
977
+ img.style.display = "block";
978
+ img.style.margin = "16px 0";
979
+ return img;
980
+ }
981
+ const tagMap = {
982
+ bold: "strong",
983
+ italic: "em",
984
+ underline: "u",
985
+ link: "a",
986
+ };
987
+ let tagName = tagMap[node.type] || node.type;
988
+ if (node.type === "link" && customLinkComponent) {
989
+ tagName = "a";
990
+ if (node.attributes) {
991
+ node.attributes["data-custom-link"] = "true";
992
+ }
993
+ }
994
+ const element = document.createElement(tagName);
995
+ if (node.attributes) {
996
+ Object.entries(node.attributes).forEach(([key, value]) => {
997
+ if (key === "fontSize" ||
998
+ key === "color" ||
999
+ key === "backgroundColor") {
1000
+ const currentStyle = element.getAttribute("style") || "";
1001
+ if (key === "fontSize") {
1002
+ element.setAttribute("style", `${currentStyle}font-size: ${value};`.trim());
1003
+ }
1004
+ else if (key === "color") {
1005
+ element.setAttribute("style", `${currentStyle}color: ${value};`.trim());
1006
+ }
1007
+ else if (key === "backgroundColor") {
1008
+ element.setAttribute("style", `${currentStyle}background-color: ${value};`.trim());
1009
+ }
1010
+ }
1011
+ else {
1012
+ element.setAttribute(key, value);
1013
+ }
1014
+ });
1015
+ }
1016
+ if (node.children) {
1017
+ node.children.forEach((child) => {
1018
+ const childNode = createNode(child);
1019
+ element.appendChild(childNode);
1020
+ });
1021
+ }
1022
+ return element;
1023
+ }
1024
+ content.blocks.forEach((block) => {
1025
+ const blockNode = createNode(block);
1026
+ container.appendChild(blockNode);
1027
+ });
1028
+ }
1029
+ function createEmptyContent() {
1030
+ return {
1031
+ blocks: [{ type: "p", children: [] }],
1032
+ };
1033
+ }
1034
+ function htmlToContent(htmlString) {
1035
+ const tempDiv = document.createElement("div");
1036
+ tempDiv.innerHTML = htmlString.trim();
1037
+ return domToContent(tempDiv);
1038
+ }
1039
+ function contentToHTML(content) {
1040
+ const tempDiv = document.createElement("div");
1041
+ contentToDOM(content, tempDiv);
1042
+ return tempDiv.innerHTML;
1043
+ }
1044
+
1045
+ class HistoryManager {
1046
+ constructor() {
1047
+ this.history = [];
1048
+ this.currentIndex = -1;
1049
+ this.maxHistorySize = 50;
1050
+ }
1051
+ push(content) {
1052
+ // Entferne alle Einträge nach currentIndex (wenn wir zurückgegangen sind)
1053
+ this.history = this.history.slice(0, this.currentIndex + 1);
1054
+ // Füge neuen Eintrag hinzu
1055
+ this.history.push(JSON.parse(JSON.stringify(content))); // Deep clone
1056
+ this.currentIndex++;
1057
+ // Begrenze die Historie
1058
+ if (this.history.length > this.maxHistorySize) {
1059
+ this.history.shift();
1060
+ this.currentIndex--;
1061
+ }
1062
+ }
1063
+ undo() {
1064
+ if (this.canUndo()) {
1065
+ this.currentIndex--;
1066
+ return JSON.parse(JSON.stringify(this.history[this.currentIndex]));
1067
+ }
1068
+ return null;
1069
+ }
1070
+ redo() {
1071
+ if (this.canRedo()) {
1072
+ this.currentIndex++;
1073
+ return JSON.parse(JSON.stringify(this.history[this.currentIndex]));
1074
+ }
1075
+ return null;
1076
+ }
1077
+ canUndo() {
1078
+ return this.currentIndex > 0;
1079
+ }
1080
+ canRedo() {
1081
+ return this.currentIndex < this.history.length - 1;
1082
+ }
1083
+ getCurrent() {
1084
+ if (this.currentIndex >= 0 && this.currentIndex < this.history.length) {
1085
+ return JSON.parse(JSON.stringify(this.history[this.currentIndex]));
1086
+ }
1087
+ return null;
1088
+ }
1089
+ reset() {
1090
+ this.history = [];
1091
+ this.currentIndex = -1;
1092
+ }
1093
+ }
1094
+
1095
+ /**
1096
+ * Erhöht den Einrückungs-Level eines List-Items (Tab)
1097
+ */
1098
+ function indentListItem(selection) {
1099
+ if (!selection || selection.rangeCount === 0)
1100
+ return false;
1101
+ const range = selection.getRangeAt(0);
1102
+ const container = range.commonAncestorContainer;
1103
+ const listItem = container.nodeType === Node.TEXT_NODE
1104
+ ? container.parentElement?.closest('li')
1105
+ : container.closest('li');
1106
+ if (!listItem)
1107
+ return false;
1108
+ const list = listItem.parentElement;
1109
+ if (!list || (list.tagName !== 'UL' && list.tagName !== 'OL'))
1110
+ return false;
1111
+ // Prüfe ob bereits verschachtelt (max depth check)
1112
+ let depth = 0;
1113
+ let current = listItem;
1114
+ while (current) {
1115
+ const parent = current.parentElement;
1116
+ if (parent && (parent.tagName === 'UL' || parent.tagName === 'OL')) {
1117
+ depth++;
1118
+ current = parent.closest('li');
1119
+ }
1120
+ else {
1121
+ break;
1122
+ }
1123
+ }
1124
+ // Max depth: 6 (wie in HTML Standard)
1125
+ if (depth >= 6)
1126
+ return false;
1127
+ // Finde vorheriges List-Item
1128
+ const previousItem = listItem.previousElementSibling;
1129
+ if (previousItem && previousItem.tagName === 'LI') {
1130
+ // Erstelle verschachtelte Liste im vorherigen Item
1131
+ let nestedList = previousItem.querySelector('ul, ol');
1132
+ if (!nestedList) {
1133
+ // Erstelle neue verschachtelte Liste
1134
+ nestedList = document.createElement(list.tagName.toLowerCase());
1135
+ previousItem.appendChild(nestedList);
1136
+ }
1137
+ // Verschiebe aktuelles Item in verschachtelte Liste
1138
+ nestedList.appendChild(listItem);
1139
+ // Cursor setzen
1140
+ const textNode = listItem.firstChild;
1141
+ if (textNode && textNode.nodeType === Node.TEXT_NODE) {
1142
+ range.setStart(textNode, 0);
1143
+ range.collapse(true);
1144
+ }
1145
+ else if (listItem.firstChild) {
1146
+ range.setStart(listItem.firstChild, 0);
1147
+ range.collapse(true);
1148
+ }
1149
+ else {
1150
+ const newText = document.createTextNode('');
1151
+ listItem.appendChild(newText);
1152
+ range.setStart(newText, 0);
1153
+ range.collapse(true);
1154
+ }
1155
+ selection.removeAllRanges();
1156
+ selection.addRange(range);
1157
+ return true;
1158
+ }
1159
+ else {
1160
+ // Kein vorheriges Item - erstelle neue verschachtelte Liste im aktuellen Item
1161
+ const nestedList = document.createElement(list.tagName.toLowerCase());
1162
+ // Verschiebe alle nachfolgenden Items in die verschachtelte Liste
1163
+ let nextSibling = listItem.nextElementSibling;
1164
+ while (nextSibling && nextSibling.tagName === 'LI') {
1165
+ const toMove = nextSibling;
1166
+ nextSibling = nextSibling.nextElementSibling;
1167
+ nestedList.appendChild(toMove);
1168
+ }
1169
+ if (nestedList.children.length > 0) {
1170
+ listItem.appendChild(nestedList);
1171
+ }
1172
+ else {
1173
+ // Wenn keine nachfolgenden Items, erstelle leeres Sub-Item
1174
+ const subItem = document.createElement('li');
1175
+ nestedList.appendChild(subItem);
1176
+ listItem.appendChild(nestedList);
1177
+ // Cursor ins Sub-Item setzen
1178
+ const newText = document.createTextNode('');
1179
+ subItem.appendChild(newText);
1180
+ range.setStart(newText, 0);
1181
+ range.collapse(true);
1182
+ selection.removeAllRanges();
1183
+ selection.addRange(range);
1184
+ }
1185
+ return true;
1186
+ }
1187
+ }
1188
+ /**
1189
+ * Reduziert den Einrückungs-Level eines List-Items (Shift+Tab)
1190
+ */
1191
+ function outdentListItem(selection) {
1192
+ if (!selection || selection.rangeCount === 0)
1193
+ return false;
1194
+ const range = selection.getRangeAt(0);
1195
+ const container = range.commonAncestorContainer;
1196
+ const listItem = container.nodeType === Node.TEXT_NODE
1197
+ ? container.parentElement?.closest('li')
1198
+ : container.closest('li');
1199
+ if (!listItem)
1200
+ return false;
1201
+ const list = listItem.parentElement;
1202
+ if (!list || (list.tagName !== 'UL' && list.tagName !== 'OL'))
1203
+ return false;
1204
+ // Prüfe ob in verschachtelter Liste
1205
+ const parentListItem = list.parentElement;
1206
+ if (!parentListItem || parentListItem.tagName !== 'LI') {
1207
+ // Bereits auf oberstem Level
1208
+ return false;
1209
+ }
1210
+ const parentList = parentListItem.parentElement;
1211
+ if (!parentList || (parentList.tagName !== 'UL' && parentList.tagName !== 'OL')) {
1212
+ return false;
1213
+ }
1214
+ // Verschiebe Item auf oberes Level
1215
+ // Finde Position nach dem Parent-Item
1216
+ const insertAfter = parentListItem;
1217
+ // Verschiebe Item und alle nachfolgenden Items aus der verschachtelten Liste
1218
+ const itemsToMove = [listItem];
1219
+ let nextSibling = listItem.nextElementSibling;
1220
+ while (nextSibling && nextSibling.tagName === 'LI') {
1221
+ itemsToMove.push(nextSibling);
1222
+ nextSibling = nextSibling.nextElementSibling;
1223
+ }
1224
+ // Füge Items nach dem Parent-Item ein
1225
+ itemsToMove.forEach(item => {
1226
+ parentList.insertBefore(item, insertAfter.nextSibling);
1227
+ });
1228
+ // Entferne leere verschachtelte Liste
1229
+ if (list.children.length === 0) {
1230
+ list.remove();
1231
+ }
1232
+ // Cursor setzen
1233
+ const textNode = listItem.firstChild;
1234
+ if (textNode && textNode.nodeType === Node.TEXT_NODE) {
1235
+ range.setStart(textNode, 0);
1236
+ range.collapse(true);
1237
+ }
1238
+ else if (listItem.firstChild) {
1239
+ range.setStart(listItem.firstChild, 0);
1240
+ range.collapse(true);
1241
+ }
1242
+ else {
1243
+ const newText = document.createTextNode('');
1244
+ listItem.appendChild(newText);
1245
+ range.setStart(newText, 0);
1246
+ range.collapse(true);
1247
+ }
1248
+ selection.removeAllRanges();
1249
+ selection.addRange(range);
1250
+ return true;
1251
+ }
1252
+
1253
+ const Toolbar = ({ plugins, editorAPI, className, }) => {
1254
+ const [updateTrigger, setUpdateTrigger] = React.useState(0);
1255
+ const [isClient, setIsClient] = React.useState(false);
1256
+ React.useEffect(() => {
1257
+ setIsClient(true);
1258
+ const handleSelectionChange = () => {
1259
+ setUpdateTrigger((prev) => prev + 1);
1260
+ };
1261
+ const handleMouseUp = () => {
1262
+ setTimeout(handleSelectionChange, 10);
1263
+ };
1264
+ const handleKeyUp = () => {
1265
+ setTimeout(handleSelectionChange, 10);
1266
+ };
1267
+ if (typeof document !== 'undefined') {
1268
+ document.addEventListener("selectionchange", handleSelectionChange);
1269
+ document.addEventListener("mouseup", handleMouseUp);
1270
+ document.addEventListener("keyup", handleKeyUp);
1271
+ }
1272
+ return () => {
1273
+ if (typeof document !== 'undefined') {
1274
+ document.removeEventListener("selectionchange", handleSelectionChange);
1275
+ document.removeEventListener("mouseup", handleMouseUp);
1276
+ document.removeEventListener("keyup", handleKeyUp);
1277
+ }
1278
+ };
1279
+ }, []);
1280
+ const handlePluginClick = (plugin, value) => {
1281
+ if (plugin.canExecute?.(editorAPI) !== false) {
1282
+ if (plugin.execute) {
1283
+ plugin.execute(editorAPI, value);
1284
+ }
1285
+ else if (plugin.command && value !== undefined) {
1286
+ editorAPI.executeCommand(plugin.command, value);
1287
+ }
1288
+ else if (plugin.command) {
1289
+ editorAPI.executeCommand(plugin.command);
1290
+ }
1291
+ setTimeout(() => setUpdateTrigger((prev) => prev + 1), 50);
1292
+ }
1293
+ };
1294
+ const leftPlugins = plugins.filter((p) => p.name !== "clearFormatting");
1295
+ const clearFormattingPlugin = plugins.find((p) => p.name === "clearFormatting");
1296
+ return (jsxRuntime.jsxs("div", { className: `rte-toolbar rte-toolbar-sticky ${className || ""}`, children: [jsxRuntime.jsx("div", { className: "rte-toolbar-left", children: leftPlugins.map((plugin) => {
1297
+ if (!plugin.renderButton)
1298
+ return null;
1299
+ const isActive = isClient && plugin.isActive
1300
+ ? plugin.isActive(editorAPI)
1301
+ : false;
1302
+ const canExecute = isClient && plugin.canExecute
1303
+ ? plugin.canExecute(editorAPI)
1304
+ : true;
1305
+ const currentValue = isClient && plugin.getCurrentValue
1306
+ ? plugin.getCurrentValue(editorAPI)
1307
+ : undefined;
1308
+ const buttonProps = {
1309
+ isActive,
1310
+ onClick: () => handlePluginClick(plugin),
1311
+ disabled: !canExecute,
1312
+ onSelect: (value) => handlePluginClick(plugin, value),
1313
+ editorAPI,
1314
+ currentValue,
1315
+ };
1316
+ return (jsxRuntime.jsx(React.Fragment, { children: plugin.renderButton(buttonProps) }, plugin.name));
1317
+ }) }), clearFormattingPlugin && clearFormattingPlugin.renderButton && (jsxRuntime.jsxs("div", { className: "rte-toolbar-right", children: [jsxRuntime.jsx("div", { className: "rte-toolbar-divider" }), (() => {
1318
+ const isActive = isClient && clearFormattingPlugin.isActive
1319
+ ? clearFormattingPlugin.isActive(editorAPI)
1320
+ : false;
1321
+ const canExecute = isClient && clearFormattingPlugin.canExecute
1322
+ ? clearFormattingPlugin.canExecute(editorAPI)
1323
+ : true;
1324
+ const buttonProps = {
1325
+ isActive,
1326
+ onClick: () => handlePluginClick(clearFormattingPlugin),
1327
+ disabled: !canExecute,
1328
+ editorAPI,
1329
+ };
1330
+ return (jsxRuntime.jsx(React.Fragment, { children: clearFormattingPlugin.renderButton(buttonProps) }, clearFormattingPlugin.name));
1331
+ })()] }))] }));
1332
+ };
1333
+
1334
+ const Editor = ({ initialContent, onChange, plugins: providedPlugins, placeholder = "Text eingeben...", className, toolbarClassName, editorClassName, fontSizes, colors, headings, customLinkComponent, customHeadingRenderer, customRenderer, onEditorAPIReady, theme, onImageUpload, }) => {
1335
+ const plugins = React.useMemo(() => {
1336
+ const allPlugins = [...(providedPlugins || defaultPlugins)];
1337
+ if (fontSizes && fontSizes.length > 0) {
1338
+ allPlugins.push(createFontSizePlugin(fontSizes));
1339
+ }
1340
+ if (colors && colors.length > 0) {
1341
+ allPlugins.push(createTextColorPlugin(colors));
1342
+ allPlugins.push(createBackgroundColorPlugin(colors));
1343
+ }
1344
+ if (headings && headings.length > 0) {
1345
+ allPlugins.push(createHeadingsPlugin(headings));
1346
+ }
1347
+ allPlugins.push(createImagePlugin(onImageUpload));
1348
+ return allPlugins;
1349
+ }, [providedPlugins, fontSizes, colors, headings, onImageUpload]);
1350
+ const editorRef = React.useRef(null);
1351
+ const historyRef = React.useRef(new HistoryManager());
1352
+ const isUpdatingRef = React.useRef(false);
1353
+ const notifyChange = React.useCallback((content) => {
1354
+ if (onChange && !isUpdatingRef.current) {
1355
+ onChange(content);
1356
+ }
1357
+ }, [onChange]);
1358
+ const restoreSelection = React.useCallback((editor) => {
1359
+ if (typeof window === 'undefined' || typeof document === 'undefined')
1360
+ return;
1361
+ const range = document.createRange();
1362
+ const selection = window.getSelection();
1363
+ if (editor.firstChild) {
1364
+ range.setStart(editor.firstChild, 0);
1365
+ range.collapse(true);
1366
+ selection?.removeAllRanges();
1367
+ selection?.addRange(range);
1368
+ }
1369
+ }, []);
1370
+ const editorAPI = React.useMemo(() => {
1371
+ const executeCommand = (command, value) => {
1372
+ const editor = editorRef.current;
1373
+ if (!editor)
1374
+ return false;
1375
+ if (command !== "undo" &&
1376
+ command !== "redo" &&
1377
+ command !== "insertImage") {
1378
+ const currentContent = domToContent(editor);
1379
+ historyRef.current.push(currentContent);
1380
+ }
1381
+ if (command === "undo") {
1382
+ const content = historyRef.current.undo();
1383
+ if (content && editor) {
1384
+ isUpdatingRef.current = true;
1385
+ contentToDOM(content, editor, customLinkComponent);
1386
+ restoreSelection(editor);
1387
+ isUpdatingRef.current = false;
1388
+ notifyChange(content);
1389
+ }
1390
+ return true;
1391
+ }
1392
+ if (command === "redo") {
1393
+ const content = historyRef.current.redo();
1394
+ if (content && editor) {
1395
+ isUpdatingRef.current = true;
1396
+ contentToDOM(content, editor, customLinkComponent);
1397
+ restoreSelection(editor);
1398
+ isUpdatingRef.current = false;
1399
+ notifyChange(content);
1400
+ }
1401
+ return true;
1402
+ }
1403
+ if (command === "insertImage" && value) {
1404
+ let selection = window.getSelection();
1405
+ if (!selection)
1406
+ return false;
1407
+ if (document.activeElement !== editor) {
1408
+ editor.focus();
1409
+ }
1410
+ if (selection.rangeCount === 0) {
1411
+ const range = document.createRange();
1412
+ if (editor.childNodes.length > 0) {
1413
+ const lastChild = editor.childNodes[editor.childNodes.length - 1];
1414
+ range.setStartAfter(lastChild);
1415
+ range.collapse(true);
1416
+ }
1417
+ else {
1418
+ const img = document.createElement("img");
1419
+ img.setAttribute("src", value);
1420
+ img.setAttribute("alt", "");
1421
+ img.style.maxWidth = "100%";
1422
+ img.style.height = "auto";
1423
+ img.style.display = "block";
1424
+ img.style.margin = "16px 0";
1425
+ editor.appendChild(img);
1426
+ const newRange = document.createRange();
1427
+ newRange.setStartAfter(img);
1428
+ newRange.collapse(true);
1429
+ selection.removeAllRanges();
1430
+ selection.addRange(newRange);
1431
+ isUpdatingRef.current = true;
1432
+ setTimeout(() => {
1433
+ if (editor) {
1434
+ const currentContent = domToContent(editor);
1435
+ historyRef.current.push(currentContent);
1436
+ isUpdatingRef.current = false;
1437
+ notifyChange(currentContent);
1438
+ }
1439
+ }, 0);
1440
+ return true;
1441
+ }
1442
+ selection.removeAllRanges();
1443
+ selection.addRange(range);
1444
+ }
1445
+ if (selection.rangeCount === 0)
1446
+ return false;
1447
+ const range = selection.getRangeAt(0);
1448
+ const container = range.commonAncestorContainer;
1449
+ let parentElement = null;
1450
+ if (container.nodeType === Node.TEXT_NODE) {
1451
+ parentElement = container.parentElement;
1452
+ }
1453
+ else if (container.nodeType === Node.ELEMENT_NODE) {
1454
+ parentElement = container;
1455
+ }
1456
+ const img = document.createElement("img");
1457
+ img.setAttribute("src", value);
1458
+ img.setAttribute("alt", "");
1459
+ img.style.maxWidth = "100%";
1460
+ img.style.height = "auto";
1461
+ img.style.display = "block";
1462
+ img.style.margin = "16px 0";
1463
+ if (parentElement &&
1464
+ parentElement !== editor &&
1465
+ (parentElement.tagName === "P" ||
1466
+ parentElement.tagName === "DIV" ||
1467
+ parentElement.tagName === "H1" ||
1468
+ parentElement.tagName === "H2" ||
1469
+ parentElement.tagName === "H3" ||
1470
+ parentElement.tagName === "H4" ||
1471
+ parentElement.tagName === "H5" ||
1472
+ parentElement.tagName === "H6")) {
1473
+ if (parentElement.nextSibling) {
1474
+ editor.insertBefore(img, parentElement.nextSibling);
1475
+ }
1476
+ else {
1477
+ editor.appendChild(img);
1478
+ }
1479
+ }
1480
+ else {
1481
+ try {
1482
+ range.insertNode(img);
1483
+ }
1484
+ catch (e) {
1485
+ editor.appendChild(img);
1486
+ }
1487
+ }
1488
+ const newRange = document.createRange();
1489
+ newRange.setStartAfter(img);
1490
+ newRange.collapse(true);
1491
+ selection.removeAllRanges();
1492
+ selection.addRange(newRange);
1493
+ isUpdatingRef.current = true;
1494
+ setTimeout(() => {
1495
+ if (editor) {
1496
+ const currentContent = domToContent(editor);
1497
+ historyRef.current.push(currentContent);
1498
+ isUpdatingRef.current = false;
1499
+ notifyChange(currentContent);
1500
+ }
1501
+ }, 0);
1502
+ return true;
1503
+ }
1504
+ const selection = window.getSelection();
1505
+ let savedRange = null;
1506
+ if (selection && selection.rangeCount > 0) {
1507
+ savedRange = selection.getRangeAt(0).cloneRange();
1508
+ }
1509
+ if (document.activeElement !== editor) {
1510
+ editor.focus();
1511
+ }
1512
+ if (!selection || selection.rangeCount === 0) {
1513
+ const range = document.createRange();
1514
+ if (editor.childNodes.length > 0) {
1515
+ const lastChild = editor.childNodes[editor.childNodes.length - 1];
1516
+ if (lastChild.nodeType === Node.TEXT_NODE) {
1517
+ range.setStart(lastChild, lastChild.textContent?.length || 0);
1518
+ range.setEnd(lastChild, lastChild.textContent?.length || 0);
1519
+ }
1520
+ else {
1521
+ range.selectNodeContents(lastChild);
1522
+ range.collapse(false);
1523
+ }
1524
+ }
1525
+ else {
1526
+ const p = document.createElement("p");
1527
+ editor.appendChild(p);
1528
+ const textNode = document.createTextNode("");
1529
+ p.appendChild(textNode);
1530
+ range.setStart(textNode, 0);
1531
+ range.setEnd(textNode, 0);
1532
+ }
1533
+ selection?.removeAllRanges();
1534
+ selection?.addRange(range);
1535
+ }
1536
+ else if (savedRange) {
1537
+ selection.removeAllRanges();
1538
+ selection.addRange(savedRange);
1539
+ }
1540
+ document.execCommand(command, false, value);
1541
+ setTimeout(() => {
1542
+ if (editor && !isUpdatingRef.current) {
1543
+ const content = domToContent(editor);
1544
+ notifyChange(content);
1545
+ }
1546
+ }, 0);
1547
+ return true;
1548
+ };
1549
+ return {
1550
+ executeCommand,
1551
+ getSelection: () => {
1552
+ if (typeof window === 'undefined')
1553
+ return null;
1554
+ return window.getSelection();
1555
+ },
1556
+ getContent: () => {
1557
+ const editor = editorRef.current;
1558
+ if (!editor)
1559
+ return createEmptyContent();
1560
+ return domToContent(editor);
1561
+ },
1562
+ setContent: (content) => {
1563
+ const editor = editorRef.current;
1564
+ if (!editor)
1565
+ return;
1566
+ isUpdatingRef.current = true;
1567
+ contentToDOM(content, editor, customLinkComponent);
1568
+ historyRef.current.push(content);
1569
+ isUpdatingRef.current = false;
1570
+ notifyChange(content);
1571
+ },
1572
+ insertBlock: (type, attributes) => {
1573
+ const selection = window.getSelection();
1574
+ if (!selection || selection.rangeCount === 0)
1575
+ return;
1576
+ const range = selection.getRangeAt(0);
1577
+ const block = document.createElement(type);
1578
+ if (attributes) {
1579
+ Object.entries(attributes).forEach(([key, value]) => {
1580
+ block.setAttribute(key, value);
1581
+ });
1582
+ }
1583
+ range.insertNode(block);
1584
+ const textNode = document.createTextNode("\u200B"); // Zero-width space
1585
+ block.appendChild(textNode);
1586
+ // Cursor setzen
1587
+ range.setStartAfter(textNode);
1588
+ range.collapse(true);
1589
+ selection.removeAllRanges();
1590
+ selection.addRange(range);
1591
+ const editor = editorRef.current;
1592
+ if (editor) {
1593
+ const content = domToContent(editor);
1594
+ notifyChange(content);
1595
+ }
1596
+ },
1597
+ insertInline: (type, attributes) => {
1598
+ const selection = window.getSelection();
1599
+ if (!selection || selection.rangeCount === 0)
1600
+ return;
1601
+ const range = selection.getRangeAt(0);
1602
+ const inline = document.createElement(type);
1603
+ if (attributes) {
1604
+ Object.entries(attributes).forEach(([key, value]) => {
1605
+ inline.setAttribute(key, value);
1606
+ });
1607
+ }
1608
+ try {
1609
+ range.surroundContents(inline);
1610
+ }
1611
+ catch (e) {
1612
+ // Falls surroundContents fehlschlägt, versuche es anders
1613
+ const contents = range.extractContents();
1614
+ inline.appendChild(contents);
1615
+ range.insertNode(inline);
1616
+ }
1617
+ // Cursor setzen
1618
+ range.setStartAfter(inline);
1619
+ range.collapse(true);
1620
+ selection.removeAllRanges();
1621
+ selection.addRange(range);
1622
+ const editor = editorRef.current;
1623
+ if (editor) {
1624
+ const content = domToContent(editor);
1625
+ notifyChange(content);
1626
+ }
1627
+ },
1628
+ undo: () => {
1629
+ executeCommand("undo");
1630
+ },
1631
+ redo: () => {
1632
+ executeCommand("redo");
1633
+ },
1634
+ canUndo: () => {
1635
+ return historyRef.current.canUndo();
1636
+ },
1637
+ canRedo: () => {
1638
+ return historyRef.current.canRedo();
1639
+ },
1640
+ importHtml: (htmlString) => {
1641
+ const content = htmlToContent(htmlString);
1642
+ const editor = editorRef.current;
1643
+ if (editor) {
1644
+ isUpdatingRef.current = true;
1645
+ contentToDOM(content, editor, customLinkComponent);
1646
+ historyRef.current.push(content);
1647
+ isUpdatingRef.current = false;
1648
+ notifyChange(content);
1649
+ }
1650
+ return content;
1651
+ },
1652
+ exportHtml: () => {
1653
+ const editor = editorRef.current;
1654
+ if (!editor)
1655
+ return "";
1656
+ const content = domToContent(editor);
1657
+ return contentToHTML(content);
1658
+ },
1659
+ clearFormatting: () => {
1660
+ const editor = editorRef.current;
1661
+ if (!editor)
1662
+ return;
1663
+ const selection = window.getSelection();
1664
+ if (selection && selection.rangeCount > 0) {
1665
+ const currentContent = domToContent(editor);
1666
+ historyRef.current.push(currentContent);
1667
+ clearFormatting(selection);
1668
+ setTimeout(() => {
1669
+ if (editor) {
1670
+ const content = domToContent(editor);
1671
+ notifyChange(content);
1672
+ }
1673
+ }, 0);
1674
+ }
1675
+ },
1676
+ clearTextColor: () => {
1677
+ const editor = editorRef.current;
1678
+ if (!editor)
1679
+ return;
1680
+ const selection = window.getSelection();
1681
+ if (selection && selection.rangeCount > 0) {
1682
+ const currentContent = domToContent(editor);
1683
+ historyRef.current.push(currentContent);
1684
+ clearTextColor(selection);
1685
+ setTimeout(() => {
1686
+ if (editor) {
1687
+ const content = domToContent(editor);
1688
+ notifyChange(content);
1689
+ }
1690
+ }, 0);
1691
+ }
1692
+ },
1693
+ clearBackgroundColor: () => {
1694
+ const editor = editorRef.current;
1695
+ if (!editor)
1696
+ return;
1697
+ const selection = window.getSelection();
1698
+ if (selection && selection.rangeCount > 0) {
1699
+ const currentContent = domToContent(editor);
1700
+ historyRef.current.push(currentContent);
1701
+ clearBackgroundColor(selection);
1702
+ setTimeout(() => {
1703
+ if (editor) {
1704
+ const content = domToContent(editor);
1705
+ notifyChange(content);
1706
+ }
1707
+ }, 0);
1708
+ }
1709
+ },
1710
+ clearFontSize: () => {
1711
+ const editor = editorRef.current;
1712
+ if (!editor)
1713
+ return;
1714
+ const selection = window.getSelection();
1715
+ if (selection && selection.rangeCount > 0) {
1716
+ const currentContent = domToContent(editor);
1717
+ historyRef.current.push(currentContent);
1718
+ clearFontSize(selection);
1719
+ setTimeout(() => {
1720
+ if (editor) {
1721
+ const content = domToContent(editor);
1722
+ notifyChange(content);
1723
+ }
1724
+ }, 0);
1725
+ }
1726
+ },
1727
+ clearLinks: () => {
1728
+ const editor = editorRef.current;
1729
+ if (!editor)
1730
+ return;
1731
+ const selection = window.getSelection();
1732
+ if (selection && selection.rangeCount > 0) {
1733
+ const currentContent = domToContent(editor);
1734
+ historyRef.current.push(currentContent);
1735
+ clearLinks(selection);
1736
+ setTimeout(() => {
1737
+ if (editor) {
1738
+ const content = domToContent(editor);
1739
+ notifyChange(content);
1740
+ }
1741
+ }, 0);
1742
+ }
1743
+ },
1744
+ };
1745
+ }, [
1746
+ notifyChange,
1747
+ restoreSelection,
1748
+ customLinkComponent,
1749
+ customHeadingRenderer,
1750
+ ]);
1751
+ React.useEffect(() => {
1752
+ if (onEditorAPIReady) {
1753
+ onEditorAPIReady(editorAPI);
1754
+ }
1755
+ }, [editorAPI, onEditorAPIReady]);
1756
+ const isInitializedRef = React.useRef(false);
1757
+ React.useEffect(() => {
1758
+ const editor = editorRef.current;
1759
+ if (!editor || isInitializedRef.current)
1760
+ return;
1761
+ const content = initialContent || createEmptyContent();
1762
+ isUpdatingRef.current = true;
1763
+ contentToDOM(content, editor, customLinkComponent);
1764
+ historyRef.current.push(content);
1765
+ isUpdatingRef.current = false;
1766
+ isInitializedRef.current = true;
1767
+ let inputTimeout = null;
1768
+ const handleInput = () => {
1769
+ if (isUpdatingRef.current)
1770
+ return;
1771
+ const content = domToContent(editor);
1772
+ notifyChange(content);
1773
+ if (inputTimeout) {
1774
+ clearTimeout(inputTimeout);
1775
+ }
1776
+ inputTimeout = setTimeout(() => {
1777
+ historyRef.current.push(content);
1778
+ inputTimeout = null;
1779
+ }, 300);
1780
+ };
1781
+ const handleKeyDown = (e) => {
1782
+ const isModifierPressed = e.metaKey || e.ctrlKey;
1783
+ if (e.key === "Tab" && !isModifierPressed && !e.altKey) {
1784
+ const selection = window.getSelection();
1785
+ const isSelectionInEditor = selection &&
1786
+ selection.rangeCount > 0 &&
1787
+ editor.contains(selection.getRangeAt(0).commonAncestorContainer);
1788
+ const isEditorFocused = document.activeElement === editor ||
1789
+ editor.contains(document.activeElement) ||
1790
+ isSelectionInEditor;
1791
+ if (!isEditorFocused) {
1792
+ return;
1793
+ }
1794
+ e.preventDefault();
1795
+ e.stopPropagation();
1796
+ if (isSelectionInEditor &&
1797
+ selection &&
1798
+ selection.rangeCount > 0) {
1799
+ const range = selection.getRangeAt(0);
1800
+ const container = range.commonAncestorContainer;
1801
+ const listItem = container.nodeType === Node.TEXT_NODE
1802
+ ? container.parentElement?.closest("li")
1803
+ : container.closest("li");
1804
+ if (listItem && editor.contains(listItem)) {
1805
+ e.stopImmediatePropagation();
1806
+ const currentContent = domToContent(editor);
1807
+ historyRef.current.push(currentContent);
1808
+ if (e.shiftKey) {
1809
+ outdentListItem(selection);
1810
+ }
1811
+ else {
1812
+ indentListItem(selection);
1813
+ }
1814
+ setTimeout(() => {
1815
+ if (editor) {
1816
+ const content = domToContent(editor);
1817
+ notifyChange(content);
1818
+ }
1819
+ }, 0);
1820
+ return;
1821
+ }
1822
+ }
1823
+ document.execCommand("insertText", false, "\t");
1824
+ }
1825
+ if (isModifierPressed && e.key === "z" && !e.shiftKey) {
1826
+ e.preventDefault();
1827
+ e.stopPropagation();
1828
+ editorAPI.undo();
1829
+ }
1830
+ else if (isModifierPressed &&
1831
+ (e.key === "y" || (e.key === "z" && e.shiftKey))) {
1832
+ e.preventDefault();
1833
+ e.stopPropagation();
1834
+ editorAPI.redo();
1835
+ }
1836
+ };
1837
+ editor.addEventListener("input", handleInput);
1838
+ editor.addEventListener("keydown", handleKeyDown);
1839
+ return () => {
1840
+ editor.removeEventListener("input", handleInput);
1841
+ editor.removeEventListener("keydown", handleKeyDown);
1842
+ if (inputTimeout) {
1843
+ clearTimeout(inputTimeout);
1844
+ }
1845
+ };
1846
+ }, [editorAPI, notifyChange]);
1847
+ const handlePaste = (e) => {
1848
+ e.preventDefault();
1849
+ const html = e.clipboardData.getData("text/html");
1850
+ const text = e.clipboardData.getData("text/plain");
1851
+ if (html) {
1852
+ try {
1853
+ const pastedContent = htmlToContent(html);
1854
+ const editor = editorRef.current;
1855
+ if (!editor)
1856
+ return;
1857
+ const selection = window.getSelection();
1858
+ if (selection && selection.rangeCount > 0) {
1859
+ const range = selection.getRangeAt(0);
1860
+ range.deleteContents();
1861
+ const tempDiv = document.createElement("div");
1862
+ contentToDOM(pastedContent, tempDiv, customLinkComponent, customHeadingRenderer);
1863
+ const fragment = document.createDocumentFragment();
1864
+ while (tempDiv.firstChild) {
1865
+ fragment.appendChild(tempDiv.firstChild);
1866
+ }
1867
+ range.insertNode(fragment);
1868
+ if (fragment.lastChild) {
1869
+ range.setStartAfter(fragment.lastChild);
1870
+ range.collapse(true);
1871
+ }
1872
+ selection.removeAllRanges();
1873
+ selection.addRange(range);
1874
+ const content = domToContent(editor);
1875
+ notifyChange(content);
1876
+ }
1877
+ }
1878
+ catch (error) {
1879
+ document.execCommand("insertText", false, text);
1880
+ }
1881
+ }
1882
+ else if (text) {
1883
+ document.execCommand("insertText", false, text);
1884
+ }
1885
+ };
1886
+ const containerStyle = theme
1887
+ ? {
1888
+ ...(theme.borderColor &&
1889
+ {
1890
+ "--rte-border-color": theme.borderColor,
1891
+ }),
1892
+ ...(theme.borderRadius &&
1893
+ {
1894
+ "--rte-border-radius": `${theme.borderRadius}px`,
1895
+ }),
1896
+ ...(theme.toolbarBg &&
1897
+ {
1898
+ "--rte-toolbar-bg": theme.toolbarBg,
1899
+ }),
1900
+ ...(theme.buttonHoverBg &&
1901
+ {
1902
+ "--rte-button-hover-bg": theme.buttonHoverBg,
1903
+ }),
1904
+ ...(theme.contentBg &&
1905
+ {
1906
+ "--rte-content-bg": theme.contentBg,
1907
+ }),
1908
+ ...(theme.primaryColor &&
1909
+ {
1910
+ "--rte-primary-color": theme.primaryColor,
1911
+ }),
1912
+ }
1913
+ : {};
1914
+ return (jsxRuntime.jsxs("div", { className: `rte-container ${className || ""}`, style: containerStyle, children: [jsxRuntime.jsx(Toolbar, { plugins: plugins, editorAPI: editorAPI, className: toolbarClassName }), jsxRuntime.jsx("div", { ref: editorRef, contentEditable: true, className: `rte-editor ${editorClassName || ""}`, "data-placeholder": placeholder, onPaste: handlePaste, suppressContentEditableWarning: true })] }));
1915
+ };
1916
+
1917
+ /**
1918
+ * Link-Plugin mit verbesserter Funktionalität
1919
+ */
1920
+ function createLinkPlugin() {
1921
+ return {
1922
+ name: 'link',
1923
+ type: 'inline',
1924
+ command: 'createLink',
1925
+ renderButton: (props) => (jsxRuntime.jsx("button", { type: "button", onClick: props.onClick, disabled: props.disabled, className: `rte-toolbar-button ${props.isActive ? 'rte-toolbar-button-active' : ''}`, title: "Link einf\u00FCgen", "aria-label": "Link einf\u00FCgen", children: jsxRuntime.jsx(IconWrapper, { icon: "mdi:link", width: 18, height: 18 }) })),
1926
+ execute: (editor) => {
1927
+ const selection = editor.getSelection();
1928
+ if (!selection || selection.rangeCount === 0)
1929
+ return;
1930
+ const range = selection.getRangeAt(0);
1931
+ const container = range.commonAncestorContainer;
1932
+ const element = container.nodeType === Node.TEXT_NODE
1933
+ ? container.parentElement
1934
+ : container;
1935
+ // Prüfe ob bereits ein Link vorhanden ist
1936
+ const existingLink = element?.closest('a');
1937
+ if (existingLink) {
1938
+ // Link entfernen
1939
+ const parent = existingLink.parentNode;
1940
+ if (parent) {
1941
+ while (existingLink.firstChild) {
1942
+ parent.insertBefore(existingLink.firstChild, existingLink);
1943
+ }
1944
+ parent.removeChild(existingLink);
1945
+ // Content aktualisieren
1946
+ const editorEl = editor.getSelection()?.anchorNode;
1947
+ if (editorEl) {
1948
+ const content = editor.getContent();
1949
+ editor.setContent(content);
1950
+ }
1951
+ }
1952
+ }
1953
+ else {
1954
+ // Neuen Link einfügen
1955
+ const url = prompt('URL eingeben:');
1956
+ if (url) {
1957
+ editor.executeCommand('createLink', url);
1958
+ }
1959
+ }
1960
+ },
1961
+ isActive: (editor) => {
1962
+ const selection = editor.getSelection();
1963
+ if (!selection || selection.rangeCount === 0)
1964
+ return false;
1965
+ const range = selection.getRangeAt(0);
1966
+ const container = range.commonAncestorContainer;
1967
+ const element = container.nodeType === Node.TEXT_NODE
1968
+ ? container.parentElement
1969
+ : container;
1970
+ if (!element)
1971
+ return false;
1972
+ return element.closest('a') !== null;
1973
+ },
1974
+ getCurrentValue: (editor) => {
1975
+ const selection = editor.getSelection();
1976
+ if (!selection || selection.rangeCount === 0)
1977
+ return undefined;
1978
+ const range = selection.getRangeAt(0);
1979
+ const container = range.commonAncestorContainer;
1980
+ const element = container.nodeType === Node.TEXT_NODE
1981
+ ? container.parentElement
1982
+ : container;
1983
+ if (!element)
1984
+ return undefined;
1985
+ const link = element.closest('a');
1986
+ return link ? link.href : undefined;
1987
+ },
1988
+ canExecute: (editor) => {
1989
+ const selection = editor.getSelection();
1990
+ return selection !== null && selection.rangeCount > 0;
1991
+ },
1992
+ };
1993
+ }
1994
+ const linkPlugin = createLinkPlugin();
1995
+ /**
1996
+ * Blockquote-Plugin
1997
+ */
1998
+ const blockquotePlugin = {
1999
+ name: 'blockquote',
2000
+ type: 'block',
2001
+ command: 'formatBlock',
2002
+ renderButton: (props) => (jsxRuntime.jsx("button", { type: "button", onClick: props.onClick, disabled: props.disabled, className: `rte-toolbar-button ${props.isActive ? 'rte-toolbar-button-active' : ''}`, title: "Zitat", "aria-label": "Zitat", children: jsxRuntime.jsx(IconWrapper, { icon: "mdi:format-quote-close", width: 18, height: 18 }) })),
2003
+ execute: (editor) => {
2004
+ const selection = editor.getSelection();
2005
+ if (!selection || selection.rangeCount === 0)
2006
+ return;
2007
+ const range = selection.getRangeAt(0);
2008
+ const container = range.commonAncestorContainer;
2009
+ const element = container.nodeType === Node.TEXT_NODE
2010
+ ? container.parentElement
2011
+ : container;
2012
+ if (!element)
2013
+ return;
2014
+ const isBlockquote = element.closest('blockquote') !== null;
2015
+ if (isBlockquote) {
2016
+ editor.executeCommand('formatBlock', '<p>');
2017
+ }
2018
+ else {
2019
+ editor.executeCommand('formatBlock', '<blockquote>');
2020
+ }
2021
+ },
2022
+ isActive: (editor) => {
2023
+ const selection = editor.getSelection();
2024
+ if (!selection || selection.rangeCount === 0)
2025
+ return false;
2026
+ const range = selection.getRangeAt(0);
2027
+ const container = range.commonAncestorContainer;
2028
+ const element = container.nodeType === Node.TEXT_NODE
2029
+ ? container.parentElement
2030
+ : container;
2031
+ if (!element)
2032
+ return false;
2033
+ return element.closest('blockquote') !== null;
2034
+ },
2035
+ canExecute: (editor) => {
2036
+ const selection = editor.getSelection();
2037
+ return selection !== null && selection.rangeCount > 0;
2038
+ },
2039
+ };
2040
+ /**
2041
+ * Unordered List Plugin
2042
+ */
2043
+ const unorderedListPlugin = {
2044
+ name: 'unorderedList',
2045
+ type: 'block',
2046
+ command: 'insertUnorderedList',
2047
+ renderButton: (props) => (jsxRuntime.jsx("button", { type: "button", onClick: props.onClick, disabled: props.disabled, className: `rte-toolbar-button ${props.isActive ? 'rte-toolbar-button-active' : ''}`, title: "Aufz\u00E4hlungsliste", "aria-label": "Aufz\u00E4hlungsliste", children: jsxRuntime.jsx(IconWrapper, { icon: "mdi:format-list-bulleted", width: 18, height: 18 }) })),
2048
+ execute: (editor) => {
2049
+ editor.executeCommand('insertUnorderedList');
2050
+ },
2051
+ isActive: (editor) => {
2052
+ if (typeof document === 'undefined')
2053
+ return false;
2054
+ return document.queryCommandState('insertUnorderedList');
2055
+ },
2056
+ canExecute: (editor) => {
2057
+ const selection = editor.getSelection();
2058
+ return selection !== null && selection.rangeCount > 0;
2059
+ },
2060
+ };
2061
+ /**
2062
+ * Ordered List Plugin
2063
+ */
2064
+ const orderedListPlugin = {
2065
+ name: 'orderedList',
2066
+ type: 'block',
2067
+ command: 'insertOrderedList',
2068
+ renderButton: (props) => (jsxRuntime.jsx("button", { type: "button", onClick: props.onClick, disabled: props.disabled, className: `rte-toolbar-button ${props.isActive ? 'rte-toolbar-button-active' : ''}`, title: "Nummerierte Liste", "aria-label": "Nummerierte Liste", children: jsxRuntime.jsx(IconWrapper, { icon: "mdi:format-list-numbered", width: 18, height: 18 }) })),
2069
+ execute: (editor) => {
2070
+ editor.executeCommand('insertOrderedList');
2071
+ },
2072
+ isActive: (editor) => {
2073
+ if (typeof document === 'undefined')
2074
+ return false;
2075
+ return document.queryCommandState('insertOrderedList');
2076
+ },
2077
+ canExecute: (editor) => {
2078
+ const selection = editor.getSelection();
2079
+ return selection !== null && selection.rangeCount > 0;
2080
+ },
2081
+ };
2082
+
2083
+ exports.Dropdown = Dropdown;
2084
+ exports.Editor = Editor;
2085
+ exports.HistoryManager = HistoryManager;
2086
+ exports.Toolbar = Toolbar;
2087
+ exports.blockquotePlugin = blockquotePlugin;
2088
+ exports.boldPlugin = boldPlugin;
2089
+ exports.clearFormattingPlugin = clearFormattingPlugin;
2090
+ exports.contentToDOM = contentToDOM;
2091
+ exports.contentToHTML = contentToHTML;
2092
+ exports.createBackgroundColorPlugin = createBackgroundColorPlugin;
2093
+ exports.createEmptyContent = createEmptyContent;
2094
+ exports.createFontSizePlugin = createFontSizePlugin;
2095
+ exports.createHeadingsPlugin = createHeadingsPlugin;
2096
+ exports.createImagePlugin = createImagePlugin;
2097
+ exports.createLinkPlugin = createLinkPlugin;
2098
+ exports.createTextColorPlugin = createTextColorPlugin;
2099
+ exports.default = Editor;
2100
+ exports.defaultPlugins = defaultPlugins;
2101
+ exports.domToContent = domToContent;
2102
+ exports.getCurrentBackgroundColor = getCurrentBackgroundColor;
2103
+ exports.getCurrentFontSize = getCurrentFontSize;
2104
+ exports.getCurrentHeading = getCurrentHeading;
2105
+ exports.getCurrentTextColor = getCurrentTextColor;
2106
+ exports.htmlToContent = htmlToContent;
2107
+ exports.indentListItem = indentListItem;
2108
+ exports.italicPlugin = italicPlugin;
2109
+ exports.linkPlugin = linkPlugin;
2110
+ exports.orderedListPlugin = orderedListPlugin;
2111
+ exports.outdentListItem = outdentListItem;
2112
+ exports.redoPlugin = redoPlugin;
2113
+ exports.underlinePlugin = underlinePlugin;
2114
+ exports.undoPlugin = undoPlugin;
2115
+ exports.unorderedListPlugin = unorderedListPlugin;
2116
+ //# sourceMappingURL=index.js.map