@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.
- package/package.json +57 -58
- package/src/components/Dropdown.tsx +0 -103
- package/src/components/Editor.css +0 -2
- package/src/components/Editor.tsx +0 -828
- package/src/components/FloatingToolbar.tsx +0 -214
- package/src/components/IconWrapper.tsx +0 -14
- package/src/components/Icons.tsx +0 -374
- package/src/components/Toolbar.tsx +0 -137
- package/src/components/index.ts +0 -3
- package/src/index.ts +0 -19
- package/src/plugins/base.tsx +0 -91
- package/src/plugins/blockFormat.tsx +0 -194
- package/src/plugins/clearFormatting.tsx +0 -31
- package/src/plugins/colors.tsx +0 -122
- package/src/plugins/fontSize.tsx +0 -81
- package/src/plugins/headings.tsx +0 -87
- package/src/plugins/image.tsx +0 -189
- package/src/plugins/index.tsx +0 -161
- package/src/plugins/listIndent.tsx +0 -90
- package/src/plugins/optional.tsx +0 -243
- package/src/styles.css +0 -638
- package/src/types.ts +0 -95
- package/src/utils/clearFormatting.ts +0 -244
- package/src/utils/content.ts +0 -290
- package/src/utils/history.ts +0 -59
- package/src/utils/listIndent.ts +0 -171
- package/src/utils/stateReflection.ts +0 -175
|
@@ -1,828 +0,0 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
|
2
|
-
import { defaultPlugins } from "../plugins";
|
|
3
|
-
import { createBlockFormatPlugin } from "../plugins/blockFormat";
|
|
4
|
-
import {
|
|
5
|
-
createBackgroundColorPlugin,
|
|
6
|
-
createTextColorPlugin,
|
|
7
|
-
} from "../plugins/colors";
|
|
8
|
-
import { createFontSizePlugin } from "../plugins/fontSize";
|
|
9
|
-
import { createImagePlugin } from "../plugins/image";
|
|
10
|
-
import { EditorAPI, EditorContent, EditorProps } from "../types";
|
|
11
|
-
import {
|
|
12
|
-
clearBackgroundColor,
|
|
13
|
-
clearFontSize,
|
|
14
|
-
clearFormatting,
|
|
15
|
-
clearLinks,
|
|
16
|
-
clearTextColor,
|
|
17
|
-
} from "../utils/clearFormatting";
|
|
18
|
-
import {
|
|
19
|
-
contentToDOM,
|
|
20
|
-
contentToHTML,
|
|
21
|
-
createEmptyContent,
|
|
22
|
-
domToContent,
|
|
23
|
-
htmlToContent,
|
|
24
|
-
} from "../utils/content";
|
|
25
|
-
import { HistoryManager } from "../utils/history";
|
|
26
|
-
import { indentListItem, outdentListItem } from "../utils/listIndent";
|
|
27
|
-
import { Toolbar } from "./Toolbar";
|
|
28
|
-
|
|
29
|
-
export const Editor: React.FC<EditorProps> = ({
|
|
30
|
-
initialContent,
|
|
31
|
-
onChange,
|
|
32
|
-
plugins: providedPlugins,
|
|
33
|
-
placeholder = "Text eingeben...",
|
|
34
|
-
className,
|
|
35
|
-
toolbarClassName,
|
|
36
|
-
editorClassName,
|
|
37
|
-
fontSizes,
|
|
38
|
-
colors,
|
|
39
|
-
headings,
|
|
40
|
-
customLinkComponent,
|
|
41
|
-
customHeadingRenderer,
|
|
42
|
-
customRenderer,
|
|
43
|
-
onEditorAPIReady,
|
|
44
|
-
theme,
|
|
45
|
-
onImageUpload,
|
|
46
|
-
}) => {
|
|
47
|
-
const plugins = useMemo(() => {
|
|
48
|
-
const allPlugins = [...(providedPlugins || defaultPlugins)];
|
|
49
|
-
|
|
50
|
-
if (fontSizes && fontSizes.length > 0) {
|
|
51
|
-
allPlugins.push(createFontSizePlugin(fontSizes));
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (colors && colors.length > 0) {
|
|
55
|
-
allPlugins.push(createTextColorPlugin(colors));
|
|
56
|
-
allPlugins.push(createBackgroundColorPlugin(colors));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// BlockFormat Plugin ist bereits in defaultPlugins enthalten
|
|
60
|
-
// Wenn custom headings angegeben sind, ersetze das Standard-Plugin
|
|
61
|
-
if (headings && headings.length > 0) {
|
|
62
|
-
// Entferne das Standard-BlockFormat-Plugin
|
|
63
|
-
const blockFormatIndex = allPlugins.findIndex(
|
|
64
|
-
(p) => p.name === "blockFormat"
|
|
65
|
-
);
|
|
66
|
-
if (blockFormatIndex !== -1) {
|
|
67
|
-
allPlugins.splice(blockFormatIndex, 1);
|
|
68
|
-
}
|
|
69
|
-
// Füge das Plugin mit custom Headlines hinzu
|
|
70
|
-
allPlugins.push(createBlockFormatPlugin(headings));
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
allPlugins.push(createImagePlugin(onImageUpload));
|
|
74
|
-
|
|
75
|
-
return allPlugins;
|
|
76
|
-
}, [providedPlugins, fontSizes, colors, headings, onImageUpload]);
|
|
77
|
-
const editorRef = useRef<HTMLDivElement>(null);
|
|
78
|
-
const historyRef = useRef<HistoryManager>(new HistoryManager());
|
|
79
|
-
const isUpdatingRef = useRef(false);
|
|
80
|
-
|
|
81
|
-
const notifyChange = useCallback(
|
|
82
|
-
(content: EditorContent) => {
|
|
83
|
-
if (onChange && !isUpdatingRef.current) {
|
|
84
|
-
onChange(content);
|
|
85
|
-
}
|
|
86
|
-
},
|
|
87
|
-
[onChange]
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
const restoreSelection = useCallback((editor: HTMLElement) => {
|
|
91
|
-
if (typeof window === "undefined" || typeof document === "undefined")
|
|
92
|
-
return;
|
|
93
|
-
const range = document.createRange();
|
|
94
|
-
const selection = window.getSelection();
|
|
95
|
-
|
|
96
|
-
if (editor.firstChild) {
|
|
97
|
-
range.setStart(editor.firstChild, 0);
|
|
98
|
-
range.collapse(true);
|
|
99
|
-
selection?.removeAllRanges();
|
|
100
|
-
selection?.addRange(range);
|
|
101
|
-
}
|
|
102
|
-
}, []);
|
|
103
|
-
|
|
104
|
-
const editorAPI = useMemo<EditorAPI>(() => {
|
|
105
|
-
const executeCommand = (command: string, value?: string): boolean => {
|
|
106
|
-
const editor = editorRef.current;
|
|
107
|
-
if (!editor) return false;
|
|
108
|
-
|
|
109
|
-
if (
|
|
110
|
-
command !== "undo" &&
|
|
111
|
-
command !== "redo" &&
|
|
112
|
-
command !== "insertImage"
|
|
113
|
-
) {
|
|
114
|
-
const currentContent = domToContent(editor);
|
|
115
|
-
historyRef.current.push(currentContent);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (command === "undo") {
|
|
119
|
-
const content = historyRef.current.undo();
|
|
120
|
-
if (content && editor) {
|
|
121
|
-
isUpdatingRef.current = true;
|
|
122
|
-
contentToDOM(
|
|
123
|
-
content,
|
|
124
|
-
editor,
|
|
125
|
-
customLinkComponent,
|
|
126
|
-
customHeadingRenderer
|
|
127
|
-
);
|
|
128
|
-
restoreSelection(editor);
|
|
129
|
-
isUpdatingRef.current = false;
|
|
130
|
-
notifyChange(content);
|
|
131
|
-
}
|
|
132
|
-
return true;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (command === "redo") {
|
|
136
|
-
const content = historyRef.current.redo();
|
|
137
|
-
if (content && editor) {
|
|
138
|
-
isUpdatingRef.current = true;
|
|
139
|
-
contentToDOM(
|
|
140
|
-
content,
|
|
141
|
-
editor,
|
|
142
|
-
customLinkComponent,
|
|
143
|
-
customHeadingRenderer
|
|
144
|
-
);
|
|
145
|
-
restoreSelection(editor);
|
|
146
|
-
isUpdatingRef.current = false;
|
|
147
|
-
notifyChange(content);
|
|
148
|
-
}
|
|
149
|
-
return true;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (command === "insertImage" && value) {
|
|
153
|
-
let selection = window.getSelection();
|
|
154
|
-
if (!selection) return false;
|
|
155
|
-
|
|
156
|
-
if (document.activeElement !== editor) {
|
|
157
|
-
editor.focus();
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (selection.rangeCount === 0) {
|
|
161
|
-
const range = document.createRange();
|
|
162
|
-
if (editor.childNodes.length > 0) {
|
|
163
|
-
const lastChild =
|
|
164
|
-
editor.childNodes[editor.childNodes.length - 1];
|
|
165
|
-
range.setStartAfter(lastChild);
|
|
166
|
-
range.collapse(true);
|
|
167
|
-
} else {
|
|
168
|
-
const img = document.createElement("img");
|
|
169
|
-
img.setAttribute("src", value);
|
|
170
|
-
img.setAttribute("alt", "");
|
|
171
|
-
img.style.maxWidth = "100%";
|
|
172
|
-
img.style.height = "auto";
|
|
173
|
-
img.style.display = "block";
|
|
174
|
-
img.style.margin = "16px 0";
|
|
175
|
-
editor.appendChild(img);
|
|
176
|
-
|
|
177
|
-
const newRange = document.createRange();
|
|
178
|
-
newRange.setStartAfter(img);
|
|
179
|
-
newRange.collapse(true);
|
|
180
|
-
selection.removeAllRanges();
|
|
181
|
-
selection.addRange(newRange);
|
|
182
|
-
|
|
183
|
-
isUpdatingRef.current = true;
|
|
184
|
-
setTimeout(() => {
|
|
185
|
-
if (editor) {
|
|
186
|
-
const currentContent = domToContent(editor);
|
|
187
|
-
historyRef.current.push(currentContent);
|
|
188
|
-
isUpdatingRef.current = false;
|
|
189
|
-
notifyChange(currentContent);
|
|
190
|
-
}
|
|
191
|
-
}, 0);
|
|
192
|
-
return true;
|
|
193
|
-
}
|
|
194
|
-
selection.removeAllRanges();
|
|
195
|
-
selection.addRange(range);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (selection.rangeCount === 0) return false;
|
|
199
|
-
const range = selection.getRangeAt(0);
|
|
200
|
-
|
|
201
|
-
const container = range.commonAncestorContainer;
|
|
202
|
-
let parentElement: HTMLElement | null = null;
|
|
203
|
-
|
|
204
|
-
if (container.nodeType === Node.TEXT_NODE) {
|
|
205
|
-
parentElement = container.parentElement;
|
|
206
|
-
} else if (container.nodeType === Node.ELEMENT_NODE) {
|
|
207
|
-
parentElement = container as HTMLElement;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const img = document.createElement("img");
|
|
211
|
-
img.setAttribute("src", value);
|
|
212
|
-
img.setAttribute("alt", "");
|
|
213
|
-
img.style.maxWidth = "100%";
|
|
214
|
-
img.style.height = "auto";
|
|
215
|
-
img.style.display = "block";
|
|
216
|
-
img.style.margin = "16px 0";
|
|
217
|
-
|
|
218
|
-
if (
|
|
219
|
-
parentElement &&
|
|
220
|
-
parentElement !== editor &&
|
|
221
|
-
(parentElement.tagName === "P" ||
|
|
222
|
-
parentElement.tagName === "DIV" ||
|
|
223
|
-
parentElement.tagName === "H1" ||
|
|
224
|
-
parentElement.tagName === "H2" ||
|
|
225
|
-
parentElement.tagName === "H3" ||
|
|
226
|
-
parentElement.tagName === "H4" ||
|
|
227
|
-
parentElement.tagName === "H5" ||
|
|
228
|
-
parentElement.tagName === "H6")
|
|
229
|
-
) {
|
|
230
|
-
if (parentElement.nextSibling) {
|
|
231
|
-
editor.insertBefore(img, parentElement.nextSibling);
|
|
232
|
-
} else {
|
|
233
|
-
editor.appendChild(img);
|
|
234
|
-
}
|
|
235
|
-
} else {
|
|
236
|
-
try {
|
|
237
|
-
range.insertNode(img);
|
|
238
|
-
} catch (e) {
|
|
239
|
-
editor.appendChild(img);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const newRange = document.createRange();
|
|
244
|
-
newRange.setStartAfter(img);
|
|
245
|
-
newRange.collapse(true);
|
|
246
|
-
selection.removeAllRanges();
|
|
247
|
-
selection.addRange(newRange);
|
|
248
|
-
|
|
249
|
-
isUpdatingRef.current = true;
|
|
250
|
-
setTimeout(() => {
|
|
251
|
-
if (editor) {
|
|
252
|
-
const currentContent = domToContent(editor);
|
|
253
|
-
historyRef.current.push(currentContent);
|
|
254
|
-
isUpdatingRef.current = false;
|
|
255
|
-
notifyChange(currentContent);
|
|
256
|
-
}
|
|
257
|
-
}, 0);
|
|
258
|
-
|
|
259
|
-
return true;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const selection = window.getSelection();
|
|
263
|
-
let savedRange: Range | null = null;
|
|
264
|
-
|
|
265
|
-
if (selection && selection.rangeCount > 0) {
|
|
266
|
-
savedRange = selection.getRangeAt(0).cloneRange();
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if (document.activeElement !== editor) {
|
|
270
|
-
editor.focus();
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
if (!selection || selection.rangeCount === 0) {
|
|
274
|
-
const range = document.createRange();
|
|
275
|
-
|
|
276
|
-
if (editor.childNodes.length > 0) {
|
|
277
|
-
const lastChild =
|
|
278
|
-
editor.childNodes[editor.childNodes.length - 1];
|
|
279
|
-
if (lastChild.nodeType === Node.TEXT_NODE) {
|
|
280
|
-
range.setStart(
|
|
281
|
-
lastChild,
|
|
282
|
-
lastChild.textContent?.length || 0
|
|
283
|
-
);
|
|
284
|
-
range.setEnd(
|
|
285
|
-
lastChild,
|
|
286
|
-
lastChild.textContent?.length || 0
|
|
287
|
-
);
|
|
288
|
-
} else {
|
|
289
|
-
range.selectNodeContents(lastChild);
|
|
290
|
-
range.collapse(false);
|
|
291
|
-
}
|
|
292
|
-
} else {
|
|
293
|
-
const p = document.createElement("p");
|
|
294
|
-
editor.appendChild(p);
|
|
295
|
-
const textNode = document.createTextNode("");
|
|
296
|
-
p.appendChild(textNode);
|
|
297
|
-
range.setStart(textNode, 0);
|
|
298
|
-
range.setEnd(textNode, 0);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
selection?.removeAllRanges();
|
|
302
|
-
selection?.addRange(range);
|
|
303
|
-
} else if (savedRange) {
|
|
304
|
-
selection.removeAllRanges();
|
|
305
|
-
selection.addRange(savedRange);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
document.execCommand(command, false, value);
|
|
309
|
-
|
|
310
|
-
setTimeout(() => {
|
|
311
|
-
if (editor && !isUpdatingRef.current) {
|
|
312
|
-
const content = domToContent(editor);
|
|
313
|
-
notifyChange(content);
|
|
314
|
-
}
|
|
315
|
-
}, 0);
|
|
316
|
-
|
|
317
|
-
return true;
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
return {
|
|
321
|
-
executeCommand,
|
|
322
|
-
|
|
323
|
-
getSelection: (): Selection | null => {
|
|
324
|
-
if (typeof window === "undefined") return null;
|
|
325
|
-
return window.getSelection();
|
|
326
|
-
},
|
|
327
|
-
|
|
328
|
-
getContent: (): EditorContent => {
|
|
329
|
-
const editor = editorRef.current;
|
|
330
|
-
if (!editor) return createEmptyContent();
|
|
331
|
-
return domToContent(editor);
|
|
332
|
-
},
|
|
333
|
-
|
|
334
|
-
setContent: (content: EditorContent): void => {
|
|
335
|
-
const editor = editorRef.current;
|
|
336
|
-
if (!editor) return;
|
|
337
|
-
|
|
338
|
-
isUpdatingRef.current = true;
|
|
339
|
-
contentToDOM(
|
|
340
|
-
content,
|
|
341
|
-
editor,
|
|
342
|
-
customLinkComponent,
|
|
343
|
-
customHeadingRenderer
|
|
344
|
-
);
|
|
345
|
-
historyRef.current.push(content);
|
|
346
|
-
isUpdatingRef.current = false;
|
|
347
|
-
notifyChange(content);
|
|
348
|
-
},
|
|
349
|
-
|
|
350
|
-
insertBlock: (
|
|
351
|
-
type: string,
|
|
352
|
-
attributes?: Record<string, string>
|
|
353
|
-
): void => {
|
|
354
|
-
const selection = window.getSelection();
|
|
355
|
-
if (!selection || selection.rangeCount === 0) return;
|
|
356
|
-
|
|
357
|
-
const range = selection.getRangeAt(0);
|
|
358
|
-
const block = document.createElement(type);
|
|
359
|
-
|
|
360
|
-
if (attributes) {
|
|
361
|
-
Object.entries(attributes).forEach(([key, value]) => {
|
|
362
|
-
block.setAttribute(key, value);
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
range.insertNode(block);
|
|
367
|
-
const textNode = document.createTextNode("\u200B"); // Zero-width space
|
|
368
|
-
block.appendChild(textNode);
|
|
369
|
-
|
|
370
|
-
// Cursor setzen
|
|
371
|
-
range.setStartAfter(textNode);
|
|
372
|
-
range.collapse(true);
|
|
373
|
-
selection.removeAllRanges();
|
|
374
|
-
selection.addRange(range);
|
|
375
|
-
|
|
376
|
-
const editor = editorRef.current;
|
|
377
|
-
if (editor) {
|
|
378
|
-
const content = domToContent(editor);
|
|
379
|
-
notifyChange(content);
|
|
380
|
-
}
|
|
381
|
-
},
|
|
382
|
-
|
|
383
|
-
insertInline: (
|
|
384
|
-
type: string,
|
|
385
|
-
attributes?: Record<string, string>
|
|
386
|
-
): void => {
|
|
387
|
-
const selection = window.getSelection();
|
|
388
|
-
if (!selection || selection.rangeCount === 0) return;
|
|
389
|
-
|
|
390
|
-
const range = selection.getRangeAt(0);
|
|
391
|
-
const inline = document.createElement(type);
|
|
392
|
-
|
|
393
|
-
if (attributes) {
|
|
394
|
-
Object.entries(attributes).forEach(([key, value]) => {
|
|
395
|
-
inline.setAttribute(key, value);
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
try {
|
|
400
|
-
range.surroundContents(inline);
|
|
401
|
-
} catch (e) {
|
|
402
|
-
// Falls surroundContents fehlschlägt, versuche es anders
|
|
403
|
-
const contents = range.extractContents();
|
|
404
|
-
inline.appendChild(contents);
|
|
405
|
-
range.insertNode(inline);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// Cursor setzen
|
|
409
|
-
range.setStartAfter(inline);
|
|
410
|
-
range.collapse(true);
|
|
411
|
-
selection.removeAllRanges();
|
|
412
|
-
selection.addRange(range);
|
|
413
|
-
|
|
414
|
-
const editor = editorRef.current;
|
|
415
|
-
if (editor) {
|
|
416
|
-
const content = domToContent(editor);
|
|
417
|
-
notifyChange(content);
|
|
418
|
-
}
|
|
419
|
-
},
|
|
420
|
-
|
|
421
|
-
undo: (): void => {
|
|
422
|
-
executeCommand("undo");
|
|
423
|
-
},
|
|
424
|
-
|
|
425
|
-
redo: (): void => {
|
|
426
|
-
executeCommand("redo");
|
|
427
|
-
},
|
|
428
|
-
|
|
429
|
-
canUndo: (): boolean => {
|
|
430
|
-
return historyRef.current.canUndo();
|
|
431
|
-
},
|
|
432
|
-
|
|
433
|
-
canRedo: (): boolean => {
|
|
434
|
-
return historyRef.current.canRedo();
|
|
435
|
-
},
|
|
436
|
-
|
|
437
|
-
importHtml: (htmlString: string): EditorContent => {
|
|
438
|
-
const content = htmlToContent(htmlString);
|
|
439
|
-
const editor = editorRef.current;
|
|
440
|
-
if (editor) {
|
|
441
|
-
isUpdatingRef.current = true;
|
|
442
|
-
contentToDOM(
|
|
443
|
-
content,
|
|
444
|
-
editor,
|
|
445
|
-
customLinkComponent,
|
|
446
|
-
customHeadingRenderer
|
|
447
|
-
);
|
|
448
|
-
historyRef.current.push(content);
|
|
449
|
-
isUpdatingRef.current = false;
|
|
450
|
-
notifyChange(content);
|
|
451
|
-
}
|
|
452
|
-
return content;
|
|
453
|
-
},
|
|
454
|
-
|
|
455
|
-
exportHtml: (): string => {
|
|
456
|
-
const editor = editorRef.current;
|
|
457
|
-
if (!editor) return "";
|
|
458
|
-
const content = domToContent(editor);
|
|
459
|
-
return contentToHTML(content);
|
|
460
|
-
},
|
|
461
|
-
|
|
462
|
-
clearFormatting: (): void => {
|
|
463
|
-
const editor = editorRef.current;
|
|
464
|
-
if (!editor) return;
|
|
465
|
-
|
|
466
|
-
const selection = window.getSelection();
|
|
467
|
-
if (selection && selection.rangeCount > 0) {
|
|
468
|
-
const currentContent = domToContent(editor);
|
|
469
|
-
historyRef.current.push(currentContent);
|
|
470
|
-
|
|
471
|
-
clearFormatting(selection);
|
|
472
|
-
|
|
473
|
-
setTimeout(() => {
|
|
474
|
-
if (editor) {
|
|
475
|
-
const content = domToContent(editor);
|
|
476
|
-
notifyChange(content);
|
|
477
|
-
}
|
|
478
|
-
}, 0);
|
|
479
|
-
}
|
|
480
|
-
},
|
|
481
|
-
|
|
482
|
-
clearTextColor: (): void => {
|
|
483
|
-
const editor = editorRef.current;
|
|
484
|
-
if (!editor) return;
|
|
485
|
-
|
|
486
|
-
const selection = window.getSelection();
|
|
487
|
-
if (selection && selection.rangeCount > 0) {
|
|
488
|
-
const currentContent = domToContent(editor);
|
|
489
|
-
historyRef.current.push(currentContent);
|
|
490
|
-
|
|
491
|
-
clearTextColor(selection);
|
|
492
|
-
|
|
493
|
-
setTimeout(() => {
|
|
494
|
-
if (editor) {
|
|
495
|
-
const content = domToContent(editor);
|
|
496
|
-
notifyChange(content);
|
|
497
|
-
}
|
|
498
|
-
}, 0);
|
|
499
|
-
}
|
|
500
|
-
},
|
|
501
|
-
|
|
502
|
-
clearBackgroundColor: (): void => {
|
|
503
|
-
const editor = editorRef.current;
|
|
504
|
-
if (!editor) return;
|
|
505
|
-
|
|
506
|
-
const selection = window.getSelection();
|
|
507
|
-
if (selection && selection.rangeCount > 0) {
|
|
508
|
-
const currentContent = domToContent(editor);
|
|
509
|
-
historyRef.current.push(currentContent);
|
|
510
|
-
|
|
511
|
-
clearBackgroundColor(selection);
|
|
512
|
-
|
|
513
|
-
setTimeout(() => {
|
|
514
|
-
if (editor) {
|
|
515
|
-
const content = domToContent(editor);
|
|
516
|
-
notifyChange(content);
|
|
517
|
-
}
|
|
518
|
-
}, 0);
|
|
519
|
-
}
|
|
520
|
-
},
|
|
521
|
-
|
|
522
|
-
clearFontSize: (): void => {
|
|
523
|
-
const editor = editorRef.current;
|
|
524
|
-
if (!editor) return;
|
|
525
|
-
|
|
526
|
-
const selection = window.getSelection();
|
|
527
|
-
if (selection && selection.rangeCount > 0) {
|
|
528
|
-
const currentContent = domToContent(editor);
|
|
529
|
-
historyRef.current.push(currentContent);
|
|
530
|
-
|
|
531
|
-
clearFontSize(selection);
|
|
532
|
-
|
|
533
|
-
setTimeout(() => {
|
|
534
|
-
if (editor) {
|
|
535
|
-
const content = domToContent(editor);
|
|
536
|
-
notifyChange(content);
|
|
537
|
-
}
|
|
538
|
-
}, 0);
|
|
539
|
-
}
|
|
540
|
-
},
|
|
541
|
-
|
|
542
|
-
clearLinks: (): void => {
|
|
543
|
-
const editor = editorRef.current;
|
|
544
|
-
if (!editor) return;
|
|
545
|
-
|
|
546
|
-
const selection = window.getSelection();
|
|
547
|
-
if (selection && selection.rangeCount > 0) {
|
|
548
|
-
const currentContent = domToContent(editor);
|
|
549
|
-
historyRef.current.push(currentContent);
|
|
550
|
-
|
|
551
|
-
clearLinks(selection);
|
|
552
|
-
|
|
553
|
-
setTimeout(() => {
|
|
554
|
-
if (editor) {
|
|
555
|
-
const content = domToContent(editor);
|
|
556
|
-
notifyChange(content);
|
|
557
|
-
}
|
|
558
|
-
}, 0);
|
|
559
|
-
}
|
|
560
|
-
},
|
|
561
|
-
|
|
562
|
-
indentListItem: (): void => {
|
|
563
|
-
const editor = editorRef.current;
|
|
564
|
-
if (!editor) return;
|
|
565
|
-
|
|
566
|
-
const selection = window.getSelection();
|
|
567
|
-
if (selection && selection.rangeCount > 0) {
|
|
568
|
-
const currentContent = domToContent(editor);
|
|
569
|
-
historyRef.current.push(currentContent);
|
|
570
|
-
|
|
571
|
-
indentListItem(selection);
|
|
572
|
-
|
|
573
|
-
setTimeout(() => {
|
|
574
|
-
if (editor) {
|
|
575
|
-
const content = domToContent(editor);
|
|
576
|
-
notifyChange(content);
|
|
577
|
-
}
|
|
578
|
-
}, 0);
|
|
579
|
-
}
|
|
580
|
-
},
|
|
581
|
-
|
|
582
|
-
outdentListItem: (): void => {
|
|
583
|
-
const editor = editorRef.current;
|
|
584
|
-
if (!editor) return;
|
|
585
|
-
|
|
586
|
-
const selection = window.getSelection();
|
|
587
|
-
if (selection && selection.rangeCount > 0) {
|
|
588
|
-
const currentContent = domToContent(editor);
|
|
589
|
-
historyRef.current.push(currentContent);
|
|
590
|
-
|
|
591
|
-
outdentListItem(selection);
|
|
592
|
-
|
|
593
|
-
setTimeout(() => {
|
|
594
|
-
if (editor) {
|
|
595
|
-
const content = domToContent(editor);
|
|
596
|
-
notifyChange(content);
|
|
597
|
-
}
|
|
598
|
-
}, 0);
|
|
599
|
-
}
|
|
600
|
-
},
|
|
601
|
-
};
|
|
602
|
-
}, [
|
|
603
|
-
notifyChange,
|
|
604
|
-
restoreSelection,
|
|
605
|
-
customLinkComponent,
|
|
606
|
-
customHeadingRenderer,
|
|
607
|
-
]);
|
|
608
|
-
|
|
609
|
-
useEffect(() => {
|
|
610
|
-
if (onEditorAPIReady) {
|
|
611
|
-
onEditorAPIReady(editorAPI);
|
|
612
|
-
}
|
|
613
|
-
}, [editorAPI, onEditorAPIReady]);
|
|
614
|
-
|
|
615
|
-
const isInitializedRef = useRef(false);
|
|
616
|
-
|
|
617
|
-
useEffect(() => {
|
|
618
|
-
const editor = editorRef.current;
|
|
619
|
-
if (!editor || isInitializedRef.current) return;
|
|
620
|
-
|
|
621
|
-
const content = initialContent || createEmptyContent();
|
|
622
|
-
isUpdatingRef.current = true;
|
|
623
|
-
contentToDOM(
|
|
624
|
-
content,
|
|
625
|
-
editor,
|
|
626
|
-
customLinkComponent,
|
|
627
|
-
customHeadingRenderer
|
|
628
|
-
);
|
|
629
|
-
historyRef.current.push(content);
|
|
630
|
-
isUpdatingRef.current = false;
|
|
631
|
-
isInitializedRef.current = true;
|
|
632
|
-
|
|
633
|
-
let inputTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
634
|
-
const handleInput = () => {
|
|
635
|
-
if (isUpdatingRef.current) return;
|
|
636
|
-
|
|
637
|
-
const content = domToContent(editor);
|
|
638
|
-
notifyChange(content);
|
|
639
|
-
|
|
640
|
-
if (inputTimeout) {
|
|
641
|
-
clearTimeout(inputTimeout);
|
|
642
|
-
}
|
|
643
|
-
inputTimeout = setTimeout(() => {
|
|
644
|
-
historyRef.current.push(content);
|
|
645
|
-
inputTimeout = null;
|
|
646
|
-
}, 300);
|
|
647
|
-
};
|
|
648
|
-
|
|
649
|
-
const handleKeyDown = (e: KeyboardEvent) => {
|
|
650
|
-
const isModifierPressed = e.metaKey || e.ctrlKey;
|
|
651
|
-
|
|
652
|
-
if (e.key === "Tab" && !isModifierPressed && !e.altKey) {
|
|
653
|
-
// Immer preventDefault aufrufen (wie Lexical), damit Tab den Fokus nicht aus dem Editor entfernt
|
|
654
|
-
e.preventDefault();
|
|
655
|
-
e.stopPropagation();
|
|
656
|
-
e.stopImmediatePropagation();
|
|
657
|
-
|
|
658
|
-
const selection = window.getSelection();
|
|
659
|
-
|
|
660
|
-
if (!selection || selection.rangeCount === 0) {
|
|
661
|
-
// Keine Selection: Tab verhindern, Fokus bleibt im Editor
|
|
662
|
-
return;
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
const range = selection.getRangeAt(0);
|
|
666
|
-
const container = range.commonAncestorContainer;
|
|
667
|
-
|
|
668
|
-
if (!editor.contains(container)) {
|
|
669
|
-
// Container nicht im Editor: Tab verhindern
|
|
670
|
-
return;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
// Prüfe ob wir in einer Liste sind
|
|
674
|
-
const listItem =
|
|
675
|
-
container.nodeType === Node.TEXT_NODE
|
|
676
|
-
? container.parentElement?.closest("li")
|
|
677
|
-
: (container as HTMLElement).closest("li");
|
|
678
|
-
|
|
679
|
-
if (listItem && editor.contains(listItem)) {
|
|
680
|
-
// In Liste: Indent/Outdent durchführen
|
|
681
|
-
const currentContent = domToContent(editor);
|
|
682
|
-
historyRef.current.push(currentContent);
|
|
683
|
-
|
|
684
|
-
if (e.shiftKey) {
|
|
685
|
-
outdentListItem(selection);
|
|
686
|
-
} else {
|
|
687
|
-
indentListItem(selection);
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
setTimeout(() => {
|
|
691
|
-
if (editor) {
|
|
692
|
-
const content = domToContent(editor);
|
|
693
|
-
notifyChange(content);
|
|
694
|
-
}
|
|
695
|
-
}, 0);
|
|
696
|
-
return;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
// Nicht in Liste: Tab verhindern, aber kein Tab-Zeichen einfügen
|
|
700
|
-
// Der Fokus bleibt im Editor (durch preventDefault)
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
if (isModifierPressed && e.key === "z" && !e.shiftKey) {
|
|
704
|
-
e.preventDefault();
|
|
705
|
-
e.stopPropagation();
|
|
706
|
-
editorAPI.undo();
|
|
707
|
-
} else if (
|
|
708
|
-
isModifierPressed &&
|
|
709
|
-
(e.key === "y" || (e.key === "z" && e.shiftKey))
|
|
710
|
-
) {
|
|
711
|
-
e.preventDefault();
|
|
712
|
-
e.stopPropagation();
|
|
713
|
-
editorAPI.redo();
|
|
714
|
-
}
|
|
715
|
-
};
|
|
716
|
-
|
|
717
|
-
editor.addEventListener("input", handleInput);
|
|
718
|
-
editor.addEventListener("keydown", handleKeyDown, true);
|
|
719
|
-
|
|
720
|
-
return () => {
|
|
721
|
-
editor.removeEventListener("input", handleInput);
|
|
722
|
-
editor.removeEventListener("keydown", handleKeyDown, true);
|
|
723
|
-
if (inputTimeout) {
|
|
724
|
-
clearTimeout(inputTimeout);
|
|
725
|
-
}
|
|
726
|
-
};
|
|
727
|
-
}, [editorAPI, notifyChange]);
|
|
728
|
-
|
|
729
|
-
const handlePaste = (e: React.ClipboardEvent) => {
|
|
730
|
-
e.preventDefault();
|
|
731
|
-
|
|
732
|
-
const html = e.clipboardData.getData("text/html");
|
|
733
|
-
const text = e.clipboardData.getData("text/plain");
|
|
734
|
-
|
|
735
|
-
if (html) {
|
|
736
|
-
try {
|
|
737
|
-
const pastedContent = htmlToContent(html);
|
|
738
|
-
const editor = editorRef.current;
|
|
739
|
-
if (!editor) return;
|
|
740
|
-
|
|
741
|
-
const selection = window.getSelection();
|
|
742
|
-
if (selection && selection.rangeCount > 0) {
|
|
743
|
-
const range = selection.getRangeAt(0);
|
|
744
|
-
|
|
745
|
-
range.deleteContents();
|
|
746
|
-
|
|
747
|
-
const tempDiv = document.createElement("div");
|
|
748
|
-
contentToDOM(
|
|
749
|
-
pastedContent,
|
|
750
|
-
tempDiv,
|
|
751
|
-
customLinkComponent,
|
|
752
|
-
customHeadingRenderer
|
|
753
|
-
);
|
|
754
|
-
|
|
755
|
-
const fragment = document.createDocumentFragment();
|
|
756
|
-
while (tempDiv.firstChild) {
|
|
757
|
-
fragment.appendChild(tempDiv.firstChild);
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
range.insertNode(fragment);
|
|
761
|
-
|
|
762
|
-
if (fragment.lastChild) {
|
|
763
|
-
range.setStartAfter(fragment.lastChild);
|
|
764
|
-
range.collapse(true);
|
|
765
|
-
}
|
|
766
|
-
selection.removeAllRanges();
|
|
767
|
-
selection.addRange(range);
|
|
768
|
-
|
|
769
|
-
const content = domToContent(editor);
|
|
770
|
-
notifyChange(content);
|
|
771
|
-
}
|
|
772
|
-
} catch (error) {
|
|
773
|
-
document.execCommand("insertText", false, text);
|
|
774
|
-
}
|
|
775
|
-
} else if (text) {
|
|
776
|
-
document.execCommand("insertText", false, text);
|
|
777
|
-
}
|
|
778
|
-
};
|
|
779
|
-
const containerStyle: React.CSSProperties = theme
|
|
780
|
-
? {
|
|
781
|
-
...(theme.borderColor &&
|
|
782
|
-
({
|
|
783
|
-
"--rte-border-color": theme.borderColor,
|
|
784
|
-
} as React.CSSProperties)),
|
|
785
|
-
...(theme.borderRadius &&
|
|
786
|
-
({
|
|
787
|
-
"--rte-border-radius": `${theme.borderRadius}px`,
|
|
788
|
-
} as React.CSSProperties)),
|
|
789
|
-
...(theme.toolbarBg &&
|
|
790
|
-
({
|
|
791
|
-
"--rte-toolbar-bg": theme.toolbarBg,
|
|
792
|
-
} as React.CSSProperties)),
|
|
793
|
-
...(theme.buttonHoverBg &&
|
|
794
|
-
({
|
|
795
|
-
"--rte-button-hover-bg": theme.buttonHoverBg,
|
|
796
|
-
} as React.CSSProperties)),
|
|
797
|
-
...(theme.contentBg &&
|
|
798
|
-
({
|
|
799
|
-
"--rte-content-bg": theme.contentBg,
|
|
800
|
-
} as React.CSSProperties)),
|
|
801
|
-
...(theme.primaryColor &&
|
|
802
|
-
({
|
|
803
|
-
"--rte-primary-color": theme.primaryColor,
|
|
804
|
-
} as React.CSSProperties)),
|
|
805
|
-
}
|
|
806
|
-
: {};
|
|
807
|
-
|
|
808
|
-
return (
|
|
809
|
-
<div
|
|
810
|
-
className={`rte-container ${className || ""}`}
|
|
811
|
-
style={containerStyle}
|
|
812
|
-
>
|
|
813
|
-
<Toolbar
|
|
814
|
-
plugins={plugins}
|
|
815
|
-
editorAPI={editorAPI}
|
|
816
|
-
className={toolbarClassName}
|
|
817
|
-
/>
|
|
818
|
-
<div
|
|
819
|
-
ref={editorRef}
|
|
820
|
-
contentEditable
|
|
821
|
-
className={`rte-editor ${editorClassName || ""}`}
|
|
822
|
-
data-placeholder={placeholder}
|
|
823
|
-
onPaste={handlePaste}
|
|
824
|
-
suppressContentEditableWarning
|
|
825
|
-
/>
|
|
826
|
-
</div>
|
|
827
|
-
);
|
|
828
|
-
};
|