@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,244 @@
1
+ /**
2
+ * Entfernt alle Formatierungen aus der aktuellen Selection
3
+ */
4
+ export function clearFormatting(selection: Selection): void {
5
+ if (!selection || selection.rangeCount === 0) return;
6
+
7
+ const range = selection.getRangeAt(0);
8
+
9
+ // Wenn Selection leer ist, nichts zu tun
10
+ if (range.collapsed) return;
11
+
12
+ try {
13
+ // Verwende document.execCommand für Standard-Formatierungen
14
+ document.execCommand('removeFormat', false);
15
+ document.execCommand('unlink', false);
16
+
17
+ // Entferne alle inline styles und Formatierungen manuell
18
+ const walker = document.createTreeWalker(
19
+ range.commonAncestorContainer,
20
+ NodeFilter.SHOW_ELEMENT,
21
+ null
22
+ );
23
+
24
+ const elements: HTMLElement[] = [];
25
+ let node: Node | null = walker.currentNode as Node;
26
+
27
+ while (node) {
28
+ if (node.nodeType === Node.ELEMENT_NODE) {
29
+ const el = node as HTMLElement;
30
+ if (range.intersectsNode(el)) {
31
+ const tagName = el.tagName.toLowerCase();
32
+
33
+ // Entferne Inline-Formatierungen (strong, em, u, span, a, etc.)
34
+ if (['strong', 'b', 'em', 'i', 'u', 'span', 'a', 'font'].includes(tagName)) {
35
+ // Ersetze durch Text-Node
36
+ const text = el.textContent || '';
37
+ const textNode = document.createTextNode(text);
38
+ el.parentNode?.replaceChild(textNode, el);
39
+ }
40
+
41
+ // Entferne alle Styles
42
+ if (el.style.length > 0) {
43
+ el.removeAttribute('style');
44
+ }
45
+
46
+ // Konvertiere Headings zu Paragraphs
47
+ if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
48
+ const p = document.createElement('p');
49
+ while (el.firstChild) {
50
+ p.appendChild(el.firstChild);
51
+ }
52
+ el.parentNode?.replaceChild(p, el);
53
+ }
54
+ }
55
+ }
56
+ node = walker.nextNode();
57
+ }
58
+
59
+ // Normalisiere: Entferne leere Formatierungs-Tags
60
+ const normalizeWalker = document.createTreeWalker(
61
+ range.commonAncestorContainer,
62
+ NodeFilter.SHOW_ELEMENT,
63
+ null
64
+ );
65
+
66
+ node = normalizeWalker.currentNode as Node;
67
+ while (node) {
68
+ if (node.nodeType === Node.ELEMENT_NODE) {
69
+ const el = node as HTMLElement;
70
+ const tagName = el.tagName.toLowerCase();
71
+
72
+ if (['strong', 'b', 'em', 'i', 'u', 'span'].includes(tagName) && !el.textContent?.trim()) {
73
+ el.parentNode?.removeChild(el);
74
+ }
75
+ }
76
+ node = normalizeWalker.nextNode();
77
+ }
78
+
79
+ } catch (error) {
80
+ console.error('Error clearing formatting:', error);
81
+ // Fallback: Einfache Methode
82
+ document.execCommand('removeFormat', false);
83
+ document.execCommand('unlink', false);
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Entfernt nur Textfarbe
89
+ */
90
+ export function clearTextColor(selection: Selection): void {
91
+ if (!selection || selection.rangeCount === 0) return;
92
+
93
+ const range = selection.getRangeAt(0);
94
+ if (range.collapsed) return;
95
+
96
+ // Finde alle Elemente mit color style
97
+ const walker = document.createTreeWalker(
98
+ range.commonAncestorContainer,
99
+ NodeFilter.SHOW_ELEMENT,
100
+ null
101
+ );
102
+
103
+ const elements: HTMLElement[] = [];
104
+ let node: Node | null = walker.currentNode as Node;
105
+
106
+ while (node) {
107
+ if (node.nodeType === Node.ELEMENT_NODE) {
108
+ const el = node as HTMLElement;
109
+ if (range.intersectsNode(el) && el.style.color) {
110
+ elements.push(el);
111
+ }
112
+ }
113
+ node = walker.nextNode();
114
+ }
115
+
116
+ elements.forEach(el => {
117
+ el.style.color = '';
118
+ if (!el.style.length) {
119
+ el.removeAttribute('style');
120
+ }
121
+ });
122
+ }
123
+
124
+ /**
125
+ * Entfernt nur Hintergrundfarbe
126
+ */
127
+ export function clearBackgroundColor(selection: Selection): void {
128
+ if (!selection || selection.rangeCount === 0) return;
129
+
130
+ const range = selection.getRangeAt(0);
131
+ if (range.collapsed) return;
132
+
133
+ // Finde alle Elemente mit backgroundColor style
134
+ const walker = document.createTreeWalker(
135
+ range.commonAncestorContainer,
136
+ NodeFilter.SHOW_ELEMENT,
137
+ null
138
+ );
139
+
140
+ const elements: HTMLElement[] = [];
141
+ let node: Node | null = walker.currentNode as Node;
142
+
143
+ while (node) {
144
+ if (node.nodeType === Node.ELEMENT_NODE) {
145
+ const el = node as HTMLElement;
146
+ if (range.intersectsNode(el) && el.style.backgroundColor) {
147
+ elements.push(el);
148
+ }
149
+ }
150
+ node = walker.nextNode();
151
+ }
152
+
153
+ elements.forEach(el => {
154
+ el.style.backgroundColor = '';
155
+ if (!el.style.length) {
156
+ el.removeAttribute('style');
157
+ }
158
+ });
159
+ }
160
+
161
+ /**
162
+ * Entfernt nur Font-Size
163
+ */
164
+ export function clearFontSize(selection: Selection): void {
165
+ if (!selection || selection.rangeCount === 0) return;
166
+
167
+ const range = selection.getRangeAt(0);
168
+ if (range.collapsed) return;
169
+
170
+ // Finde alle Elemente mit fontSize style
171
+ const walker = document.createTreeWalker(
172
+ range.commonAncestorContainer,
173
+ NodeFilter.SHOW_ELEMENT,
174
+ null
175
+ );
176
+
177
+ const elements: HTMLElement[] = [];
178
+ let node: Node | null = walker.currentNode as Node;
179
+
180
+ while (node) {
181
+ if (node.nodeType === Node.ELEMENT_NODE) {
182
+ const el = node as HTMLElement;
183
+ if (range.intersectsNode(el) && el.style.fontSize) {
184
+ elements.push(el);
185
+ }
186
+ }
187
+ node = walker.nextNode();
188
+ }
189
+
190
+ elements.forEach(el => {
191
+ el.style.fontSize = '';
192
+ if (!el.style.length) {
193
+ el.removeAttribute('style');
194
+ }
195
+ });
196
+ }
197
+
198
+ /**
199
+ * Entfernt Links (behält Text)
200
+ */
201
+ export function clearLinks(selection: Selection): void {
202
+ if (!selection || selection.rangeCount === 0) return;
203
+
204
+ const range = selection.getRangeAt(0);
205
+
206
+ // Finde alle Links in der Selection
207
+ const walker = document.createTreeWalker(
208
+ range.commonAncestorContainer,
209
+ NodeFilter.SHOW_ELEMENT,
210
+ {
211
+ acceptNode: (node: Node) => {
212
+ if (node.nodeType === Node.ELEMENT_NODE) {
213
+ const el = node as HTMLElement;
214
+ if (el.tagName.toLowerCase() === 'a' && range.intersectsNode(el)) {
215
+ return NodeFilter.FILTER_ACCEPT;
216
+ }
217
+ }
218
+ return NodeFilter.FILTER_SKIP;
219
+ }
220
+ }
221
+ );
222
+
223
+ const links: HTMLAnchorElement[] = [];
224
+ let node: Node | null = walker.currentNode as Node;
225
+
226
+ while (node) {
227
+ if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName.toLowerCase() === 'a') {
228
+ links.push(node as HTMLAnchorElement);
229
+ }
230
+ node = walker.nextNode();
231
+ }
232
+
233
+ // Entferne Links (behält Text)
234
+ links.forEach(link => {
235
+ const parent = link.parentNode;
236
+ if (parent) {
237
+ while (link.firstChild) {
238
+ parent.insertBefore(link.firstChild, link);
239
+ }
240
+ parent.removeChild(link);
241
+ }
242
+ });
243
+ }
244
+
@@ -0,0 +1,290 @@
1
+ import React from "react";
2
+ import { EditorContent, EditorNode } from "../types";
3
+
4
+ export function domToContent(element: HTMLElement): EditorContent {
5
+ const blocks: EditorNode[] = [];
6
+
7
+ function processNode(node: Node): EditorNode | null {
8
+ if (node.nodeType === Node.TEXT_NODE) {
9
+ const text = node.textContent;
10
+ if (text === null || text === undefined) return null;
11
+ return { type: "text", text };
12
+ }
13
+
14
+ if (node.nodeType === Node.ELEMENT_NODE) {
15
+ const el = node as HTMLElement;
16
+ const tagName = el.tagName.toLowerCase();
17
+
18
+ if (tagName === "img") {
19
+ const attributes: Record<string, string> = {};
20
+ const src = el.getAttribute("src");
21
+ const alt = el.getAttribute("alt");
22
+ if (src) attributes.src = src;
23
+ if (alt) attributes.alt = alt;
24
+ return {
25
+ type: "image",
26
+ attributes:
27
+ Object.keys(attributes).length > 0
28
+ ? attributes
29
+ : undefined,
30
+ };
31
+ }
32
+
33
+ if (
34
+ [
35
+ "p",
36
+ "div",
37
+ "h1",
38
+ "h2",
39
+ "h3",
40
+ "h4",
41
+ "h5",
42
+ "h6",
43
+ "blockquote",
44
+ "ul",
45
+ "ol",
46
+ "li",
47
+ ].includes(tagName)
48
+ ) {
49
+ const children: EditorNode[] = [];
50
+ Array.from(el.childNodes).forEach((child) => {
51
+ const processed = processNode(child);
52
+ if (processed) children.push(processed);
53
+ });
54
+
55
+ const attributes: Record<string, string> = {};
56
+ if (tagName === "a") {
57
+ const href = el.getAttribute("href");
58
+ if (href) attributes.href = href;
59
+ }
60
+
61
+ return {
62
+ type: tagName,
63
+ children: children.length > 0 ? children : [],
64
+ attributes:
65
+ Object.keys(attributes).length > 0
66
+ ? attributes
67
+ : undefined,
68
+ };
69
+ }
70
+
71
+ if (
72
+ ["strong", "b", "em", "i", "u", "a", "span"].includes(tagName)
73
+ ) {
74
+ const children: EditorNode[] = [];
75
+ Array.from(el.childNodes).forEach((child) => {
76
+ const processed = processNode(child);
77
+ if (processed) children.push(processed);
78
+ });
79
+
80
+ const attributes: Record<string, string> = {};
81
+
82
+ Array.from(el.attributes).forEach((attr) => {
83
+ attributes[attr.name] = attr.value;
84
+ });
85
+
86
+ if (tagName === "a") {
87
+ const href = el.getAttribute("href");
88
+ if (href) attributes.href = href;
89
+ return {
90
+ type: "link",
91
+ children: children.length > 0 ? children : undefined,
92
+ attributes:
93
+ Object.keys(attributes).length > 0
94
+ ? attributes
95
+ : undefined,
96
+ };
97
+ }
98
+
99
+ if (tagName === "span") {
100
+ const style = el.getAttribute("style");
101
+ if (style) {
102
+ const styleObj: Record<string, string> = {};
103
+ style.split(";").forEach((rule) => {
104
+ const [key, value] = rule
105
+ .split(":")
106
+ .map((s) => s.trim());
107
+ if (key && value) {
108
+ if (key === "font-size") {
109
+ attributes.fontSize = value;
110
+ } else if (key === "color") {
111
+ attributes.color = value;
112
+ } else if (key === "background-color") {
113
+ attributes.backgroundColor = value;
114
+ }
115
+ }
116
+ });
117
+ }
118
+ }
119
+
120
+ return {
121
+ type:
122
+ tagName === "strong" || tagName === "b"
123
+ ? "bold"
124
+ : tagName === "em" || tagName === "i"
125
+ ? "italic"
126
+ : tagName === "u"
127
+ ? "underline"
128
+ : tagName,
129
+ children: children.length > 0 ? children : undefined,
130
+ attributes:
131
+ Object.keys(attributes).length > 0
132
+ ? attributes
133
+ : undefined,
134
+ };
135
+ }
136
+
137
+ const children: EditorNode[] = [];
138
+ Array.from(el.childNodes).forEach((child) => {
139
+ const processed = processNode(child);
140
+ if (processed) children.push(processed);
141
+ });
142
+
143
+ if (children.length > 0) {
144
+ return {
145
+ type: tagName,
146
+ children,
147
+ };
148
+ }
149
+ }
150
+
151
+ return null;
152
+ }
153
+
154
+ Array.from(element.childNodes).forEach((node) => {
155
+ const processed = processNode(node);
156
+ if (processed) {
157
+ if (processed.type === "text") {
158
+ blocks.push({ type: "p", children: [processed] });
159
+ } else {
160
+ blocks.push(processed);
161
+ }
162
+ }
163
+ });
164
+
165
+ if (blocks.length === 0) {
166
+ blocks.push({ type: "p", children: [] });
167
+ }
168
+
169
+ return { blocks };
170
+ }
171
+
172
+ export function contentToDOM(
173
+ content: EditorContent,
174
+ container: HTMLElement,
175
+ customLinkComponent?: React.ComponentType<{
176
+ href: string;
177
+ children: React.ReactNode;
178
+ [key: string]: any;
179
+ }>,
180
+ customHeadingRenderer?: (
181
+ level: string,
182
+ children: React.ReactNode
183
+ ) => React.ReactElement
184
+ ): void {
185
+ container.innerHTML = "";
186
+
187
+ function createNode(node: EditorNode): Node {
188
+ if (node.type === "text" && node.text !== undefined) {
189
+ return document.createTextNode(node.text);
190
+ }
191
+
192
+ if (node.type === "image") {
193
+ const img = document.createElement("img");
194
+ if (node.attributes) {
195
+ if (node.attributes.src)
196
+ img.setAttribute("src", node.attributes.src);
197
+ if (node.attributes.alt)
198
+ img.setAttribute("alt", node.attributes.alt);
199
+ if (node.attributes.uploading === "true") {
200
+ img.setAttribute("data-uploading", "true");
201
+ }
202
+ }
203
+ img.style.maxWidth = "100%";
204
+ img.style.height = "auto";
205
+ img.style.display = "block";
206
+ img.style.margin = "16px 0";
207
+ return img;
208
+ }
209
+
210
+ const tagMap: Record<string, string> = {
211
+ bold: "strong",
212
+ italic: "em",
213
+ underline: "u",
214
+ link: "a",
215
+ };
216
+
217
+ let tagName = tagMap[node.type] || node.type;
218
+
219
+ if (node.type === "link" && customLinkComponent) {
220
+ tagName = "a";
221
+ if (node.attributes) {
222
+ node.attributes["data-custom-link"] = "true";
223
+ }
224
+ }
225
+
226
+ const element = document.createElement(tagName);
227
+
228
+ if (node.attributes) {
229
+ Object.entries(node.attributes).forEach(([key, value]) => {
230
+ if (
231
+ key === "fontSize" ||
232
+ key === "color" ||
233
+ key === "backgroundColor"
234
+ ) {
235
+ const currentStyle = element.getAttribute("style") || "";
236
+ if (key === "fontSize") {
237
+ element.setAttribute(
238
+ "style",
239
+ `${currentStyle}font-size: ${value};`.trim()
240
+ );
241
+ } else if (key === "color") {
242
+ element.setAttribute(
243
+ "style",
244
+ `${currentStyle}color: ${value};`.trim()
245
+ );
246
+ } else if (key === "backgroundColor") {
247
+ element.setAttribute(
248
+ "style",
249
+ `${currentStyle}background-color: ${value};`.trim()
250
+ );
251
+ }
252
+ } else {
253
+ element.setAttribute(key, value);
254
+ }
255
+ });
256
+ }
257
+
258
+ if (node.children) {
259
+ node.children.forEach((child) => {
260
+ const childNode = createNode(child);
261
+ element.appendChild(childNode);
262
+ });
263
+ }
264
+
265
+ return element;
266
+ }
267
+
268
+ content.blocks.forEach((block) => {
269
+ const blockNode = createNode(block);
270
+ container.appendChild(blockNode);
271
+ });
272
+ }
273
+
274
+ export function createEmptyContent(): EditorContent {
275
+ return {
276
+ blocks: [{ type: "p", children: [] }],
277
+ };
278
+ }
279
+
280
+ export function htmlToContent(htmlString: string): EditorContent {
281
+ const tempDiv = document.createElement("div");
282
+ tempDiv.innerHTML = htmlString.trim();
283
+ return domToContent(tempDiv);
284
+ }
285
+
286
+ export function contentToHTML(content: EditorContent): string {
287
+ const tempDiv = document.createElement("div");
288
+ contentToDOM(content, tempDiv);
289
+ return tempDiv.innerHTML;
290
+ }
@@ -0,0 +1,59 @@
1
+ import { EditorContent } from '../types';
2
+
3
+ export class HistoryManager {
4
+ private history: EditorContent[] = [];
5
+ private currentIndex: number = -1;
6
+ private maxHistorySize: number = 50;
7
+
8
+ push(content: EditorContent): void {
9
+ // Entferne alle Einträge nach currentIndex (wenn wir zurückgegangen sind)
10
+ this.history = this.history.slice(0, this.currentIndex + 1);
11
+
12
+ // Füge neuen Eintrag hinzu
13
+ this.history.push(JSON.parse(JSON.stringify(content))); // Deep clone
14
+ this.currentIndex++;
15
+
16
+ // Begrenze die Historie
17
+ if (this.history.length > this.maxHistorySize) {
18
+ this.history.shift();
19
+ this.currentIndex--;
20
+ }
21
+ }
22
+
23
+ undo(): EditorContent | null {
24
+ if (this.canUndo()) {
25
+ this.currentIndex--;
26
+ return JSON.parse(JSON.stringify(this.history[this.currentIndex]));
27
+ }
28
+ return null;
29
+ }
30
+
31
+ redo(): EditorContent | null {
32
+ if (this.canRedo()) {
33
+ this.currentIndex++;
34
+ return JSON.parse(JSON.stringify(this.history[this.currentIndex]));
35
+ }
36
+ return null;
37
+ }
38
+
39
+ canUndo(): boolean {
40
+ return this.currentIndex > 0;
41
+ }
42
+
43
+ canRedo(): boolean {
44
+ return this.currentIndex < this.history.length - 1;
45
+ }
46
+
47
+ getCurrent(): EditorContent | null {
48
+ if (this.currentIndex >= 0 && this.currentIndex < this.history.length) {
49
+ return JSON.parse(JSON.stringify(this.history[this.currentIndex]));
50
+ }
51
+ return null;
52
+ }
53
+
54
+ reset(): void {
55
+ this.history = [];
56
+ this.currentIndex = -1;
57
+ }
58
+ }
59
+