@overlap/rte 0.1.2 → 0.1.4

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.
@@ -1,87 +0,0 @@
1
- import { Dropdown } from "../components/Dropdown";
2
- import { ButtonProps, EditorAPI, Plugin } from "../types";
3
- import { getCurrentHeading } from "../utils/stateReflection";
4
-
5
- const defaultHeadings = ["h1", "h2", "h3"];
6
-
7
- const headingLabels: Record<string, string> = {
8
- h1: "Überschrift 1",
9
- h2: "Überschrift 2",
10
- h3: "Überschrift 3",
11
- h4: "Überschrift 4",
12
- h5: "Überschrift 5",
13
- h6: "Überschrift 6",
14
- };
15
-
16
- export function createHeadingsPlugin(
17
- headings: string[] = defaultHeadings
18
- ): Plugin {
19
- const options = [
20
- { value: "p", label: "Normal", headingPreview: "p" },
21
- ...headings.map((h) => ({
22
- value: h,
23
- label: headingLabels[h] || h.toUpperCase(),
24
- headingPreview: h,
25
- })),
26
- ];
27
-
28
- return {
29
- name: "headings",
30
- type: "block",
31
- renderButton: (
32
- props: ButtonProps & {
33
- onSelect?: (value: string) => void;
34
- editorAPI?: EditorAPI;
35
- currentValue?: string;
36
- }
37
- ) => {
38
- // Aktuelles Heading aus State Reflection
39
- const currentValue =
40
- props.currentValue ||
41
- (props.editorAPI
42
- ? getCurrentHeading(props.editorAPI, headings)
43
- : undefined);
44
-
45
- return (
46
- <Dropdown
47
- icon="mdi:format-header-1"
48
- label="Überschrift"
49
- options={options}
50
- onSelect={(value) => {
51
- if (props.onSelect) {
52
- props.onSelect(value);
53
- } else {
54
- props.onClick();
55
- }
56
- }}
57
- currentValue={currentValue}
58
- disabled={props.disabled}
59
- />
60
- );
61
- },
62
- getCurrentValue: (editor: EditorAPI) => {
63
- return getCurrentHeading(editor, headings);
64
- },
65
- execute: (editor: EditorAPI, value?: string) => {
66
- const tag = value || "p";
67
- editor.executeCommand("formatBlock", `<${tag}>`);
68
- },
69
- isActive: (editor: EditorAPI) => {
70
- const selection = editor.getSelection();
71
- if (!selection || selection.rangeCount === 0) return false;
72
-
73
- const range = selection.getRangeAt(0);
74
- const container = range.commonAncestorContainer;
75
- const element =
76
- container.nodeType === Node.TEXT_NODE
77
- ? container.parentElement
78
- : (container as HTMLElement);
79
-
80
- if (!element) return false;
81
-
82
- const tagName = element.tagName.toLowerCase();
83
- return headings.includes(tagName);
84
- },
85
- canExecute: () => true,
86
- };
87
- }
@@ -1,189 +0,0 @@
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
-
@@ -1,161 +0,0 @@
1
- import { IconWrapper } from "../components/IconWrapper";
2
- import { ButtonProps, EditorAPI, Plugin } from "../types";
3
- import { createCommandPlugin, createInlinePlugin } from "./base";
4
- import { createBlockFormatPlugin } from "./blockFormat";
5
- import { clearFormattingPlugin } from "./clearFormatting";
6
-
7
- const defaultHeadings = ["h1", "h2", "h3"];
8
-
9
- /**
10
- * Standard-Plugins
11
- */
12
- export const boldPlugin: Plugin = createInlinePlugin(
13
- "bold",
14
- "bold",
15
- "mdi:format-bold",
16
- "Fett"
17
- );
18
-
19
- export const italicPlugin: Plugin = createInlinePlugin(
20
- "italic",
21
- "italic",
22
- "mdi:format-italic",
23
- "Kursiv"
24
- );
25
-
26
- export const underlinePlugin: Plugin = createInlinePlugin(
27
- "underline",
28
- "underline",
29
- "mdi:format-underline",
30
- "Unterstrichen"
31
- );
32
-
33
- export const undoPlugin: Plugin = createCommandPlugin(
34
- "undo",
35
- "undo",
36
- "mdi:undo",
37
- "Rückgängig"
38
- );
39
-
40
- export const redoPlugin: Plugin = createCommandPlugin(
41
- "redo",
42
- "redo",
43
- "mdi:redo",
44
- "Wiederholen"
45
- );
46
-
47
- /**
48
- * Indent List Item Plugin (Tab für Unterliste)
49
- */
50
- const indentListItemPlugin: Plugin = {
51
- name: "indentListItem",
52
- type: "command",
53
- renderButton: (props: ButtonProps) => (
54
- <button
55
- type="button"
56
- onClick={props.onClick}
57
- disabled={props.disabled}
58
- className="rte-toolbar-button"
59
- title="Einrücken (Unterliste)"
60
- aria-label="Einrücken (Unterliste)"
61
- >
62
- <IconWrapper
63
- icon="mdi:format-indent-increase"
64
- width={18}
65
- height={18}
66
- />
67
- </button>
68
- ),
69
- execute: (editor: EditorAPI) => {
70
- editor.indentListItem();
71
- },
72
- canExecute: (editor: EditorAPI) => {
73
- const selection = editor.getSelection();
74
- if (!selection || selection.rangeCount === 0) return false;
75
-
76
- const range = selection.getRangeAt(0);
77
- const container = range.commonAncestorContainer;
78
- const listItem =
79
- container.nodeType === Node.TEXT_NODE
80
- ? container.parentElement?.closest("li")
81
- : (container as HTMLElement).closest("li");
82
-
83
- return listItem !== null;
84
- },
85
- };
86
-
87
- /**
88
- * Outdent List Item Plugin (Shift+Tab)
89
- */
90
- const outdentListItemPlugin: Plugin = {
91
- name: "outdentListItem",
92
- type: "command",
93
- renderButton: (props: ButtonProps) => (
94
- <button
95
- type="button"
96
- onClick={props.onClick}
97
- disabled={props.disabled}
98
- className="rte-toolbar-button"
99
- title="Ausrücken"
100
- aria-label="Ausrücken"
101
- >
102
- <IconWrapper
103
- icon="mdi:format-indent-decrease"
104
- width={18}
105
- height={18}
106
- />
107
- </button>
108
- ),
109
- execute: (editor: EditorAPI) => {
110
- editor.outdentListItem();
111
- },
112
- canExecute: (editor: EditorAPI) => {
113
- const selection = editor.getSelection();
114
- if (!selection || selection.rangeCount === 0) return false;
115
-
116
- const range = selection.getRangeAt(0);
117
- const container = range.commonAncestorContainer;
118
- const listItem =
119
- container.nodeType === Node.TEXT_NODE
120
- ? container.parentElement?.closest("li")
121
- : (container as HTMLElement).closest("li");
122
-
123
- if (!listItem) return false;
124
-
125
- // Prüfe ob in verschachtelter Liste
126
- const list = listItem.parentElement;
127
- if (!list || (list.tagName !== "UL" && list.tagName !== "OL"))
128
- return false;
129
-
130
- const parentListItem = list.parentElement;
131
- return parentListItem !== null && parentListItem.tagName === "LI";
132
- },
133
- };
134
-
135
- // Export plugins for direct use
136
- export { indentListItemPlugin, outdentListItemPlugin };
137
-
138
- /**
139
- * Standard-Plugin-Liste
140
- * Die Plugins werden hier direkt referenziert, um sicherzustellen, dass sie in defaultPlugins enthalten sind
141
- */
142
- const _indentPlugin = indentListItemPlugin;
143
- const _outdentPlugin = outdentListItemPlugin;
144
-
145
- /**
146
- * Standard Block-Format Plugin (Headlines, Listen, Quote in einem Dropdown)
147
- * Verwendet standardmäßig h1, h2, h3, kann aber über Editor-Props angepasst werden
148
- */
149
- const defaultBlockFormatPlugin = createBlockFormatPlugin(defaultHeadings);
150
-
151
- export const defaultPlugins: Plugin[] = [
152
- undoPlugin,
153
- redoPlugin,
154
- boldPlugin,
155
- italicPlugin,
156
- underlinePlugin,
157
- defaultBlockFormatPlugin,
158
- clearFormattingPlugin,
159
- _indentPlugin,
160
- _outdentPlugin,
161
- ];
@@ -1,90 +0,0 @@
1
- import { IconWrapper } from "../components/IconWrapper";
2
- import { ButtonProps, EditorAPI, Plugin } from "../types";
3
-
4
- /**
5
- * Indent List Item Plugin (Tab für Unterliste)
6
- */
7
- export const indentListItemPlugin: Plugin = {
8
- name: "indentListItem",
9
- type: "command",
10
- renderButton: (props: ButtonProps) => (
11
- <button
12
- type="button"
13
- onClick={props.onClick}
14
- disabled={props.disabled}
15
- className="rte-toolbar-button"
16
- title="Einrücken (Unterliste)"
17
- aria-label="Einrücken (Unterliste)"
18
- >
19
- <IconWrapper
20
- icon="mdi:format-indent-increase"
21
- width={18}
22
- height={18}
23
- />
24
- </button>
25
- ),
26
- execute: (editor: EditorAPI) => {
27
- editor.indentListItem();
28
- },
29
- canExecute: (editor: EditorAPI) => {
30
- const selection = editor.getSelection();
31
- if (!selection || selection.rangeCount === 0) return false;
32
-
33
- const range = selection.getRangeAt(0);
34
- const container = range.commonAncestorContainer;
35
- const listItem =
36
- container.nodeType === Node.TEXT_NODE
37
- ? container.parentElement?.closest("li")
38
- : (container as HTMLElement).closest("li");
39
-
40
- return listItem !== null;
41
- },
42
- };
43
-
44
- /**
45
- * Outdent List Item Plugin (Shift+Tab)
46
- */
47
- export const outdentListItemPlugin: Plugin = {
48
- name: "outdentListItem",
49
- type: "command",
50
- renderButton: (props: ButtonProps) => (
51
- <button
52
- type="button"
53
- onClick={props.onClick}
54
- disabled={props.disabled}
55
- className="rte-toolbar-button"
56
- title="Ausrücken"
57
- aria-label="Ausrücken"
58
- >
59
- <IconWrapper
60
- icon="mdi:format-indent-decrease"
61
- width={18}
62
- height={18}
63
- />
64
- </button>
65
- ),
66
- execute: (editor: EditorAPI) => {
67
- editor.outdentListItem();
68
- },
69
- canExecute: (editor: EditorAPI) => {
70
- const selection = editor.getSelection();
71
- if (!selection || selection.rangeCount === 0) return false;
72
-
73
- const range = selection.getRangeAt(0);
74
- const container = range.commonAncestorContainer;
75
- const listItem =
76
- container.nodeType === Node.TEXT_NODE
77
- ? container.parentElement?.closest("li")
78
- : (container as HTMLElement).closest("li");
79
-
80
- if (!listItem) return false;
81
-
82
- // Prüfe ob in verschachtelter Liste
83
- const list = listItem.parentElement;
84
- if (!list || (list.tagName !== "UL" && list.tagName !== "OL"))
85
- return false;
86
-
87
- const parentListItem = list.parentElement;
88
- return parentListItem !== null && parentListItem.tagName === "LI";
89
- },
90
- };