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