@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,137 +0,0 @@
1
- import React, { useEffect, useState } from "react";
2
- import { ButtonProps, EditorAPI, Plugin } from "../types";
3
-
4
- interface ToolbarProps {
5
- plugins: Plugin[];
6
- editorAPI: EditorAPI;
7
- className?: string;
8
- }
9
-
10
- export const Toolbar: React.FC<ToolbarProps> = ({
11
- plugins,
12
- editorAPI,
13
- className,
14
- }) => {
15
- const [updateTrigger, setUpdateTrigger] = useState(0);
16
- const [isClient, setIsClient] = useState(false);
17
-
18
- useEffect(() => {
19
- setIsClient(true);
20
-
21
- const handleSelectionChange = () => {
22
- setUpdateTrigger((prev) => prev + 1);
23
- };
24
-
25
- const handleMouseUp = () => {
26
- setTimeout(handleSelectionChange, 10);
27
- };
28
-
29
- const handleKeyUp = () => {
30
- setTimeout(handleSelectionChange, 10);
31
- };
32
-
33
- if (typeof document !== 'undefined') {
34
- document.addEventListener("selectionchange", handleSelectionChange);
35
- document.addEventListener("mouseup", handleMouseUp);
36
- document.addEventListener("keyup", handleKeyUp);
37
- }
38
-
39
- return () => {
40
- if (typeof document !== 'undefined') {
41
- document.removeEventListener(
42
- "selectionchange",
43
- handleSelectionChange
44
- );
45
- document.removeEventListener("mouseup", handleMouseUp);
46
- document.removeEventListener("keyup", handleKeyUp);
47
- }
48
- };
49
- }, []);
50
-
51
- const handlePluginClick = (plugin: Plugin, value?: string) => {
52
- if (plugin.canExecute?.(editorAPI) !== false) {
53
- if (plugin.execute) {
54
- plugin.execute(editorAPI, value);
55
- } else if (plugin.command && value !== undefined) {
56
- editorAPI.executeCommand(plugin.command, value);
57
- } else if (plugin.command) {
58
- editorAPI.executeCommand(plugin.command);
59
- }
60
- setTimeout(() => setUpdateTrigger((prev) => prev + 1), 50);
61
- }
62
- };
63
-
64
- const leftPlugins = plugins.filter((p) => p.name !== "clearFormatting");
65
- const clearFormattingPlugin = plugins.find(
66
- (p) => p.name === "clearFormatting"
67
- );
68
-
69
- return (
70
- <div className={`rte-toolbar rte-toolbar-sticky ${className || ""}`}>
71
- <div className="rte-toolbar-left">
72
- {leftPlugins.map((plugin) => {
73
- if (!plugin.renderButton) return null;
74
-
75
- const isActive = isClient && plugin.isActive
76
- ? plugin.isActive(editorAPI)
77
- : false;
78
- const canExecute = isClient && plugin.canExecute
79
- ? plugin.canExecute(editorAPI)
80
- : true;
81
-
82
- const currentValue = isClient && plugin.getCurrentValue
83
- ? plugin.getCurrentValue(editorAPI)
84
- : undefined;
85
-
86
- const buttonProps: ButtonProps & { [key: string]: any } = {
87
- isActive,
88
- onClick: () => handlePluginClick(plugin),
89
- disabled: !canExecute,
90
- onSelect: (value: string) =>
91
- handlePluginClick(plugin, value),
92
- editorAPI,
93
- currentValue,
94
- };
95
-
96
- return (
97
- <React.Fragment key={plugin.name}>
98
- {plugin.renderButton(buttonProps)}
99
- </React.Fragment>
100
- );
101
- })}
102
- </div>
103
-
104
- {clearFormattingPlugin && clearFormattingPlugin.renderButton && (
105
- <div className="rte-toolbar-right">
106
- <div className="rte-toolbar-divider" />
107
- {(() => {
108
- const isActive = isClient && clearFormattingPlugin.isActive
109
- ? clearFormattingPlugin.isActive(editorAPI)
110
- : false;
111
- const canExecute = isClient && clearFormattingPlugin.canExecute
112
- ? clearFormattingPlugin.canExecute(editorAPI)
113
- : true;
114
-
115
- const buttonProps: ButtonProps & {
116
- [key: string]: any;
117
- } = {
118
- isActive,
119
- onClick: () =>
120
- handlePluginClick(clearFormattingPlugin),
121
- disabled: !canExecute,
122
- editorAPI,
123
- };
124
-
125
- return (
126
- <React.Fragment key={clearFormattingPlugin.name}>
127
- {clearFormattingPlugin.renderButton(
128
- buttonProps
129
- )}
130
- </React.Fragment>
131
- );
132
- })()}
133
- </div>
134
- )}
135
- </div>
136
- );
137
- };
@@ -1,3 +0,0 @@
1
- export { Editor } from './Editor';
2
- export { Toolbar } from './Toolbar';
3
-
package/src/index.ts DELETED
@@ -1,19 +0,0 @@
1
- export { Dropdown } from "./components/Dropdown";
2
- export { Editor } from "./components/Editor";
3
- export { Toolbar } from "./components/Toolbar";
4
- export * from "./plugins";
5
- export * from "./plugins/blockFormat";
6
- export * from "./plugins/clearFormatting";
7
- export * from "./plugins/colors";
8
- export * from "./plugins/fontSize";
9
- export * from "./plugins/headings";
10
- export * from "./plugins/image";
11
- export * from "./plugins/optional";
12
- export * from "./types";
13
- export * from "./utils/content";
14
- export { contentToHTML, htmlToContent } from "./utils/content";
15
- export { HistoryManager } from "./utils/history";
16
- export { indentListItem, outdentListItem } from "./utils/listIndent";
17
- export * from "./utils/stateReflection";
18
-
19
- export { Editor as default } from "./components/Editor";
@@ -1,91 +0,0 @@
1
- import React from 'react';
2
- import { Plugin, EditorAPI, ButtonProps } from '../types';
3
- import { IconWrapper } from '../components/IconWrapper';
4
-
5
- /**
6
- * Basis-Plugin für Inline-Formatierungen
7
- */
8
- export function createInlinePlugin(
9
- name: string,
10
- command: string,
11
- icon: string,
12
- label: string
13
- ): Plugin {
14
- return {
15
- name,
16
- type: 'inline',
17
- command,
18
- renderButton: (props: ButtonProps) => (
19
- <button
20
- type="button"
21
- onClick={props.onClick}
22
- disabled={props.disabled}
23
- className={`rte-toolbar-button ${props.isActive ? 'rte-toolbar-button-active' : ''}`}
24
- title={label}
25
- aria-label={label}
26
- >
27
- <IconWrapper icon={icon} width={18} height={18} />
28
- </button>
29
- ),
30
- execute: (editor: EditorAPI) => {
31
- editor.executeCommand(command);
32
- },
33
- isActive: (editor: EditorAPI) => {
34
- if (typeof window === 'undefined' || typeof document === 'undefined') return false;
35
- const selection = editor.getSelection();
36
- if (!selection || selection.rangeCount === 0) return false;
37
-
38
- const range = selection.getRangeAt(0);
39
- const container = range.commonAncestorContainer;
40
- const element = container.nodeType === Node.TEXT_NODE
41
- ? container.parentElement
42
- : container as HTMLElement;
43
-
44
- if (!element) return false;
45
-
46
- return document.queryCommandState(command);
47
- },
48
- canExecute: (editor: EditorAPI) => {
49
- // Formatierung sollte auch ohne Selection möglich sein
50
- // (z.B. wenn Editor leer ist, wird beim Klick eine Selection erstellt)
51
- return true;
52
- },
53
- };
54
- }
55
-
56
- /**
57
- * Basis-Plugin für Commands
58
- */
59
- export function createCommandPlugin(
60
- name: string,
61
- command: string,
62
- icon: string,
63
- label: string
64
- ): Plugin {
65
- return {
66
- name,
67
- type: 'command',
68
- command,
69
- renderButton: (props: ButtonProps) => (
70
- <button
71
- type="button"
72
- onClick={props.onClick}
73
- disabled={props.disabled}
74
- className="rte-toolbar-button"
75
- title={label}
76
- aria-label={label}
77
- >
78
- <IconWrapper icon={icon} width={18} height={18} />
79
- </button>
80
- ),
81
- execute: (editor: EditorAPI) => {
82
- editor.executeCommand(command);
83
- },
84
- canExecute: (editor: EditorAPI) => {
85
- if (command === 'undo') return editor.canUndo();
86
- if (command === 'redo') return editor.canRedo();
87
- return true;
88
- },
89
- };
90
- }
91
-
@@ -1,194 +0,0 @@
1
- import { Dropdown } from "../components/Dropdown";
2
- import { ButtonProps, EditorAPI, Plugin } from "../types";
3
-
4
- const defaultHeadings = ["h1", "h2", "h3"];
5
-
6
- const headingLabels: Record<string, string> = {
7
- h1: "Überschrift 1",
8
- h2: "Überschrift 2",
9
- h3: "Überschrift 3",
10
- h4: "Überschrift 4",
11
- h5: "Überschrift 5",
12
- h6: "Überschrift 6",
13
- };
14
-
15
- /**
16
- * Erstellt ein Block-Format-Plugin, das Headlines, Listen und Quote in einem Dropdown kombiniert
17
- * @param headings - Array von Heading-Levels (z.B. ["h1", "h2", "h3"])
18
- */
19
- export function createBlockFormatPlugin(
20
- headings: string[] = defaultHeadings
21
- ): Plugin {
22
- const options = [
23
- { value: "p", label: "Normal", headingPreview: "p" },
24
- ...headings.map((h) => ({
25
- value: h,
26
- label: headingLabels[h] || h.toUpperCase(),
27
- headingPreview: h,
28
- })),
29
- {
30
- value: "ul",
31
- label: "Aufzählungsliste",
32
- icon: "mdi:format-list-bulleted",
33
- },
34
- {
35
- value: "ol",
36
- label: "Nummerierte Liste",
37
- icon: "mdi:format-list-numbered",
38
- },
39
- { value: "blockquote", label: "Zitat", icon: "mdi:format-quote-close" },
40
- ];
41
-
42
- return {
43
- name: "blockFormat",
44
- type: "block",
45
- renderButton: (
46
- props: ButtonProps & {
47
- onSelect?: (value: string) => void;
48
- editorAPI?: EditorAPI;
49
- currentValue?: string;
50
- }
51
- ) => {
52
- // Aktuelles Format bestimmen
53
- const editor = props.editorAPI;
54
- let currentValue = props.currentValue;
55
-
56
- if (!currentValue && editor) {
57
- const selection = editor.getSelection();
58
- if (selection && selection.rangeCount > 0) {
59
- const range = selection.getRangeAt(0);
60
- const container = range.commonAncestorContainer;
61
- const element =
62
- container.nodeType === Node.TEXT_NODE
63
- ? container.parentElement
64
- : (container as HTMLElement);
65
-
66
- if (element) {
67
- const tagName = element.tagName.toLowerCase();
68
-
69
- // Prüfe auf Heading
70
- if (headings.includes(tagName)) {
71
- currentValue = tagName;
72
- }
73
- // Prüfe auf Blockquote
74
- else if (element.closest("blockquote")) {
75
- currentValue = "blockquote";
76
- }
77
- // Prüfe auf Liste
78
- else if (element.closest("ul")) {
79
- currentValue = "ul";
80
- } else if (element.closest("ol")) {
81
- currentValue = "ol";
82
- }
83
- // Prüfe auf Paragraph
84
- else if (tagName === "p") {
85
- currentValue = "p";
86
- }
87
- }
88
- }
89
- }
90
-
91
- return (
92
- <Dropdown
93
- icon="mdi:format-header-1"
94
- label="Format"
95
- options={options}
96
- onSelect={(value) => {
97
- // onSelect wird von der Toolbar übergeben und ruft handlePluginClick auf
98
- if (props.onSelect) {
99
- props.onSelect(value);
100
- }
101
- }}
102
- currentValue={currentValue}
103
- disabled={props.disabled}
104
- />
105
- );
106
- },
107
- getCurrentValue: (editor: EditorAPI) => {
108
- const selection = editor.getSelection();
109
- if (!selection || selection.rangeCount === 0) return undefined;
110
-
111
- const range = selection.getRangeAt(0);
112
- const container = range.commonAncestorContainer;
113
- const element =
114
- container.nodeType === Node.TEXT_NODE
115
- ? container.parentElement
116
- : (container as HTMLElement);
117
-
118
- if (!element) return undefined;
119
-
120
- const tagName = element.tagName.toLowerCase();
121
-
122
- // Prüfe auf Heading
123
- if (headings.includes(tagName)) {
124
- return tagName;
125
- }
126
- // Prüfe auf Blockquote
127
- if (element.closest("blockquote")) {
128
- return "blockquote";
129
- }
130
- // Prüfe auf Liste
131
- if (element.closest("ul")) {
132
- return "ul";
133
- }
134
- if (element.closest("ol")) {
135
- return "ol";
136
- }
137
- // Prüfe auf Paragraph
138
- if (tagName === "p") {
139
- return "p";
140
- }
141
-
142
- return undefined;
143
- },
144
- execute: (editor: EditorAPI, value?: string) => {
145
- if (!value) return;
146
-
147
- if (value === "ul") {
148
- editor.executeCommand("insertUnorderedList");
149
- } else if (value === "ol") {
150
- editor.executeCommand("insertOrderedList");
151
- } else if (value === "blockquote") {
152
- const selection = editor.getSelection();
153
- if (selection && selection.rangeCount > 0) {
154
- const range = selection.getRangeAt(0);
155
- const container = range.commonAncestorContainer;
156
- const element =
157
- container.nodeType === Node.TEXT_NODE
158
- ? container.parentElement
159
- : (container as HTMLElement);
160
-
161
- if (element?.closest("blockquote")) {
162
- editor.executeCommand("formatBlock", "<p>");
163
- } else {
164
- editor.executeCommand("formatBlock", "<blockquote>");
165
- }
166
- }
167
- } else {
168
- editor.executeCommand("formatBlock", `<${value}>`);
169
- }
170
- },
171
- isActive: (editor: EditorAPI) => {
172
- const selection = editor.getSelection();
173
- if (!selection || selection.rangeCount === 0) return false;
174
-
175
- const range = selection.getRangeAt(0);
176
- const container = range.commonAncestorContainer;
177
- const element =
178
- container.nodeType === Node.TEXT_NODE
179
- ? container.parentElement
180
- : (container as HTMLElement);
181
-
182
- if (!element) return false;
183
-
184
- const tagName = element.tagName.toLowerCase();
185
- return (
186
- headings.includes(tagName) ||
187
- element.closest("blockquote") !== null ||
188
- element.closest("ul") !== null ||
189
- element.closest("ol") !== null
190
- );
191
- },
192
- canExecute: () => true,
193
- };
194
- }
@@ -1,31 +0,0 @@
1
- import React from 'react';
2
- import { Plugin, EditorAPI, ButtonProps } from '../types';
3
- import { IconWrapper } from '../components/IconWrapper';
4
-
5
- /**
6
- * Clear Formatting Plugin - Entfernt alle Formatierungen
7
- */
8
- export const clearFormattingPlugin: Plugin = {
9
- name: 'clearFormatting',
10
- type: 'command',
11
- renderButton: (props: ButtonProps) => (
12
- <button
13
- type="button"
14
- onClick={props.onClick}
15
- disabled={props.disabled}
16
- className="rte-toolbar-button"
17
- title="Formatierung entfernen"
18
- aria-label="Formatierung entfernen"
19
- >
20
- <IconWrapper icon="mdi:format-clear" width={18} height={18} />
21
- </button>
22
- ),
23
- execute: (editor: EditorAPI) => {
24
- editor.clearFormatting();
25
- },
26
- canExecute: (editor: EditorAPI) => {
27
- const selection = editor.getSelection();
28
- return selection !== null && selection.rangeCount > 0 && !selection.isCollapsed;
29
- },
30
- };
31
-
@@ -1,122 +0,0 @@
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
-
@@ -1,81 +0,0 @@
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
-