@overlap/rte 0.1.1 → 0.1.2

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.
@@ -0,0 +1,194 @@
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,23 +1,24 @@
1
- import React from 'react';
2
- import { Plugin, EditorAPI, ButtonProps } from '../types';
3
- import { Dropdown } from '../components/Dropdown';
4
- import { getCurrentHeading } from '../utils/stateReflection';
1
+ import { Dropdown } from "../components/Dropdown";
2
+ import { ButtonProps, EditorAPI, Plugin } from "../types";
3
+ import { getCurrentHeading } from "../utils/stateReflection";
5
4
 
6
- const defaultHeadings = ['h1', 'h2', 'h3'];
5
+ const defaultHeadings = ["h1", "h2", "h3"];
7
6
 
8
7
  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',
8
+ h1: "Überschrift 1",
9
+ h2: "Überschrift 2",
10
+ h3: "Überschrift 3",
11
+ h4: "Überschrift 4",
12
+ h5: "Überschrift 5",
13
+ h6: "Überschrift 6",
15
14
  };
16
15
 
17
- export function createHeadingsPlugin(headings: string[] = defaultHeadings): Plugin {
16
+ export function createHeadingsPlugin(
17
+ headings: string[] = defaultHeadings
18
+ ): Plugin {
18
19
  const options = [
19
- { value: 'p', label: 'Normal', headingPreview: 'p' },
20
- ...headings.map(h => ({
20
+ { value: "p", label: "Normal", headingPreview: "p" },
21
+ ...headings.map((h) => ({
21
22
  value: h,
22
23
  label: headingLabels[h] || h.toUpperCase(),
23
24
  headingPreview: h,
@@ -25,12 +26,22 @@ export function createHeadingsPlugin(headings: string[] = defaultHeadings): Plug
25
26
  ];
26
27
 
27
28
  return {
28
- name: 'headings',
29
- type: 'block',
30
- renderButton: (props: ButtonProps & { onSelect?: (value: string) => void; editorAPI?: EditorAPI; currentValue?: string }) => {
29
+ name: "headings",
30
+ type: "block",
31
+ renderButton: (
32
+ props: ButtonProps & {
33
+ onSelect?: (value: string) => void;
34
+ editorAPI?: EditorAPI;
35
+ currentValue?: string;
36
+ }
37
+ ) => {
31
38
  // Aktuelles Heading aus State Reflection
32
- const currentValue = props.currentValue || (props.editorAPI ? getCurrentHeading(props.editorAPI, headings) : undefined);
33
-
39
+ const currentValue =
40
+ props.currentValue ||
41
+ (props.editorAPI
42
+ ? getCurrentHeading(props.editorAPI, headings)
43
+ : undefined);
44
+
34
45
  return (
35
46
  <Dropdown
36
47
  icon="mdi:format-header-1"
@@ -52,25 +63,25 @@ export function createHeadingsPlugin(headings: string[] = defaultHeadings): Plug
52
63
  return getCurrentHeading(editor, headings);
53
64
  },
54
65
  execute: (editor: EditorAPI, value?: string) => {
55
- const tag = value || 'p';
56
- editor.executeCommand('formatBlock', `<${tag}>`);
66
+ const tag = value || "p";
67
+ editor.executeCommand("formatBlock", `<${tag}>`);
57
68
  },
58
69
  isActive: (editor: EditorAPI) => {
59
70
  const selection = editor.getSelection();
60
71
  if (!selection || selection.rangeCount === 0) return false;
61
-
72
+
62
73
  const range = selection.getRangeAt(0);
63
74
  const container = range.commonAncestorContainer;
64
- const element = container.nodeType === Node.TEXT_NODE
65
- ? container.parentElement
66
- : container as HTMLElement;
67
-
75
+ const element =
76
+ container.nodeType === Node.TEXT_NODE
77
+ ? container.parentElement
78
+ : (container as HTMLElement);
79
+
68
80
  if (!element) return false;
69
-
81
+
70
82
  const tagName = element.tagName.toLowerCase();
71
83
  return headings.includes(tagName);
72
84
  },
73
85
  canExecute: () => true,
74
86
  };
75
87
  }
76
-
@@ -0,0 +1,161 @@
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
+ ];
@@ -0,0 +1,90 @@
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
+ };