@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,122 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Plugin, EditorAPI, ButtonProps } from '../types';
|
|
3
|
+
import { Dropdown } from '../components/Dropdown';
|
|
4
|
+
import { getCurrentTextColor, getCurrentBackgroundColor } from '../utils/stateReflection';
|
|
5
|
+
|
|
6
|
+
const defaultColors = [
|
|
7
|
+
{ value: '#000000', label: 'Schwarz', color: '#000000' },
|
|
8
|
+
{ value: '#333333', label: 'Dunkelgrau', color: '#333333' },
|
|
9
|
+
{ value: '#666666', label: 'Grau', color: '#666666' },
|
|
10
|
+
{ value: '#ff0000', label: 'Rot', color: '#ff0000' },
|
|
11
|
+
{ value: '#0000ff', label: 'Blau', color: '#0000ff' },
|
|
12
|
+
{ value: '#00aa00', label: 'Grün', color: '#00aa00' },
|
|
13
|
+
{ value: '#ffaa00', label: 'Orange', color: '#ffaa00' },
|
|
14
|
+
{ value: '#aa00ff', label: 'Lila', color: '#aa00ff' },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export function createTextColorPlugin(colors: string[] = defaultColors.map(c => c.value)): Plugin {
|
|
18
|
+
// Finde Labels für bekannte Farben
|
|
19
|
+
const getColorLabel = (color: string): string => {
|
|
20
|
+
const found = defaultColors.find(c => c.value.toLowerCase() === color.toLowerCase());
|
|
21
|
+
return found ? found.label : color;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const colorOptions = colors.map(color => ({
|
|
25
|
+
value: color,
|
|
26
|
+
label: getColorLabel(color),
|
|
27
|
+
color,
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
name: 'textColor',
|
|
32
|
+
type: 'inline',
|
|
33
|
+
renderButton: (props: ButtonProps & { onSelect?: (value: string) => void; editorAPI?: EditorAPI; currentValue?: string }) => {
|
|
34
|
+
// Aktuelle Textfarbe aus State Reflection
|
|
35
|
+
const currentValue = props.currentValue || (props.editorAPI ? getCurrentTextColor(props.editorAPI) : undefined);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Dropdown
|
|
39
|
+
icon="mdi:format-color-text"
|
|
40
|
+
label="Textfarbe"
|
|
41
|
+
options={colorOptions.map(opt => ({
|
|
42
|
+
value: opt.value,
|
|
43
|
+
label: opt.label,
|
|
44
|
+
color: opt.color,
|
|
45
|
+
}))}
|
|
46
|
+
onSelect={(value) => {
|
|
47
|
+
if (props.onSelect) {
|
|
48
|
+
props.onSelect(value);
|
|
49
|
+
} else {
|
|
50
|
+
props.onClick();
|
|
51
|
+
}
|
|
52
|
+
}}
|
|
53
|
+
currentValue={currentValue}
|
|
54
|
+
disabled={props.disabled}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
},
|
|
58
|
+
execute: (editor: EditorAPI, value?: string) => {
|
|
59
|
+
if (value) {
|
|
60
|
+
editor.executeCommand('foreColor', value);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
canExecute: () => true,
|
|
64
|
+
getCurrentValue: (editor: EditorAPI) => {
|
|
65
|
+
return getCurrentTextColor(editor);
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function createBackgroundColorPlugin(colors: string[] = defaultColors.map(c => c.value)): Plugin {
|
|
71
|
+
// Finde Labels für bekannte Farben
|
|
72
|
+
const getColorLabel = (color: string): string => {
|
|
73
|
+
const found = defaultColors.find(c => c.value.toLowerCase() === color.toLowerCase());
|
|
74
|
+
return found ? found.label : color;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const colorOptions = colors.map(color => ({
|
|
78
|
+
value: color,
|
|
79
|
+
label: getColorLabel(color),
|
|
80
|
+
color,
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
name: 'backgroundColor',
|
|
85
|
+
type: 'inline',
|
|
86
|
+
renderButton: (props: ButtonProps & { onSelect?: (value: string) => void; editorAPI?: EditorAPI; currentValue?: string }) => {
|
|
87
|
+
// Aktuelle Hintergrundfarbe aus State Reflection
|
|
88
|
+
const currentValue = props.currentValue || (props.editorAPI ? getCurrentBackgroundColor(props.editorAPI) : undefined);
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<Dropdown
|
|
92
|
+
icon="mdi:format-color-fill"
|
|
93
|
+
label="Hintergrundfarbe"
|
|
94
|
+
options={colorOptions.map(opt => ({
|
|
95
|
+
value: opt.value,
|
|
96
|
+
label: opt.label,
|
|
97
|
+
color: opt.color,
|
|
98
|
+
}))}
|
|
99
|
+
onSelect={(value) => {
|
|
100
|
+
if (props.onSelect) {
|
|
101
|
+
props.onSelect(value);
|
|
102
|
+
} else {
|
|
103
|
+
props.onClick();
|
|
104
|
+
}
|
|
105
|
+
}}
|
|
106
|
+
currentValue={currentValue}
|
|
107
|
+
disabled={props.disabled}
|
|
108
|
+
/>
|
|
109
|
+
);
|
|
110
|
+
},
|
|
111
|
+
execute: (editor: EditorAPI, value?: string) => {
|
|
112
|
+
if (value) {
|
|
113
|
+
editor.executeCommand('backColor', value);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
canExecute: () => true,
|
|
117
|
+
getCurrentValue: (editor: EditorAPI) => {
|
|
118
|
+
return getCurrentBackgroundColor(editor);
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Plugin, EditorAPI, ButtonProps } from '../types';
|
|
3
|
+
import { Dropdown } from '../components/Dropdown';
|
|
4
|
+
import { getCurrentFontSize } from '../utils/stateReflection';
|
|
5
|
+
|
|
6
|
+
export function createFontSizePlugin(fontSizes: number[] = [12, 14, 16, 18, 20, 24]): Plugin {
|
|
7
|
+
return {
|
|
8
|
+
name: 'fontSize',
|
|
9
|
+
type: 'inline',
|
|
10
|
+
renderButton: (props: ButtonProps & { fontSizes?: number[]; onSelect?: (value: string) => void; editorAPI?: EditorAPI; currentValue?: string }) => {
|
|
11
|
+
const sizes = props.fontSizes || fontSizes;
|
|
12
|
+
const options = sizes.map(size => ({
|
|
13
|
+
value: size.toString(),
|
|
14
|
+
label: `${size}px`,
|
|
15
|
+
preview: size.toString(),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
// Aktuelle Font-Size aus State Reflection
|
|
19
|
+
const currentValue = props.currentValue || (props.editorAPI ? getCurrentFontSize(props.editorAPI) : undefined);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<Dropdown
|
|
23
|
+
icon="mdi:format-size"
|
|
24
|
+
label="Schriftgröße"
|
|
25
|
+
options={options}
|
|
26
|
+
onSelect={(value) => {
|
|
27
|
+
if (props.onSelect) {
|
|
28
|
+
props.onSelect(value);
|
|
29
|
+
} else {
|
|
30
|
+
props.onClick();
|
|
31
|
+
}
|
|
32
|
+
}}
|
|
33
|
+
currentValue={currentValue}
|
|
34
|
+
disabled={props.disabled}
|
|
35
|
+
/>
|
|
36
|
+
);
|
|
37
|
+
},
|
|
38
|
+
getCurrentValue: (editor: EditorAPI) => {
|
|
39
|
+
return getCurrentFontSize(editor);
|
|
40
|
+
},
|
|
41
|
+
execute: (editor: EditorAPI, value?: string) => {
|
|
42
|
+
if (value) {
|
|
43
|
+
// Setze inline style für präzise Größe
|
|
44
|
+
const selection = editor.getSelection();
|
|
45
|
+
if (selection && selection.rangeCount > 0) {
|
|
46
|
+
const range = selection.getRangeAt(0);
|
|
47
|
+
let element: HTMLElement | null = null;
|
|
48
|
+
|
|
49
|
+
if (range.commonAncestorContainer.nodeType === Node.TEXT_NODE) {
|
|
50
|
+
element = range.commonAncestorContainer.parentElement;
|
|
51
|
+
} else {
|
|
52
|
+
element = range.commonAncestorContainer as HTMLElement;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (element) {
|
|
56
|
+
// Erstelle oder aktualisiere span mit fontSize
|
|
57
|
+
const span = document.createElement('span');
|
|
58
|
+
span.style.fontSize = `${value}px`;
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
range.surroundContents(span);
|
|
62
|
+
} catch (e) {
|
|
63
|
+
// Falls surroundContents fehlschlägt
|
|
64
|
+
const contents = range.extractContents();
|
|
65
|
+
span.appendChild(contents);
|
|
66
|
+
range.insertNode(span);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Cursor setzen
|
|
70
|
+
range.setStartAfter(span);
|
|
71
|
+
range.collapse(true);
|
|
72
|
+
selection.removeAllRanges();
|
|
73
|
+
selection.addRange(range);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
canExecute: () => true,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Plugin, EditorAPI, ButtonProps } from '../types';
|
|
3
|
+
import { Dropdown } from '../components/Dropdown';
|
|
4
|
+
import { getCurrentHeading } from '../utils/stateReflection';
|
|
5
|
+
|
|
6
|
+
const defaultHeadings = ['h1', 'h2', 'h3'];
|
|
7
|
+
|
|
8
|
+
const headingLabels: Record<string, string> = {
|
|
9
|
+
h1: 'Überschrift 1',
|
|
10
|
+
h2: 'Überschrift 2',
|
|
11
|
+
h3: 'Überschrift 3',
|
|
12
|
+
h4: 'Überschrift 4',
|
|
13
|
+
h5: 'Überschrift 5',
|
|
14
|
+
h6: 'Überschrift 6',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function createHeadingsPlugin(headings: string[] = defaultHeadings): Plugin {
|
|
18
|
+
const options = [
|
|
19
|
+
{ value: 'p', label: 'Normal', headingPreview: 'p' },
|
|
20
|
+
...headings.map(h => ({
|
|
21
|
+
value: h,
|
|
22
|
+
label: headingLabels[h] || h.toUpperCase(),
|
|
23
|
+
headingPreview: h,
|
|
24
|
+
})),
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
name: 'headings',
|
|
29
|
+
type: 'block',
|
|
30
|
+
renderButton: (props: ButtonProps & { onSelect?: (value: string) => void; editorAPI?: EditorAPI; currentValue?: string }) => {
|
|
31
|
+
// Aktuelles Heading aus State Reflection
|
|
32
|
+
const currentValue = props.currentValue || (props.editorAPI ? getCurrentHeading(props.editorAPI, headings) : undefined);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<Dropdown
|
|
36
|
+
icon="mdi:format-header-1"
|
|
37
|
+
label="Überschrift"
|
|
38
|
+
options={options}
|
|
39
|
+
onSelect={(value) => {
|
|
40
|
+
if (props.onSelect) {
|
|
41
|
+
props.onSelect(value);
|
|
42
|
+
} else {
|
|
43
|
+
props.onClick();
|
|
44
|
+
}
|
|
45
|
+
}}
|
|
46
|
+
currentValue={currentValue}
|
|
47
|
+
disabled={props.disabled}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
},
|
|
51
|
+
getCurrentValue: (editor: EditorAPI) => {
|
|
52
|
+
return getCurrentHeading(editor, headings);
|
|
53
|
+
},
|
|
54
|
+
execute: (editor: EditorAPI, value?: string) => {
|
|
55
|
+
const tag = value || 'p';
|
|
56
|
+
editor.executeCommand('formatBlock', `<${tag}>`);
|
|
57
|
+
},
|
|
58
|
+
isActive: (editor: EditorAPI) => {
|
|
59
|
+
const selection = editor.getSelection();
|
|
60
|
+
if (!selection || selection.rangeCount === 0) return false;
|
|
61
|
+
|
|
62
|
+
const range = selection.getRangeAt(0);
|
|
63
|
+
const container = range.commonAncestorContainer;
|
|
64
|
+
const element = container.nodeType === Node.TEXT_NODE
|
|
65
|
+
? container.parentElement
|
|
66
|
+
: container as HTMLElement;
|
|
67
|
+
|
|
68
|
+
if (!element) return false;
|
|
69
|
+
|
|
70
|
+
const tagName = element.tagName.toLowerCase();
|
|
71
|
+
return headings.includes(tagName);
|
|
72
|
+
},
|
|
73
|
+
canExecute: () => true,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import React, { useState, useRef } from 'react';
|
|
2
|
+
import { Plugin, EditorAPI, ButtonProps } from '../types';
|
|
3
|
+
import { IconWrapper } from '../components/IconWrapper';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Image-Plugin mit URL-Eingabe und File-Upload
|
|
7
|
+
*/
|
|
8
|
+
export function createImagePlugin(onImageUpload?: (file: File) => Promise<string>): Plugin {
|
|
9
|
+
return {
|
|
10
|
+
name: 'image',
|
|
11
|
+
type: 'block',
|
|
12
|
+
renderButton: (props: ButtonProps & { editorAPI?: EditorAPI }) => {
|
|
13
|
+
const [showModal, setShowModal] = useState(false);
|
|
14
|
+
const [imageUrl, setImageUrl] = useState('');
|
|
15
|
+
const [altText, setAltText] = useState('');
|
|
16
|
+
const [isUploading, setIsUploading] = useState(false);
|
|
17
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
18
|
+
|
|
19
|
+
const handleInsertImage = () => {
|
|
20
|
+
if (!props.editorAPI) return;
|
|
21
|
+
|
|
22
|
+
const src = imageUrl.trim();
|
|
23
|
+
|
|
24
|
+
if (!src) {
|
|
25
|
+
alert('Bitte geben Sie eine Bild-URL ein');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Verwende executeCommand, das alles korrekt handhabt
|
|
30
|
+
// Alt-Text wird später über das Datenmodell gespeichert
|
|
31
|
+
props.editorAPI.executeCommand('insertImage', src);
|
|
32
|
+
|
|
33
|
+
// Modal schließen
|
|
34
|
+
setShowModal(false);
|
|
35
|
+
setImageUrl('');
|
|
36
|
+
setAltText('');
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
40
|
+
const file = e.target.files?.[0];
|
|
41
|
+
if (!file || !onImageUpload) return;
|
|
42
|
+
|
|
43
|
+
if (!file.type.startsWith('image/')) {
|
|
44
|
+
alert('Bitte wählen Sie eine Bilddatei aus');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
setIsUploading(true);
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const uploadedUrl = await onImageUpload(file);
|
|
52
|
+
setImageUrl(uploadedUrl);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error('Image upload failed:', error);
|
|
55
|
+
alert('Fehler beim Hochladen des Bildes');
|
|
56
|
+
} finally {
|
|
57
|
+
setIsUploading(false);
|
|
58
|
+
if (fileInputRef.current) {
|
|
59
|
+
fileInputRef.current.value = '';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<>
|
|
66
|
+
<button
|
|
67
|
+
type="button"
|
|
68
|
+
onClick={() => setShowModal(true)}
|
|
69
|
+
disabled={props.disabled}
|
|
70
|
+
className="rte-toolbar-button"
|
|
71
|
+
title="Bild einfügen"
|
|
72
|
+
aria-label="Bild einfügen"
|
|
73
|
+
>
|
|
74
|
+
<IconWrapper icon="mdi:image" width={18} height={18} />
|
|
75
|
+
</button>
|
|
76
|
+
|
|
77
|
+
{showModal && (
|
|
78
|
+
<div
|
|
79
|
+
className="rte-image-modal-overlay"
|
|
80
|
+
onClick={(e) => {
|
|
81
|
+
if (e.target === e.currentTarget) {
|
|
82
|
+
setShowModal(false);
|
|
83
|
+
}
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
<div className="rte-image-modal">
|
|
87
|
+
<div className="rte-image-modal-header">
|
|
88
|
+
<h3>Bild einfügen</h3>
|
|
89
|
+
<button
|
|
90
|
+
type="button"
|
|
91
|
+
onClick={() => setShowModal(false)}
|
|
92
|
+
className="rte-image-modal-close"
|
|
93
|
+
aria-label="Schließen"
|
|
94
|
+
>
|
|
95
|
+
<IconWrapper icon="mdi:close" width={20} height={20} />
|
|
96
|
+
</button>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<div className="rte-image-modal-content">
|
|
100
|
+
{onImageUpload && (
|
|
101
|
+
<div className="rte-image-upload-section">
|
|
102
|
+
<label className="rte-image-upload-label">
|
|
103
|
+
<input
|
|
104
|
+
ref={fileInputRef}
|
|
105
|
+
type="file"
|
|
106
|
+
accept="image/*"
|
|
107
|
+
onChange={handleFileSelect}
|
|
108
|
+
style={{ display: 'none' }}
|
|
109
|
+
/>
|
|
110
|
+
<div className="rte-image-upload-button">
|
|
111
|
+
{isUploading ? (
|
|
112
|
+
<>
|
|
113
|
+
<IconWrapper icon="mdi:loading" width={24} height={24} className="rte-spin" />
|
|
114
|
+
<span>Wird hochgeladen...</span>
|
|
115
|
+
</>
|
|
116
|
+
) : (
|
|
117
|
+
<>
|
|
118
|
+
<IconWrapper icon="mdi:upload" width={24} height={24} />
|
|
119
|
+
<span>Datei auswählen</span>
|
|
120
|
+
</>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
</label>
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
|
|
127
|
+
<div className="rte-image-url-section">
|
|
128
|
+
<label>
|
|
129
|
+
Bild-URL
|
|
130
|
+
<input
|
|
131
|
+
type="url"
|
|
132
|
+
value={imageUrl}
|
|
133
|
+
onChange={(e) => setImageUrl(e.target.value)}
|
|
134
|
+
placeholder="https://example.com/image.jpg"
|
|
135
|
+
className="rte-image-url-input"
|
|
136
|
+
/>
|
|
137
|
+
</label>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<div className="rte-image-alt-section">
|
|
141
|
+
<label>
|
|
142
|
+
Alt-Text (optional)
|
|
143
|
+
<input
|
|
144
|
+
type="text"
|
|
145
|
+
value={altText}
|
|
146
|
+
onChange={(e) => setAltText(e.target.value)}
|
|
147
|
+
placeholder="Bildbeschreibung"
|
|
148
|
+
className="rte-image-alt-input"
|
|
149
|
+
/>
|
|
150
|
+
</label>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
{imageUrl && (
|
|
154
|
+
<div className="rte-image-preview">
|
|
155
|
+
<img src={imageUrl} alt={altText || 'Preview'} />
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<div className="rte-image-modal-footer">
|
|
161
|
+
<button
|
|
162
|
+
type="button"
|
|
163
|
+
onClick={() => setShowModal(false)}
|
|
164
|
+
className="rte-image-modal-cancel"
|
|
165
|
+
>
|
|
166
|
+
Abbrechen
|
|
167
|
+
</button>
|
|
168
|
+
<button
|
|
169
|
+
type="button"
|
|
170
|
+
onClick={handleInsertImage}
|
|
171
|
+
disabled={!imageUrl.trim() || isUploading}
|
|
172
|
+
className="rte-image-modal-insert"
|
|
173
|
+
>
|
|
174
|
+
Einfügen
|
|
175
|
+
</button>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
180
|
+
</>
|
|
181
|
+
);
|
|
182
|
+
},
|
|
183
|
+
execute: (editor: EditorAPI) => {
|
|
184
|
+
// Wird über renderButton gehandhabt
|
|
185
|
+
},
|
|
186
|
+
canExecute: () => true,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Plugin } from '../types';
|
|
2
|
+
import { createInlinePlugin, createCommandPlugin } from './base';
|
|
3
|
+
import { clearFormattingPlugin } from './clearFormatting';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Standard-Plugins
|
|
7
|
+
*/
|
|
8
|
+
export const boldPlugin: Plugin = createInlinePlugin(
|
|
9
|
+
'bold',
|
|
10
|
+
'bold',
|
|
11
|
+
'mdi:format-bold',
|
|
12
|
+
'Fett'
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
export const italicPlugin: Plugin = createInlinePlugin(
|
|
16
|
+
'italic',
|
|
17
|
+
'italic',
|
|
18
|
+
'mdi:format-italic',
|
|
19
|
+
'Kursiv'
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
export const underlinePlugin: Plugin = createInlinePlugin(
|
|
23
|
+
'underline',
|
|
24
|
+
'underline',
|
|
25
|
+
'mdi:format-underline',
|
|
26
|
+
'Unterstrichen'
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export const undoPlugin: Plugin = createCommandPlugin(
|
|
30
|
+
'undo',
|
|
31
|
+
'undo',
|
|
32
|
+
'mdi:undo',
|
|
33
|
+
'Rückgängig'
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
export const redoPlugin: Plugin = createCommandPlugin(
|
|
37
|
+
'redo',
|
|
38
|
+
'redo',
|
|
39
|
+
'mdi:redo',
|
|
40
|
+
'Wiederholen'
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Standard-Plugin-Liste
|
|
45
|
+
*/
|
|
46
|
+
export const defaultPlugins: Plugin[] = [
|
|
47
|
+
undoPlugin,
|
|
48
|
+
redoPlugin,
|
|
49
|
+
boldPlugin,
|
|
50
|
+
italicPlugin,
|
|
51
|
+
underlinePlugin,
|
|
52
|
+
clearFormattingPlugin,
|
|
53
|
+
];
|
|
54
|
+
|