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