@overlap/rte 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +269 -0
  2. package/dist/components/Dropdown.d.ts +19 -0
  3. package/dist/components/Dropdown.d.ts.map +1 -0
  4. package/dist/components/Editor.d.ts +4 -0
  5. package/dist/components/Editor.d.ts.map +1 -0
  6. package/dist/components/FloatingToolbar.d.ts +10 -0
  7. package/dist/components/FloatingToolbar.d.ts.map +1 -0
  8. package/dist/components/IconWrapper.d.ts +10 -0
  9. package/dist/components/IconWrapper.d.ts.map +1 -0
  10. package/dist/components/Icons.d.ts +32 -0
  11. package/dist/components/Icons.d.ts.map +1 -0
  12. package/dist/components/Toolbar.d.ts +10 -0
  13. package/dist/components/Toolbar.d.ts.map +1 -0
  14. package/dist/components/index.d.ts +3 -0
  15. package/dist/components/index.d.ts.map +1 -0
  16. package/dist/index.d.ts +208 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.esm.js +2080 -0
  19. package/dist/index.esm.js.map +1 -0
  20. package/dist/index.js +2116 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/plugins/base.d.ts +10 -0
  23. package/dist/plugins/base.d.ts.map +1 -0
  24. package/dist/plugins/clearFormatting.d.ts +6 -0
  25. package/dist/plugins/clearFormatting.d.ts.map +1 -0
  26. package/dist/plugins/colors.d.ts +4 -0
  27. package/dist/plugins/colors.d.ts.map +1 -0
  28. package/dist/plugins/fontSize.d.ts +3 -0
  29. package/dist/plugins/fontSize.d.ts.map +1 -0
  30. package/dist/plugins/headings.d.ts +3 -0
  31. package/dist/plugins/headings.d.ts.map +1 -0
  32. package/dist/plugins/image.d.ts +6 -0
  33. package/dist/plugins/image.d.ts.map +1 -0
  34. package/dist/plugins/index.d.ts +14 -0
  35. package/dist/plugins/index.d.ts.map +1 -0
  36. package/dist/plugins/optional.d.ts +19 -0
  37. package/dist/plugins/optional.d.ts.map +1 -0
  38. package/dist/styles.css +638 -0
  39. package/dist/types.d.ts +81 -0
  40. package/dist/types.d.ts.map +1 -0
  41. package/dist/utils/clearFormatting.d.ts +21 -0
  42. package/dist/utils/clearFormatting.d.ts.map +1 -0
  43. package/dist/utils/content.d.ts +12 -0
  44. package/dist/utils/content.d.ts.map +1 -0
  45. package/dist/utils/history.d.ts +14 -0
  46. package/dist/utils/history.d.ts.map +1 -0
  47. package/dist/utils/listIndent.d.ts +9 -0
  48. package/dist/utils/listIndent.d.ts.map +1 -0
  49. package/dist/utils/stateReflection.d.ts +18 -0
  50. package/dist/utils/stateReflection.d.ts.map +1 -0
  51. package/package.json +48 -0
  52. package/src/components/Dropdown.tsx +103 -0
  53. package/src/components/Editor.css +2 -0
  54. package/src/components/Editor.tsx +785 -0
  55. package/src/components/FloatingToolbar.tsx +214 -0
  56. package/src/components/IconWrapper.tsx +14 -0
  57. package/src/components/Icons.tsx +145 -0
  58. package/src/components/Toolbar.tsx +137 -0
  59. package/src/components/index.ts +3 -0
  60. package/src/index.ts +19 -0
  61. package/src/plugins/base.tsx +91 -0
  62. package/src/plugins/clearFormatting.tsx +31 -0
  63. package/src/plugins/colors.tsx +122 -0
  64. package/src/plugins/fontSize.tsx +81 -0
  65. package/src/plugins/headings.tsx +76 -0
  66. package/src/plugins/image.tsx +189 -0
  67. package/src/plugins/index.ts +54 -0
  68. package/src/plugins/optional.tsx +221 -0
  69. package/src/styles.css +638 -0
  70. package/src/types.ts +92 -0
  71. package/src/utils/clearFormatting.ts +244 -0
  72. package/src/utils/content.ts +290 -0
  73. package/src/utils/history.ts +59 -0
  74. package/src/utils/listIndent.ts +171 -0
  75. package/src/utils/stateReflection.ts +175 -0
@@ -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
+