@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.
- package/README.md +269 -0
- package/dist/components/Dropdown.d.ts +19 -0
- package/dist/components/Dropdown.d.ts.map +1 -0
- package/dist/components/Editor.d.ts +4 -0
- package/dist/components/Editor.d.ts.map +1 -0
- package/dist/components/FloatingToolbar.d.ts +10 -0
- package/dist/components/FloatingToolbar.d.ts.map +1 -0
- package/dist/components/IconWrapper.d.ts +10 -0
- package/dist/components/IconWrapper.d.ts.map +1 -0
- package/dist/components/Icons.d.ts +32 -0
- package/dist/components/Icons.d.ts.map +1 -0
- package/dist/components/Toolbar.d.ts +10 -0
- package/dist/components/Toolbar.d.ts.map +1 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/index.d.ts +208 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.esm.js +2080 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +2116 -0
- package/dist/index.js.map +1 -0
- package/dist/plugins/base.d.ts +10 -0
- package/dist/plugins/base.d.ts.map +1 -0
- package/dist/plugins/clearFormatting.d.ts +6 -0
- package/dist/plugins/clearFormatting.d.ts.map +1 -0
- package/dist/plugins/colors.d.ts +4 -0
- package/dist/plugins/colors.d.ts.map +1 -0
- package/dist/plugins/fontSize.d.ts +3 -0
- package/dist/plugins/fontSize.d.ts.map +1 -0
- package/dist/plugins/headings.d.ts +3 -0
- package/dist/plugins/headings.d.ts.map +1 -0
- package/dist/plugins/image.d.ts +6 -0
- package/dist/plugins/image.d.ts.map +1 -0
- package/dist/plugins/index.d.ts +14 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/optional.d.ts +19 -0
- package/dist/plugins/optional.d.ts.map +1 -0
- package/dist/styles.css +638 -0
- package/dist/types.d.ts +81 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils/clearFormatting.d.ts +21 -0
- package/dist/utils/clearFormatting.d.ts.map +1 -0
- package/dist/utils/content.d.ts +12 -0
- package/dist/utils/content.d.ts.map +1 -0
- package/dist/utils/history.d.ts +14 -0
- package/dist/utils/history.d.ts.map +1 -0
- package/dist/utils/listIndent.d.ts +9 -0
- package/dist/utils/listIndent.d.ts.map +1 -0
- package/dist/utils/stateReflection.d.ts +18 -0
- package/dist/utils/stateReflection.d.ts.map +1 -0
- package/package.json +48 -0
- package/src/components/Dropdown.tsx +103 -0
- package/src/components/Editor.css +2 -0
- package/src/components/Editor.tsx +785 -0
- package/src/components/FloatingToolbar.tsx +214 -0
- package/src/components/IconWrapper.tsx +14 -0
- package/src/components/Icons.tsx +145 -0
- package/src/components/Toolbar.tsx +137 -0
- package/src/components/index.ts +3 -0
- package/src/index.ts +19 -0
- package/src/plugins/base.tsx +91 -0
- package/src/plugins/clearFormatting.tsx +31 -0
- package/src/plugins/colors.tsx +122 -0
- package/src/plugins/fontSize.tsx +81 -0
- package/src/plugins/headings.tsx +76 -0
- package/src/plugins/image.tsx +189 -0
- package/src/plugins/index.ts +54 -0
- package/src/plugins/optional.tsx +221 -0
- package/src/styles.css +638 -0
- package/src/types.ts +92 -0
- package/src/utils/clearFormatting.ts +244 -0
- package/src/utils/content.ts +290 -0
- package/src/utils/history.ts +59 -0
- package/src/utils/listIndent.ts +171 -0
- 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
|
+
|