@overlap/rte 0.1.5 → 0.1.7
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 +52 -40
- package/dist/components/Editor.d.ts.map +1 -1
- package/dist/components/Icons.d.ts +11 -0
- package/dist/components/Icons.d.ts.map +1 -1
- package/dist/constants.d.ts +9 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/hooks/useCheckbox.d.ts +23 -0
- package/dist/hooks/useCheckbox.d.ts.map +1 -0
- package/dist/hooks/useEditorEvents.d.ts +18 -0
- package/dist/hooks/useEditorEvents.d.ts.map +1 -0
- package/dist/hooks/useEditorInit.d.ts +23 -0
- package/dist/hooks/useEditorInit.d.ts.map +1 -0
- package/dist/hooks/useEditorSelection.d.ts +7 -0
- package/dist/hooks/useEditorSelection.d.ts.map +1 -0
- package/dist/index.d.ts +77 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +2355 -772
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +2366 -770
- package/dist/index.js.map +1 -1
- package/dist/plugins/alignment.d.ts +9 -0
- package/dist/plugins/alignment.d.ts.map +1 -0
- package/dist/plugins/blockFormat.d.ts +2 -2
- package/dist/plugins/blockFormat.d.ts.map +1 -1
- package/dist/plugins/index.d.ts +4 -0
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/linkDialog.d.ts +17 -0
- package/dist/plugins/linkDialog.d.ts.map +1 -0
- package/dist/plugins/table.d.ts +7 -0
- package/dist/plugins/table.d.ts.map +1 -0
- package/dist/styles.css +767 -386
- package/dist/types.d.ts +2 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/checkbox.d.ts +27 -0
- package/dist/utils/checkbox.d.ts.map +1 -0
- package/dist/utils/content.d.ts +19 -2
- package/dist/utils/content.d.ts.map +1 -1
- package/dist/utils/dom.d.ts +31 -0
- package/dist/utils/dom.d.ts.map +1 -0
- package/dist/utils/table.d.ts +30 -0
- package/dist/utils/table.d.ts.map +1 -0
- package/package.json +2 -3
- 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
package/dist/index.esm.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
2
|
-
import React, { useState, useRef, useEffect,
|
|
2
|
+
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
|
3
3
|
|
|
4
4
|
const BoldIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M15.6 10.79c.97-.67 1.65-1.77 1.65-2.79 0-2.26-1.75-4-4-4H7v14h7.04c2.09 0 3.71-1.7 3.71-3.79 0-1.52-.86-2.82-2.15-3.42zM10 6.5h3c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-3v-3zm3.5 9H10v-3h3.5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5z" }) }));
|
|
5
5
|
const ItalicIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M10 4v3h2.21l-3.42 8H6v3h8v-3h-2.21l3.42-8H18V4z" }) }));
|
|
@@ -12,8 +12,9 @@ const QuoteIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { wid
|
|
|
12
12
|
const BulletListIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5zm0 12c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zM7 19h14v-2H7v2zm0-6h14v-2H7v2zm0-8v2h14V5H7z" }) }));
|
|
13
13
|
const NumberedListIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M2 17h2v.5H3v1h1v.5H2v1h3v-4H2v1zm1-9h1V4H2v1h1v3zm-1 3h1.8L2 13.1v.9h3v-1H3.2L5 11.9V11H2zm6-5v2h14V6H8zm0 14h14v-2H8v2zm0-6h14v-2H8v2z" }) }));
|
|
14
14
|
const TextColorIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M2 20h20v4H2v-4zm3.49-3h2.42l1.27-3.58h5.64L16.09 17h2.42L13.25 3h-2.5L5.49 17zm4.22-5.61l2.03-5.79h.12l2.03 5.79H9.71z" }) }));
|
|
15
|
-
const BackgroundColorIcon = ({ width = 18, height = 18, className, }) => (jsxs("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: [jsx("path", { d: "
|
|
15
|
+
const BackgroundColorIcon = ({ width = 18, height = 18, className, }) => (jsxs("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: className, children: [jsx("path", { d: "M14.5 2.5L21.5 9.5L13 18L6 11L14.5 2.5Z" }), jsx("path", { d: "M6 11L13 18" }), jsx("rect", { x: "2", y: "16", width: "8", height: "6", fill: "currentColor", opacity: "0.6", rx: "1" }), jsx("path", { d: "M21.5 9.5L19 7" })] }));
|
|
16
16
|
const HeadingIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M5 4v3h5.5v12h3V7H19V4H5z" }) }));
|
|
17
|
+
const FormatIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M3 4h18v2H3V4zm0 4h12v2H3V8zm0 4h18v2H3v-2zm0 4h12v2H3v-2z" }) }));
|
|
17
18
|
const FontSizeIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M9 4v3h5v12h3V7h5V4H9zm-6 8h3v8h3v-8h3V10H3z" }) }));
|
|
18
19
|
const ImageIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z" }) }));
|
|
19
20
|
const CloseIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" }) }));
|
|
@@ -21,6 +22,16 @@ const LoadingIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { w
|
|
|
21
22
|
const UploadIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M9 16h6v-6h4l-7-7-7 7h4zm-4 2h14v2H5z" }) }));
|
|
22
23
|
const IndentIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M3 21h18v-2H3v2zM3 8l4 4-4 4V8zm8 9h10v-2H11v2zM3 3v2h18V3H3zm8 6h10V7H11v2zm0 4h10v-2H11v2z" }) }));
|
|
23
24
|
const OutdentIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M3 21h18v-2H3v2zM11 8l4 4-4 4V8zM3 3v2h18V3H3zm0 4h10v2H3V7zm0 4h10v2H3v-2zm0 4h18v2H3v-2z" }) }));
|
|
25
|
+
const CheckboxIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M19 3H5c-1.11 0-2 .89-2 2v14c0 1.11.89 2 2 2h14c1.11 0 2-.89 2-2V5c0-1.11-.89-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" }) }));
|
|
26
|
+
const StrikethroughIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M10 19h4v-3h-4v3zM5 4v3h5v3h4V7h5V4H5zM3 14h18v-2H3v2z" }) }));
|
|
27
|
+
const SubscriptIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M22 18h-2v1h3v1h-4v-2.5c0-.83.67-1.5 1.5-1.5h1.5v-1h-3v-1h2.5c.83 0 1.5.67 1.5 1.5v1c0 .83-.67 1.5-1.5 1.5zM5.88 18h2.66l3.4-5.42h.12l3.4 5.42h2.66l-4.65-7.27L17.81 4h-2.68l-3.07 4.99h-.12L8.87 4H6.19l4.32 6.73L5.88 18z" }) }));
|
|
28
|
+
const SuperscriptIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M22 7h-2v1h3v1h-4V6.5c0-.83.67-1.5 1.5-1.5h1.5V4h-3V3h2.5c.83 0 1.5.67 1.5 1.5v1c0 .83-.67 1.5-1.5 1.5zM5.88 20h2.66l3.4-5.42h.12l3.4 5.42h2.66l-4.65-7.27L17.81 6h-2.68l-3.07 4.99h-.12L8.87 6H6.19l4.32 6.73L5.88 20z" }) }));
|
|
29
|
+
const CodeIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z" }) }));
|
|
30
|
+
const AlignLeftIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M15 15H3v2h12v-2zm0-8H3v2h12V7zM3 13h18v-2H3v2zm0 8h18v-2H3v2zM3 3v2h18V3H3z" }) }));
|
|
31
|
+
const AlignCenterIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M7 15v2h10v-2H7zm-4 6h18v-2H3v2zm0-8h18v-2H3v2zm4-6v2h10V7H7zM3 3v2h18V3H3z" }) }));
|
|
32
|
+
const AlignRightIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M3 21h18v-2H3v2zm6-4h12v-2H9v2zm-6-4h18v-2H3v2zm6-4h12V7H9v2zM3 3v2h18V3H3z" }) }));
|
|
33
|
+
const AlignJustifyIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M3 21h18v-2H3v2zm0-4h18v-2H3v2zm0-4h18v-2H3v2zm0-4h18V7H3v2zm0-6v2h18V3H3z" }) }));
|
|
34
|
+
const TableIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M3 3v18h18V3H3zm8 16H5v-6h6v6zm0-8H5V5h6v6zm8 8h-6v-6h6v6zm0-8h-6V5h6v6z" }) }));
|
|
24
35
|
const iconMap = {
|
|
25
36
|
"mdi:format-bold": BoldIcon,
|
|
26
37
|
"mdi:format-italic": ItalicIcon,
|
|
@@ -35,6 +46,7 @@ const iconMap = {
|
|
|
35
46
|
"mdi:format-color-text": TextColorIcon,
|
|
36
47
|
"mdi:format-color-fill": BackgroundColorIcon,
|
|
37
48
|
"mdi:format-header-1": HeadingIcon,
|
|
49
|
+
"mdi:format-paragraph": FormatIcon,
|
|
38
50
|
"mdi:format-size": FontSizeIcon,
|
|
39
51
|
"mdi:image": ImageIcon,
|
|
40
52
|
"mdi:close": CloseIcon,
|
|
@@ -42,6 +54,16 @@ const iconMap = {
|
|
|
42
54
|
"mdi:upload": UploadIcon,
|
|
43
55
|
"mdi:format-indent-increase": IndentIcon,
|
|
44
56
|
"mdi:format-indent-decrease": OutdentIcon,
|
|
57
|
+
"mdi:checkbox-marked-outline": CheckboxIcon,
|
|
58
|
+
"mdi:format-strikethrough": StrikethroughIcon,
|
|
59
|
+
"mdi:format-subscript": SubscriptIcon,
|
|
60
|
+
"mdi:format-superscript": SuperscriptIcon,
|
|
61
|
+
"mdi:code-tags": CodeIcon,
|
|
62
|
+
"mdi:format-align-left": AlignLeftIcon,
|
|
63
|
+
"mdi:format-align-center": AlignCenterIcon,
|
|
64
|
+
"mdi:format-align-right": AlignRightIcon,
|
|
65
|
+
"mdi:format-align-justify": AlignJustifyIcon,
|
|
66
|
+
"mdi:table": TableIcon,
|
|
45
67
|
};
|
|
46
68
|
const Icon = ({ icon, width = 18, height = 18, className }) => {
|
|
47
69
|
const IconComponent = iconMap[icon];
|
|
@@ -135,7 +157,91 @@ function createCommandPlugin(name, command, icon, label) {
|
|
|
135
157
|
};
|
|
136
158
|
}
|
|
137
159
|
|
|
138
|
-
|
|
160
|
+
/**
|
|
161
|
+
* Pure DOM utility functions.
|
|
162
|
+
* No React dependencies - only native browser APIs.
|
|
163
|
+
*/
|
|
164
|
+
/**
|
|
165
|
+
* Checks if a UL element is a checkbox list.
|
|
166
|
+
* Detects: own format, Lexical format, and GitHub format.
|
|
167
|
+
*/
|
|
168
|
+
function isCheckboxList(element) {
|
|
169
|
+
if (element.tagName !== "UL")
|
|
170
|
+
return false;
|
|
171
|
+
// Own format
|
|
172
|
+
if (element.classList.contains("rte-checkbox-list"))
|
|
173
|
+
return true;
|
|
174
|
+
// Lexical format (attribute)
|
|
175
|
+
if (element.getAttribute("__lexicallisttype") === "check")
|
|
176
|
+
return true;
|
|
177
|
+
// Lexical theme class (any class containing "checklist")
|
|
178
|
+
const hasChecklistClass = Array.from(element.classList).some((cls) => cls.toLowerCase().includes("checklist"));
|
|
179
|
+
if (hasChecklistClass)
|
|
180
|
+
return true;
|
|
181
|
+
// GitHub format
|
|
182
|
+
if (element.classList.contains("contains-task-list"))
|
|
183
|
+
return true;
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Finds the closest checkbox list ancestor from an element.
|
|
188
|
+
* Works with all supported formats (own, Lexical, GitHub).
|
|
189
|
+
*/
|
|
190
|
+
function findClosestCheckboxList(element) {
|
|
191
|
+
let current = element;
|
|
192
|
+
while (current) {
|
|
193
|
+
if (current.tagName === "UL" && isCheckboxList(current)) {
|
|
194
|
+
return current;
|
|
195
|
+
}
|
|
196
|
+
current = current.parentElement;
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Checks if a list item has a nested list as its first child (not a leaf item).
|
|
202
|
+
*/
|
|
203
|
+
function isNestedListItem(li) {
|
|
204
|
+
const firstChild = li.firstChild;
|
|
205
|
+
return (firstChild instanceof HTMLElement &&
|
|
206
|
+
(firstChild.tagName === "UL" || firstChild.tagName === "OL"));
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Finds the closest list item from a node.
|
|
210
|
+
*/
|
|
211
|
+
function findClosestListItem(node) {
|
|
212
|
+
const element = node.nodeType === Node.TEXT_NODE
|
|
213
|
+
? node.parentElement
|
|
214
|
+
: node;
|
|
215
|
+
return element?.closest("li") || null;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Sets cursor position in a text node after an async DOM update.
|
|
219
|
+
*/
|
|
220
|
+
function setCursorInTextNode(textNode, position, editor) {
|
|
221
|
+
requestAnimationFrame(() => {
|
|
222
|
+
requestAnimationFrame(() => {
|
|
223
|
+
try {
|
|
224
|
+
const range = document.createRange();
|
|
225
|
+
const maxPos = textNode.textContent?.length || 0;
|
|
226
|
+
const safePos = Math.min(Math.max(0, position), maxPos);
|
|
227
|
+
range.setStart(textNode, safePos);
|
|
228
|
+
range.collapse(true);
|
|
229
|
+
const selection = window.getSelection();
|
|
230
|
+
if (selection) {
|
|
231
|
+
selection.removeAllRanges();
|
|
232
|
+
selection.addRange(range);
|
|
233
|
+
if (editor)
|
|
234
|
+
editor.focus();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
catch (_) {
|
|
238
|
+
// Silently fail - cursor positioning is best-effort
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const defaultHeadings$2 = ["h1", "h2", "h3", "h4", "h5", "h6"];
|
|
139
245
|
const headingLabels$1 = {
|
|
140
246
|
h1: "Überschrift 1",
|
|
141
247
|
h2: "Überschrift 2",
|
|
@@ -145,8 +251,8 @@ const headingLabels$1 = {
|
|
|
145
251
|
h6: "Überschrift 6",
|
|
146
252
|
};
|
|
147
253
|
/**
|
|
148
|
-
*
|
|
149
|
-
* @param headings - Array
|
|
254
|
+
* Creates a Block Format plugin that combines headings, lists, and quote in a dropdown.
|
|
255
|
+
* @param headings - Array of heading levels (e.g. ["h1", "h2", "h3"])
|
|
150
256
|
*/
|
|
151
257
|
function createBlockFormatPlugin(headings = defaultHeadings$2) {
|
|
152
258
|
const options = [
|
|
@@ -166,16 +272,64 @@ function createBlockFormatPlugin(headings = defaultHeadings$2) {
|
|
|
166
272
|
label: "Nummerierte Liste",
|
|
167
273
|
icon: "mdi:format-list-numbered",
|
|
168
274
|
},
|
|
169
|
-
{
|
|
275
|
+
{
|
|
276
|
+
value: "checkbox-list",
|
|
277
|
+
label: "Checkbox-Liste",
|
|
278
|
+
icon: "mdi:checkbox-marked-outline",
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
value: "blockquote",
|
|
282
|
+
label: "Zitat",
|
|
283
|
+
icon: "mdi:format-quote-close",
|
|
284
|
+
},
|
|
170
285
|
];
|
|
286
|
+
/** Detects the current block format at the cursor position. */
|
|
287
|
+
function detectCurrentFormat(editor) {
|
|
288
|
+
const selection = editor.getSelection();
|
|
289
|
+
if (!selection || selection.rangeCount === 0)
|
|
290
|
+
return undefined;
|
|
291
|
+
const range = selection.getRangeAt(0);
|
|
292
|
+
const container = range.commonAncestorContainer;
|
|
293
|
+
const element = container.nodeType === Node.TEXT_NODE
|
|
294
|
+
? container.parentElement
|
|
295
|
+
: container;
|
|
296
|
+
if (!element)
|
|
297
|
+
return undefined;
|
|
298
|
+
const tagName = element.tagName.toLowerCase();
|
|
299
|
+
if (headings.includes(tagName))
|
|
300
|
+
return tagName;
|
|
301
|
+
if (element.closest("blockquote"))
|
|
302
|
+
return "blockquote";
|
|
303
|
+
if (findClosestCheckboxList(element))
|
|
304
|
+
return "checkbox-list";
|
|
305
|
+
if (element.closest("ul"))
|
|
306
|
+
return "ul";
|
|
307
|
+
if (element.closest("ol"))
|
|
308
|
+
return "ol";
|
|
309
|
+
if (tagName === "p")
|
|
310
|
+
return "p";
|
|
311
|
+
return undefined;
|
|
312
|
+
}
|
|
171
313
|
return {
|
|
172
314
|
name: "blockFormat",
|
|
173
315
|
type: "block",
|
|
174
316
|
renderButton: (props) => {
|
|
175
|
-
// Aktuelles Format bestimmen
|
|
176
317
|
const editor = props.editorAPI;
|
|
318
|
+
const onSelect = props.onSelect;
|
|
177
319
|
let currentValue = props.currentValue;
|
|
178
320
|
if (!currentValue && editor) {
|
|
321
|
+
currentValue = detectCurrentFormat(editor);
|
|
322
|
+
}
|
|
323
|
+
return (jsx(Dropdown, { icon: "mdi:format-paragraph", label: "Format", options: options, onSelect: (value) => {
|
|
324
|
+
if (onSelect)
|
|
325
|
+
onSelect(value);
|
|
326
|
+
}, currentValue: currentValue, disabled: props.disabled }));
|
|
327
|
+
},
|
|
328
|
+
getCurrentValue: (editor) => detectCurrentFormat(editor),
|
|
329
|
+
execute: (editor, value) => {
|
|
330
|
+
if (!value)
|
|
331
|
+
return;
|
|
332
|
+
if (value === "checkbox-list") {
|
|
179
333
|
const selection = editor.getSelection();
|
|
180
334
|
if (selection && selection.rangeCount > 0) {
|
|
181
335
|
const range = selection.getRangeAt(0);
|
|
@@ -183,74 +337,26 @@ function createBlockFormatPlugin(headings = defaultHeadings$2) {
|
|
|
183
337
|
const element = container.nodeType === Node.TEXT_NODE
|
|
184
338
|
? container.parentElement
|
|
185
339
|
: container;
|
|
186
|
-
if (element)
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
else if (element.closest("ol")) {
|
|
201
|
-
currentValue = "ol";
|
|
202
|
-
}
|
|
203
|
-
// Prüfe auf Paragraph
|
|
204
|
-
else if (tagName === "p") {
|
|
205
|
-
currentValue = "p";
|
|
206
|
-
}
|
|
340
|
+
if (!element)
|
|
341
|
+
return;
|
|
342
|
+
const checkboxList = findClosestCheckboxList(element);
|
|
343
|
+
if (checkboxList) {
|
|
344
|
+
// Remove checkbox list: convert to normal list
|
|
345
|
+
checkboxList.classList.remove("rte-checkbox-list");
|
|
346
|
+
checkboxList
|
|
347
|
+
.querySelectorAll("li[role='checkbox']")
|
|
348
|
+
.forEach((li) => {
|
|
349
|
+
li.removeAttribute("role");
|
|
350
|
+
li.removeAttribute("tabIndex");
|
|
351
|
+
li.removeAttribute("aria-checked");
|
|
352
|
+
});
|
|
207
353
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
return (jsx(Dropdown, { icon: "mdi:format-header-1", label: "Format", options: options, onSelect: (value) => {
|
|
211
|
-
// onSelect wird von der Toolbar übergeben und ruft handlePluginClick auf
|
|
212
|
-
if (props.onSelect) {
|
|
213
|
-
props.onSelect(value);
|
|
354
|
+
else {
|
|
355
|
+
editor.executeCommand("insertCheckboxList");
|
|
214
356
|
}
|
|
215
|
-
}
|
|
216
|
-
},
|
|
217
|
-
getCurrentValue: (editor) => {
|
|
218
|
-
const selection = editor.getSelection();
|
|
219
|
-
if (!selection || selection.rangeCount === 0)
|
|
220
|
-
return undefined;
|
|
221
|
-
const range = selection.getRangeAt(0);
|
|
222
|
-
const container = range.commonAncestorContainer;
|
|
223
|
-
const element = container.nodeType === Node.TEXT_NODE
|
|
224
|
-
? container.parentElement
|
|
225
|
-
: container;
|
|
226
|
-
if (!element)
|
|
227
|
-
return undefined;
|
|
228
|
-
const tagName = element.tagName.toLowerCase();
|
|
229
|
-
// Prüfe auf Heading
|
|
230
|
-
if (headings.includes(tagName)) {
|
|
231
|
-
return tagName;
|
|
232
|
-
}
|
|
233
|
-
// Prüfe auf Blockquote
|
|
234
|
-
if (element.closest("blockquote")) {
|
|
235
|
-
return "blockquote";
|
|
236
|
-
}
|
|
237
|
-
// Prüfe auf Liste
|
|
238
|
-
if (element.closest("ul")) {
|
|
239
|
-
return "ul";
|
|
240
|
-
}
|
|
241
|
-
if (element.closest("ol")) {
|
|
242
|
-
return "ol";
|
|
243
|
-
}
|
|
244
|
-
// Prüfe auf Paragraph
|
|
245
|
-
if (tagName === "p") {
|
|
246
|
-
return "p";
|
|
357
|
+
}
|
|
247
358
|
}
|
|
248
|
-
|
|
249
|
-
},
|
|
250
|
-
execute: (editor, value) => {
|
|
251
|
-
if (!value)
|
|
252
|
-
return;
|
|
253
|
-
if (value === "ul") {
|
|
359
|
+
else if (value === "ul") {
|
|
254
360
|
editor.executeCommand("insertUnorderedList");
|
|
255
361
|
}
|
|
256
362
|
else if (value === "ol") {
|
|
@@ -290,6 +396,7 @@ function createBlockFormatPlugin(headings = defaultHeadings$2) {
|
|
|
290
396
|
const tagName = element.tagName.toLowerCase();
|
|
291
397
|
return (headings.includes(tagName) ||
|
|
292
398
|
element.closest("blockquote") !== null ||
|
|
399
|
+
findClosestCheckboxList(element) !== null ||
|
|
293
400
|
element.closest("ul") !== null ||
|
|
294
401
|
element.closest("ol") !== null);
|
|
295
402
|
},
|
|
@@ -313,13 +420,69 @@ const clearFormattingPlugin = {
|
|
|
313
420
|
},
|
|
314
421
|
};
|
|
315
422
|
|
|
316
|
-
const defaultHeadings$1 = ["h1", "h2", "h3"];
|
|
423
|
+
const defaultHeadings$1 = ["h1", "h2", "h3", "h4", "h5", "h6"];
|
|
317
424
|
/**
|
|
318
425
|
* Standard-Plugins
|
|
319
426
|
*/
|
|
320
427
|
const boldPlugin = createInlinePlugin("bold", "bold", "mdi:format-bold", "Fett");
|
|
321
428
|
const italicPlugin = createInlinePlugin("italic", "italic", "mdi:format-italic", "Kursiv");
|
|
322
429
|
const underlinePlugin = createInlinePlugin("underline", "underline", "mdi:format-underline", "Unterstrichen");
|
|
430
|
+
const strikethroughPlugin = createInlinePlugin("strikethrough", "strikeThrough", "mdi:format-strikethrough", "Durchgestrichen");
|
|
431
|
+
const subscriptPlugin = createInlinePlugin("subscript", "subscript", "mdi:format-subscript", "Tiefgestellt");
|
|
432
|
+
const superscriptPlugin = createInlinePlugin("superscript", "superscript", "mdi:format-superscript", "Hochgestellt");
|
|
433
|
+
const codeInlinePlugin = {
|
|
434
|
+
name: "codeInline",
|
|
435
|
+
type: "inline",
|
|
436
|
+
renderButton: (props) => (jsx("button", { type: "button", onClick: props.onClick, disabled: props.disabled, className: `rte-toolbar-button ${props.isActive ? "rte-toolbar-button-active" : ""}`, title: "Code", "aria-label": "Code", children: jsx(IconWrapper, { icon: "mdi:code-tags", width: 18, height: 18 }) })),
|
|
437
|
+
execute: (editor) => {
|
|
438
|
+
const selection = editor.getSelection();
|
|
439
|
+
if (!selection || selection.rangeCount === 0)
|
|
440
|
+
return;
|
|
441
|
+
const range = selection.getRangeAt(0);
|
|
442
|
+
const container = range.commonAncestorContainer;
|
|
443
|
+
const element = container.nodeType === Node.TEXT_NODE
|
|
444
|
+
? container.parentElement
|
|
445
|
+
: container;
|
|
446
|
+
const existingCode = element?.closest("code");
|
|
447
|
+
if (existingCode) {
|
|
448
|
+
// Unwrap code
|
|
449
|
+
const parent = existingCode.parentNode;
|
|
450
|
+
if (parent) {
|
|
451
|
+
while (existingCode.firstChild) {
|
|
452
|
+
parent.insertBefore(existingCode.firstChild, existingCode);
|
|
453
|
+
}
|
|
454
|
+
parent.removeChild(existingCode);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
else if (!range.collapsed) {
|
|
458
|
+
// Wrap in code
|
|
459
|
+
const code = document.createElement("code");
|
|
460
|
+
try {
|
|
461
|
+
range.surroundContents(code);
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
// If surroundContents fails (partial selection), use extractContents
|
|
465
|
+
const fragment = range.extractContents();
|
|
466
|
+
code.appendChild(fragment);
|
|
467
|
+
range.insertNode(code);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
isActive: () => {
|
|
472
|
+
if (typeof document === "undefined")
|
|
473
|
+
return false;
|
|
474
|
+
const selection = document.getSelection();
|
|
475
|
+
if (!selection || selection.rangeCount === 0)
|
|
476
|
+
return false;
|
|
477
|
+
const range = selection.getRangeAt(0);
|
|
478
|
+
const container = range.commonAncestorContainer;
|
|
479
|
+
const element = container.nodeType === Node.TEXT_NODE
|
|
480
|
+
? container.parentElement
|
|
481
|
+
: container;
|
|
482
|
+
return element?.closest("code") !== null;
|
|
483
|
+
},
|
|
484
|
+
canExecute: () => true,
|
|
485
|
+
};
|
|
323
486
|
const undoPlugin = createCommandPlugin("undo", "undo", "mdi:undo", "Rückgängig");
|
|
324
487
|
const redoPlugin = createCommandPlugin("redo", "redo", "mdi:redo", "Wiederholen");
|
|
325
488
|
/**
|
|
@@ -387,10 +550,10 @@ const defaultBlockFormatPlugin = createBlockFormatPlugin(defaultHeadings$1);
|
|
|
387
550
|
const defaultPlugins = [
|
|
388
551
|
undoPlugin,
|
|
389
552
|
redoPlugin,
|
|
553
|
+
defaultBlockFormatPlugin,
|
|
390
554
|
boldPlugin,
|
|
391
555
|
italicPlugin,
|
|
392
556
|
underlinePlugin,
|
|
393
|
-
defaultBlockFormatPlugin,
|
|
394
557
|
clearFormattingPlugin,
|
|
395
558
|
_indentPlugin,
|
|
396
559
|
_outdentPlugin,
|
|
@@ -779,6 +942,138 @@ function createImagePlugin(onImageUpload) {
|
|
|
779
942
|
};
|
|
780
943
|
}
|
|
781
944
|
|
|
945
|
+
/**
|
|
946
|
+
* Pure checkbox utility functions for checkbox lists.
|
|
947
|
+
* Supports detection of own format, Lexical format, and GitHub format.
|
|
948
|
+
* No React dependencies.
|
|
949
|
+
*/
|
|
950
|
+
/**
|
|
951
|
+
* Updates a list item's checkbox attributes (role, aria-checked, tabindex).
|
|
952
|
+
*/
|
|
953
|
+
function updateListItemChecked(li, checked) {
|
|
954
|
+
const parent = li.parentElement;
|
|
955
|
+
if (!parent ||
|
|
956
|
+
parent.tagName !== "UL" ||
|
|
957
|
+
!isCheckboxList(parent) ||
|
|
958
|
+
isNestedListItem(li)) {
|
|
959
|
+
li.removeAttribute("role");
|
|
960
|
+
li.removeAttribute("tabIndex");
|
|
961
|
+
li.removeAttribute("aria-checked");
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
li.setAttribute("role", "checkbox");
|
|
965
|
+
li.setAttribute("tabIndex", "-1");
|
|
966
|
+
li.setAttribute("aria-checked", checked ? "true" : "false");
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Toggles the checked state of a checkbox list item.
|
|
970
|
+
*/
|
|
971
|
+
function toggleListItemChecked(li) {
|
|
972
|
+
const currentChecked = li.getAttribute("aria-checked") === "true";
|
|
973
|
+
li.setAttribute("aria-checked", currentChecked ? "false" : "true");
|
|
974
|
+
if (li.getAttribute("role") !== "checkbox") {
|
|
975
|
+
li.setAttribute("role", "checkbox");
|
|
976
|
+
li.setAttribute("tabIndex", "-1");
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Ensures all checkbox list items in the editor have correct attributes.
|
|
981
|
+
* Normalizes foreign formats (Lexical, GitHub) to internal format.
|
|
982
|
+
*/
|
|
983
|
+
function ensureAllCheckboxes(editor) {
|
|
984
|
+
const allULs = editor.querySelectorAll("ul");
|
|
985
|
+
allULs.forEach((ul) => {
|
|
986
|
+
if (!isCheckboxList(ul))
|
|
987
|
+
return;
|
|
988
|
+
// Normalize: ensure our class is always present
|
|
989
|
+
if (!ul.classList.contains("rte-checkbox-list")) {
|
|
990
|
+
ul.classList.add("rte-checkbox-list");
|
|
991
|
+
}
|
|
992
|
+
// Handle GitHub format: convert <input type="checkbox"> to aria-checked
|
|
993
|
+
ul.querySelectorAll(":scope > li").forEach((li) => {
|
|
994
|
+
const input = li.querySelector('input[type="checkbox"]');
|
|
995
|
+
if (input) {
|
|
996
|
+
const checked = input.checked || input.hasAttribute("checked");
|
|
997
|
+
if (li.getAttribute("aria-checked") === null) {
|
|
998
|
+
li.setAttribute("aria-checked", checked ? "true" : "false");
|
|
999
|
+
}
|
|
1000
|
+
input.remove();
|
|
1001
|
+
}
|
|
1002
|
+
});
|
|
1003
|
+
const listItems = ul.querySelectorAll(":scope > li");
|
|
1004
|
+
listItems.forEach((li) => {
|
|
1005
|
+
const htmlLi = li;
|
|
1006
|
+
if (!isNestedListItem(htmlLi)) {
|
|
1007
|
+
const ariaChecked = htmlLi.getAttribute("aria-checked");
|
|
1008
|
+
const checked = ariaChecked === "true";
|
|
1009
|
+
updateListItemChecked(htmlLi, checked);
|
|
1010
|
+
}
|
|
1011
|
+
else {
|
|
1012
|
+
htmlLi.removeAttribute("role");
|
|
1013
|
+
htmlLi.removeAttribute("tabIndex");
|
|
1014
|
+
htmlLi.removeAttribute("aria-checked");
|
|
1015
|
+
}
|
|
1016
|
+
});
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Returns the currently focused checkbox list item, or null.
|
|
1021
|
+
*/
|
|
1022
|
+
function getActiveCheckListItem() {
|
|
1023
|
+
const activeElement = document.activeElement;
|
|
1024
|
+
if (activeElement &&
|
|
1025
|
+
activeElement.tagName === "LI" &&
|
|
1026
|
+
activeElement.parentElement &&
|
|
1027
|
+
isCheckboxList(activeElement.parentElement)) {
|
|
1028
|
+
return activeElement;
|
|
1029
|
+
}
|
|
1030
|
+
return null;
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* Finds the next or previous checkbox list item sibling.
|
|
1034
|
+
*/
|
|
1035
|
+
function findCheckListItemSibling(li, backward) {
|
|
1036
|
+
let sibling = backward
|
|
1037
|
+
? li.previousElementSibling
|
|
1038
|
+
: li.nextElementSibling;
|
|
1039
|
+
let parent = li;
|
|
1040
|
+
// Walk up the tree to find a non-null sibling
|
|
1041
|
+
while (sibling == null && parent) {
|
|
1042
|
+
const parentList = parent.parentElement;
|
|
1043
|
+
if (parentList && parentList.tagName === "UL") {
|
|
1044
|
+
const grandParent = parentList.parentElement;
|
|
1045
|
+
if (grandParent && grandParent.tagName === "LI") {
|
|
1046
|
+
parent = grandParent;
|
|
1047
|
+
sibling = backward
|
|
1048
|
+
? parent.previousElementSibling
|
|
1049
|
+
: parent.nextElementSibling;
|
|
1050
|
+
}
|
|
1051
|
+
else {
|
|
1052
|
+
break;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
else {
|
|
1056
|
+
break;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
// Walk down the tree to find the first non-nested list item
|
|
1060
|
+
while (sibling && sibling.tagName === "LI") {
|
|
1061
|
+
const child = backward
|
|
1062
|
+
? sibling.lastElementChild
|
|
1063
|
+
: sibling.firstElementChild;
|
|
1064
|
+
if (child &&
|
|
1065
|
+
(child.tagName === "UL" || child.tagName === "OL")) {
|
|
1066
|
+
sibling = backward
|
|
1067
|
+
? child.lastElementChild
|
|
1068
|
+
: child.firstElementChild;
|
|
1069
|
+
}
|
|
1070
|
+
else {
|
|
1071
|
+
return sibling;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
return null;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
782
1077
|
/**
|
|
783
1078
|
* Entfernt alle Formatierungen aus der aktuellen Selection
|
|
784
1079
|
*/
|
|
@@ -972,143 +1267,301 @@ function clearLinks(selection) {
|
|
|
972
1267
|
});
|
|
973
1268
|
}
|
|
974
1269
|
|
|
1270
|
+
/**
|
|
1271
|
+
* Converts a DOM element (editor root) to EditorContent JSON.
|
|
1272
|
+
* Supports own format, Lexical HTML, and GitHub HTML.
|
|
1273
|
+
*/
|
|
975
1274
|
function domToContent(element) {
|
|
976
1275
|
const blocks = [];
|
|
1276
|
+
// Normalize all checkbox lists before conversion
|
|
1277
|
+
ensureAllCheckboxes(element);
|
|
977
1278
|
function processNode(node) {
|
|
978
1279
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
979
1280
|
const text = node.textContent;
|
|
980
1281
|
if (text === null || text === undefined)
|
|
981
1282
|
return null;
|
|
1283
|
+
// In checkbox lists, skip whitespace-only text nodes
|
|
1284
|
+
// if the LI has other meaningful text
|
|
1285
|
+
const isOnlyWhitespace = /^[\s\u200B]*$/.test(text);
|
|
1286
|
+
const isInCheckboxList = node.parentElement?.closest("ul.rte-checkbox-list") !== null;
|
|
1287
|
+
if (isOnlyWhitespace && isInCheckboxList) {
|
|
1288
|
+
const li = node.parentElement?.closest("li");
|
|
1289
|
+
if (li) {
|
|
1290
|
+
const allText = li.textContent || "";
|
|
1291
|
+
const textWithoutWhitespace = allText.replace(/[\s\u200B]/g, "");
|
|
1292
|
+
if (textWithoutWhitespace.length > 0) {
|
|
1293
|
+
return null;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
982
1297
|
return { type: "text", text };
|
|
983
1298
|
}
|
|
984
|
-
if (node.nodeType
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
if (
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
"
|
|
1008
|
-
|
|
1009
|
-
"
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1299
|
+
if (node.nodeType !== Node.ELEMENT_NODE) {
|
|
1300
|
+
return null;
|
|
1301
|
+
}
|
|
1302
|
+
const el = node;
|
|
1303
|
+
const tagName = el.tagName.toLowerCase();
|
|
1304
|
+
// Skip checkbox input elements (GitHub format remnants)
|
|
1305
|
+
if (tagName === "input" && el.getAttribute("type") === "checkbox") {
|
|
1306
|
+
return null;
|
|
1307
|
+
}
|
|
1308
|
+
// Handle <br> as empty text
|
|
1309
|
+
if (tagName === "br") {
|
|
1310
|
+
return null;
|
|
1311
|
+
}
|
|
1312
|
+
if (tagName === "img") {
|
|
1313
|
+
const attributes = {};
|
|
1314
|
+
const src = el.getAttribute("src");
|
|
1315
|
+
const alt = el.getAttribute("alt");
|
|
1316
|
+
const attachmentId = el.getAttribute("data-attachment-id");
|
|
1317
|
+
if (src)
|
|
1318
|
+
attributes.src = src;
|
|
1319
|
+
if (alt)
|
|
1320
|
+
attributes.alt = alt;
|
|
1321
|
+
if (attachmentId)
|
|
1322
|
+
attributes["data-attachment-id"] = attachmentId;
|
|
1323
|
+
return {
|
|
1324
|
+
type: "image",
|
|
1325
|
+
attributes: Object.keys(attributes).length > 0
|
|
1326
|
+
? attributes
|
|
1327
|
+
: undefined,
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
// Table elements
|
|
1331
|
+
if (["table", "thead", "tbody", "tr", "td", "th"].includes(tagName)) {
|
|
1332
|
+
const children = [];
|
|
1333
|
+
const attributes = {};
|
|
1334
|
+
// Preserve class
|
|
1335
|
+
const cls = el.getAttribute("class");
|
|
1336
|
+
if (cls)
|
|
1337
|
+
attributes.class = cls;
|
|
1338
|
+
// Table cell attributes
|
|
1339
|
+
if (tagName === "td" || tagName === "th") {
|
|
1340
|
+
const colspan = el.getAttribute("colspan");
|
|
1341
|
+
const rowspan = el.getAttribute("rowspan");
|
|
1342
|
+
if (colspan && colspan !== "1")
|
|
1343
|
+
attributes.colspan = colspan;
|
|
1344
|
+
if (rowspan && rowspan !== "1")
|
|
1345
|
+
attributes.rowspan = rowspan;
|
|
1346
|
+
// Preserve text-align on cells
|
|
1347
|
+
const textAlign = el.style.textAlign;
|
|
1348
|
+
if (textAlign && textAlign !== "left" && textAlign !== "start") {
|
|
1349
|
+
attributes.textAlign = textAlign;
|
|
1027
1350
|
}
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
attributes
|
|
1032
|
-
? attributes
|
|
1033
|
-
: undefined,
|
|
1034
|
-
};
|
|
1351
|
+
// Preserve background-color
|
|
1352
|
+
const bgColor = el.style.backgroundColor;
|
|
1353
|
+
if (bgColor)
|
|
1354
|
+
attributes.backgroundColor = bgColor;
|
|
1035
1355
|
}
|
|
1036
|
-
|
|
1037
|
-
const
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
attributes
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1356
|
+
Array.from(el.childNodes).forEach((child) => {
|
|
1357
|
+
const processed = processNode(child);
|
|
1358
|
+
if (processed)
|
|
1359
|
+
children.push(processed);
|
|
1360
|
+
});
|
|
1361
|
+
return {
|
|
1362
|
+
type: tagName,
|
|
1363
|
+
children: children.length > 0 ? children : [],
|
|
1364
|
+
attributes: Object.keys(attributes).length > 0
|
|
1365
|
+
? attributes
|
|
1366
|
+
: undefined,
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
// Block elements
|
|
1370
|
+
if ([
|
|
1371
|
+
"p",
|
|
1372
|
+
"div",
|
|
1373
|
+
"h1",
|
|
1374
|
+
"h2",
|
|
1375
|
+
"h3",
|
|
1376
|
+
"h4",
|
|
1377
|
+
"h5",
|
|
1378
|
+
"h6",
|
|
1379
|
+
"blockquote",
|
|
1380
|
+
"ul",
|
|
1381
|
+
"ol",
|
|
1382
|
+
"li",
|
|
1383
|
+
].includes(tagName)) {
|
|
1384
|
+
const children = [];
|
|
1385
|
+
const attributes = {};
|
|
1386
|
+
// Preserve text-align on block elements
|
|
1387
|
+
const textAlign = el.style.textAlign;
|
|
1388
|
+
if (textAlign && textAlign !== "left" && textAlign !== "start" && textAlign !== "") {
|
|
1389
|
+
attributes.textAlign = textAlign;
|
|
1390
|
+
}
|
|
1391
|
+
// Detect checkbox lists (own + Lexical + GitHub formats)
|
|
1392
|
+
if (tagName === "ul" && isCheckboxList(el)) {
|
|
1393
|
+
attributes.class = "rte-checkbox-list";
|
|
1394
|
+
}
|
|
1395
|
+
// Detect checkbox list items via aria-checked
|
|
1396
|
+
if (tagName === "li") {
|
|
1397
|
+
const parentUl = el.closest("ul");
|
|
1398
|
+
if (parentUl &&
|
|
1399
|
+
isCheckboxList(parentUl) &&
|
|
1400
|
+
el.getAttribute("role") === "checkbox") {
|
|
1401
|
+
const ariaChecked = el.getAttribute("aria-checked");
|
|
1402
|
+
if (ariaChecked !== null) {
|
|
1403
|
+
attributes.checkboxChecked = ariaChecked;
|
|
1078
1404
|
}
|
|
1079
1405
|
}
|
|
1080
|
-
return {
|
|
1081
|
-
type: tagName === "strong" || tagName === "b"
|
|
1082
|
-
? "bold"
|
|
1083
|
-
: tagName === "em" || tagName === "i"
|
|
1084
|
-
? "italic"
|
|
1085
|
-
: tagName === "u"
|
|
1086
|
-
? "underline"
|
|
1087
|
-
: tagName,
|
|
1088
|
-
children: children.length > 0 ? children : undefined,
|
|
1089
|
-
attributes: Object.keys(attributes).length > 0
|
|
1090
|
-
? attributes
|
|
1091
|
-
: undefined,
|
|
1092
|
-
};
|
|
1093
1406
|
}
|
|
1407
|
+
// Process children recursively
|
|
1408
|
+
Array.from(el.childNodes).forEach((child) => {
|
|
1409
|
+
// Skip checkbox input nodes
|
|
1410
|
+
if (child.nodeType === Node.ELEMENT_NODE &&
|
|
1411
|
+
child.tagName.toLowerCase() === "input" &&
|
|
1412
|
+
child.getAttribute("type") === "checkbox") {
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
const processed = processNode(child);
|
|
1416
|
+
if (processed) {
|
|
1417
|
+
children.push(processed);
|
|
1418
|
+
}
|
|
1419
|
+
});
|
|
1420
|
+
return {
|
|
1421
|
+
type: tagName,
|
|
1422
|
+
children: children.length > 0 ? children : [],
|
|
1423
|
+
attributes: Object.keys(attributes).length > 0
|
|
1424
|
+
? attributes
|
|
1425
|
+
: undefined,
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
// Inline elements
|
|
1429
|
+
if ([
|
|
1430
|
+
"strong",
|
|
1431
|
+
"b",
|
|
1432
|
+
"em",
|
|
1433
|
+
"i",
|
|
1434
|
+
"u",
|
|
1435
|
+
"s",
|
|
1436
|
+
"del",
|
|
1437
|
+
"strike",
|
|
1438
|
+
"a",
|
|
1439
|
+
"span",
|
|
1440
|
+
"sub",
|
|
1441
|
+
"sup",
|
|
1442
|
+
"code",
|
|
1443
|
+
].includes(tagName)) {
|
|
1094
1444
|
const children = [];
|
|
1095
1445
|
Array.from(el.childNodes).forEach((child) => {
|
|
1096
1446
|
const processed = processNode(child);
|
|
1097
1447
|
if (processed)
|
|
1098
1448
|
children.push(processed);
|
|
1099
1449
|
});
|
|
1100
|
-
|
|
1450
|
+
const attributes = {};
|
|
1451
|
+
Array.from(el.attributes).forEach((attr) => {
|
|
1452
|
+
attributes[attr.name] = attr.value;
|
|
1453
|
+
});
|
|
1454
|
+
// Links — capture all relevant attributes
|
|
1455
|
+
if (tagName === "a") {
|
|
1456
|
+
const linkAttrs = {};
|
|
1457
|
+
const href = el.getAttribute("href");
|
|
1458
|
+
if (href)
|
|
1459
|
+
linkAttrs.href = href;
|
|
1460
|
+
const target = el.getAttribute("target");
|
|
1461
|
+
if (target)
|
|
1462
|
+
linkAttrs.target = target;
|
|
1463
|
+
const rel = el.getAttribute("rel");
|
|
1464
|
+
if (rel)
|
|
1465
|
+
linkAttrs.rel = rel;
|
|
1466
|
+
const title = el.getAttribute("title");
|
|
1467
|
+
if (title)
|
|
1468
|
+
linkAttrs.title = title;
|
|
1469
|
+
const pageRef = el.getAttribute("data-page-ref");
|
|
1470
|
+
if (pageRef)
|
|
1471
|
+
linkAttrs["data-page-ref"] = pageRef;
|
|
1472
|
+
const urlExtra = el.getAttribute("data-url-extra");
|
|
1473
|
+
if (urlExtra)
|
|
1474
|
+
linkAttrs["data-url-extra"] = urlExtra;
|
|
1101
1475
|
return {
|
|
1102
|
-
type:
|
|
1103
|
-
children,
|
|
1476
|
+
type: "link",
|
|
1477
|
+
children: children.length > 0 ? children : undefined,
|
|
1478
|
+
attributes: Object.keys(linkAttrs).length > 0
|
|
1479
|
+
? linkAttrs
|
|
1480
|
+
: undefined,
|
|
1104
1481
|
};
|
|
1105
1482
|
}
|
|
1483
|
+
// Spans: handle Lexical's white-space: pre-wrap wrapper
|
|
1484
|
+
if (tagName === "span") {
|
|
1485
|
+
const style = el.getAttribute("style") || "";
|
|
1486
|
+
const hasSemanticStyle = style.includes("font-size") ||
|
|
1487
|
+
style.includes("color") ||
|
|
1488
|
+
style.includes("background-color");
|
|
1489
|
+
// Transparent wrapper (e.g. Lexical's <span style="white-space: pre-wrap;">)
|
|
1490
|
+
// Return text content directly
|
|
1491
|
+
if (!hasSemanticStyle) {
|
|
1492
|
+
if (el.childNodes.length === 1 &&
|
|
1493
|
+
el.firstChild?.nodeType === Node.TEXT_NODE) {
|
|
1494
|
+
return {
|
|
1495
|
+
type: "text",
|
|
1496
|
+
text: el.firstChild.textContent || "",
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
// Multiple children: flatten (return each child as-is)
|
|
1500
|
+
if (children.length === 1) {
|
|
1501
|
+
return children[0];
|
|
1502
|
+
}
|
|
1503
|
+
// If multiple children, keep as span but without extra attributes
|
|
1504
|
+
if (children.length > 1) {
|
|
1505
|
+
return {
|
|
1506
|
+
type: "span",
|
|
1507
|
+
children,
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
return null;
|
|
1511
|
+
}
|
|
1512
|
+
// Semantic span: extract meaningful style properties
|
|
1513
|
+
if (style) {
|
|
1514
|
+
style.split(";").forEach((rule) => {
|
|
1515
|
+
const [key, value] = rule
|
|
1516
|
+
.split(":")
|
|
1517
|
+
.map((s) => s.trim());
|
|
1518
|
+
if (key && value) {
|
|
1519
|
+
if (key === "font-size") {
|
|
1520
|
+
attributes.fontSize = value;
|
|
1521
|
+
}
|
|
1522
|
+
else if (key === "color") {
|
|
1523
|
+
attributes.color = value;
|
|
1524
|
+
}
|
|
1525
|
+
else if (key === "background-color") {
|
|
1526
|
+
attributes.backgroundColor = value;
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
// Map tag names to semantic types
|
|
1533
|
+
const type = tagName === "strong" || tagName === "b"
|
|
1534
|
+
? "bold"
|
|
1535
|
+
: tagName === "em" || tagName === "i"
|
|
1536
|
+
? "italic"
|
|
1537
|
+
: tagName === "u"
|
|
1538
|
+
? "underline"
|
|
1539
|
+
: tagName === "s" ||
|
|
1540
|
+
tagName === "del" ||
|
|
1541
|
+
tagName === "strike"
|
|
1542
|
+
? "strikethrough"
|
|
1543
|
+
: tagName === "sub"
|
|
1544
|
+
? "subscript"
|
|
1545
|
+
: tagName === "sup"
|
|
1546
|
+
? "superscript"
|
|
1547
|
+
: tagName === "code"
|
|
1548
|
+
? "code"
|
|
1549
|
+
: tagName;
|
|
1550
|
+
return {
|
|
1551
|
+
type,
|
|
1552
|
+
children: children.length > 0 ? children : undefined,
|
|
1553
|
+
attributes: Object.keys(attributes).length > 0
|
|
1554
|
+
? attributes
|
|
1555
|
+
: undefined,
|
|
1556
|
+
};
|
|
1106
1557
|
}
|
|
1107
1558
|
return null;
|
|
1108
1559
|
}
|
|
1560
|
+
// Process all child nodes of the editor element
|
|
1109
1561
|
Array.from(element.childNodes).forEach((node) => {
|
|
1110
1562
|
const processed = processNode(node);
|
|
1111
1563
|
if (processed) {
|
|
1564
|
+
// Wrap bare text nodes in a paragraph
|
|
1112
1565
|
if (processed.type === "text") {
|
|
1113
1566
|
blocks.push({ type: "p", children: [processed] });
|
|
1114
1567
|
}
|
|
@@ -1122,6 +1575,9 @@ function domToContent(element) {
|
|
|
1122
1575
|
}
|
|
1123
1576
|
return { blocks };
|
|
1124
1577
|
}
|
|
1578
|
+
/**
|
|
1579
|
+
* Converts EditorContent JSON to DOM and appends to the container.
|
|
1580
|
+
*/
|
|
1125
1581
|
function contentToDOM(content, container, customLinkComponent, customHeadingRenderer) {
|
|
1126
1582
|
container.innerHTML = "";
|
|
1127
1583
|
function createNode(node) {
|
|
@@ -1149,7 +1605,11 @@ function contentToDOM(content, container, customLinkComponent, customHeadingRend
|
|
|
1149
1605
|
bold: "strong",
|
|
1150
1606
|
italic: "em",
|
|
1151
1607
|
underline: "u",
|
|
1608
|
+
strikethrough: "s",
|
|
1152
1609
|
link: "a",
|
|
1610
|
+
subscript: "sub",
|
|
1611
|
+
superscript: "sup",
|
|
1612
|
+
code: "code",
|
|
1153
1613
|
};
|
|
1154
1614
|
let tagName = tagMap[node.type] || node.type;
|
|
1155
1615
|
if (node.type === "link" && customLinkComponent) {
|
|
@@ -1161,53 +1621,103 @@ function contentToDOM(content, container, customLinkComponent, customHeadingRend
|
|
|
1161
1621
|
const element = document.createElement(tagName);
|
|
1162
1622
|
if (node.attributes) {
|
|
1163
1623
|
Object.entries(node.attributes).forEach(([key, value]) => {
|
|
1164
|
-
if (key === "fontSize"
|
|
1165
|
-
|
|
1166
|
-
key === "backgroundColor") {
|
|
1167
|
-
const currentStyle = element.getAttribute("style") || "";
|
|
1168
|
-
if (key === "fontSize") {
|
|
1169
|
-
element.setAttribute("style", `${currentStyle}font-size: ${value};`.trim());
|
|
1170
|
-
}
|
|
1171
|
-
else if (key === "color") {
|
|
1172
|
-
element.setAttribute("style", `${currentStyle}color: ${value};`.trim());
|
|
1173
|
-
}
|
|
1174
|
-
else if (key === "backgroundColor") {
|
|
1175
|
-
element.setAttribute("style", `${currentStyle}background-color: ${value};`.trim());
|
|
1176
|
-
}
|
|
1624
|
+
if (key === "fontSize") {
|
|
1625
|
+
element.style.fontSize = value;
|
|
1177
1626
|
}
|
|
1627
|
+
else if (key === "color") {
|
|
1628
|
+
element.style.color = value;
|
|
1629
|
+
}
|
|
1630
|
+
else if (key === "backgroundColor") {
|
|
1631
|
+
element.style.backgroundColor = value;
|
|
1632
|
+
}
|
|
1633
|
+
else if (key === "textAlign") {
|
|
1634
|
+
element.style.textAlign = value;
|
|
1635
|
+
}
|
|
1636
|
+
else if (key === "href" && tagName === "a") {
|
|
1637
|
+
element.setAttribute("href", value);
|
|
1638
|
+
}
|
|
1639
|
+
else if (key === "target" && tagName === "a") {
|
|
1640
|
+
element.setAttribute("target", value);
|
|
1641
|
+
}
|
|
1642
|
+
else if (key === "rel" && tagName === "a") {
|
|
1643
|
+
element.setAttribute("rel", value);
|
|
1644
|
+
}
|
|
1645
|
+
else if (key === "title" && tagName === "a") {
|
|
1646
|
+
element.setAttribute("title", value);
|
|
1647
|
+
}
|
|
1648
|
+
else if (key === "data-page-ref" && tagName === "a") {
|
|
1649
|
+
element.setAttribute("data-page-ref", value);
|
|
1650
|
+
}
|
|
1651
|
+
else if (key === "data-url-extra" && tagName === "a") {
|
|
1652
|
+
element.setAttribute("data-url-extra", value);
|
|
1653
|
+
}
|
|
1654
|
+
else if (key === "data-attachment-id") {
|
|
1655
|
+
element.setAttribute("data-attachment-id", value);
|
|
1656
|
+
}
|
|
1657
|
+
else if (key === "colspan") {
|
|
1658
|
+
element.setAttribute("colspan", value);
|
|
1659
|
+
}
|
|
1660
|
+
else if (key === "rowspan") {
|
|
1661
|
+
element.setAttribute("rowspan", value);
|
|
1662
|
+
}
|
|
1663
|
+
else if (key === "class") {
|
|
1664
|
+
element.className = value;
|
|
1665
|
+
}
|
|
1666
|
+
else if (key === "checkboxChecked") ;
|
|
1178
1667
|
else {
|
|
1179
1668
|
element.setAttribute(key, value);
|
|
1180
1669
|
}
|
|
1181
1670
|
});
|
|
1182
1671
|
}
|
|
1672
|
+
// Ensure tables have the rte-table class
|
|
1673
|
+
if (node.type === "table" && !element.classList.contains("rte-table")) {
|
|
1674
|
+
element.classList.add("rte-table");
|
|
1675
|
+
}
|
|
1183
1676
|
if (node.children) {
|
|
1184
1677
|
node.children.forEach((child) => {
|
|
1185
|
-
|
|
1186
|
-
element.appendChild(childNode);
|
|
1678
|
+
element.appendChild(createNode(child));
|
|
1187
1679
|
});
|
|
1188
1680
|
}
|
|
1681
|
+
// Set checkbox attributes on list items
|
|
1682
|
+
if (node.type === "li" &&
|
|
1683
|
+
node.attributes?.checkboxChecked !== undefined) {
|
|
1684
|
+
element.setAttribute("role", "checkbox");
|
|
1685
|
+
element.setAttribute("tabIndex", "-1");
|
|
1686
|
+
element.setAttribute("aria-checked", node.attributes.checkboxChecked);
|
|
1687
|
+
}
|
|
1189
1688
|
return element;
|
|
1190
1689
|
}
|
|
1191
1690
|
content.blocks.forEach((block) => {
|
|
1192
|
-
|
|
1193
|
-
container.appendChild(blockNode);
|
|
1691
|
+
container.appendChild(createNode(block));
|
|
1194
1692
|
});
|
|
1693
|
+
// Normalize all checkbox lists after DOM creation
|
|
1694
|
+
ensureAllCheckboxes(container);
|
|
1195
1695
|
}
|
|
1196
|
-
|
|
1696
|
+
/**
|
|
1697
|
+
* Creates empty editor content with a single paragraph.
|
|
1698
|
+
*/
|
|
1699
|
+
function createEmptyContent() {
|
|
1197
1700
|
return {
|
|
1198
1701
|
blocks: [{ type: "p", children: [] }],
|
|
1199
1702
|
};
|
|
1200
1703
|
}
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
return domToContent(tempDiv);
|
|
1205
|
-
}
|
|
1704
|
+
/**
|
|
1705
|
+
* Converts EditorContent to an HTML string.
|
|
1706
|
+
*/
|
|
1206
1707
|
function contentToHTML(content) {
|
|
1207
1708
|
const tempDiv = document.createElement("div");
|
|
1208
1709
|
contentToDOM(content, tempDiv);
|
|
1209
1710
|
return tempDiv.innerHTML;
|
|
1210
1711
|
}
|
|
1712
|
+
/**
|
|
1713
|
+
* Converts an HTML string to EditorContent.
|
|
1714
|
+
* Supports Lexical, GitHub, and standard HTML formats.
|
|
1715
|
+
*/
|
|
1716
|
+
function htmlToContent(htmlString) {
|
|
1717
|
+
const tempDiv = document.createElement("div");
|
|
1718
|
+
tempDiv.innerHTML = htmlString;
|
|
1719
|
+
return domToContent(tempDiv);
|
|
1720
|
+
}
|
|
1211
1721
|
|
|
1212
1722
|
class HistoryManager {
|
|
1213
1723
|
constructor() {
|
|
@@ -1417,321 +1927,974 @@ function outdentListItem(selection) {
|
|
|
1417
1927
|
return true;
|
|
1418
1928
|
}
|
|
1419
1929
|
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1930
|
+
/** Debounce time for pushing to history after input (ms) */
|
|
1931
|
+
const HISTORY_DEBOUNCE_MS = 300;
|
|
1932
|
+
/** Width of the clickable checkbox area in pixels */
|
|
1933
|
+
const CHECKBOX_CLICK_ZONE_PX = 40;
|
|
1934
|
+
|
|
1935
|
+
/**
|
|
1936
|
+
* Hook that manages all checkbox list interactions.
|
|
1937
|
+
* Consolidates click handling, keyboard navigation, and checkbox insertion.
|
|
1938
|
+
* Uses event delegation (single listener on editor root) for all checkbox events.
|
|
1939
|
+
*/
|
|
1940
|
+
function useCheckbox({ editorRef, isUpdatingRef, pushToHistory, notifyChange, getDomContent, }) {
|
|
1941
|
+
// --- Event Listeners (click + pointerdown) ---
|
|
1423
1942
|
useEffect(() => {
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1943
|
+
const editor = editorRef.current;
|
|
1944
|
+
if (!editor)
|
|
1945
|
+
return;
|
|
1946
|
+
const handleClick = (event) => {
|
|
1947
|
+
const clickTarget = event.target;
|
|
1948
|
+
if (!(clickTarget instanceof HTMLElement))
|
|
1949
|
+
return;
|
|
1950
|
+
const listItem = clickTarget.tagName === "LI"
|
|
1951
|
+
? clickTarget
|
|
1952
|
+
: clickTarget.closest("li");
|
|
1953
|
+
if (!listItem)
|
|
1954
|
+
return;
|
|
1955
|
+
const parentNode = listItem.parentElement;
|
|
1956
|
+
if (!parentNode || !isCheckboxList(parentNode))
|
|
1957
|
+
return;
|
|
1958
|
+
if (listItem.getAttribute("role") !== "checkbox")
|
|
1959
|
+
return;
|
|
1960
|
+
if (isNestedListItem(listItem))
|
|
1961
|
+
return;
|
|
1962
|
+
const rect = listItem.getBoundingClientRect();
|
|
1963
|
+
const clientX = event.clientX;
|
|
1964
|
+
const isInCheckboxArea = listItem.dir === "rtl"
|
|
1965
|
+
? clientX >= rect.right - CHECKBOX_CLICK_ZONE_PX
|
|
1966
|
+
: clientX <= rect.left + CHECKBOX_CLICK_ZONE_PX;
|
|
1967
|
+
if (isInCheckboxArea) {
|
|
1968
|
+
event.preventDefault();
|
|
1969
|
+
event.stopPropagation();
|
|
1970
|
+
toggleListItemChecked(listItem);
|
|
1971
|
+
// Trigger change event
|
|
1972
|
+
const changeEvent = new Event("input", { bubbles: true });
|
|
1973
|
+
editor.dispatchEvent(changeEvent);
|
|
1974
|
+
}
|
|
1430
1975
|
};
|
|
1431
|
-
const
|
|
1432
|
-
|
|
1976
|
+
const handlePointerDown = (event) => {
|
|
1977
|
+
const target = event.target;
|
|
1978
|
+
if (!(target instanceof HTMLElement))
|
|
1979
|
+
return;
|
|
1980
|
+
const listItem = target.tagName === "LI"
|
|
1981
|
+
? target
|
|
1982
|
+
: target.closest("li");
|
|
1983
|
+
if (!listItem)
|
|
1984
|
+
return;
|
|
1985
|
+
if (isNestedListItem(listItem))
|
|
1986
|
+
return;
|
|
1987
|
+
const parentNode = listItem.parentElement;
|
|
1988
|
+
if (!parentNode || !isCheckboxList(parentNode))
|
|
1989
|
+
return;
|
|
1990
|
+
const rect = listItem.getBoundingClientRect();
|
|
1991
|
+
const clientX = event.clientX;
|
|
1992
|
+
const isInCheckboxArea = listItem.dir === "rtl"
|
|
1993
|
+
? clientX >= rect.right - CHECKBOX_CLICK_ZONE_PX
|
|
1994
|
+
: clientX <= rect.left + CHECKBOX_CLICK_ZONE_PX;
|
|
1995
|
+
if (isInCheckboxArea) {
|
|
1996
|
+
event.preventDefault();
|
|
1997
|
+
}
|
|
1433
1998
|
};
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
document.addEventListener("mouseup", handleMouseUp);
|
|
1437
|
-
document.addEventListener("keyup", handleKeyUp);
|
|
1438
|
-
}
|
|
1999
|
+
editor.addEventListener("click", handleClick, true);
|
|
2000
|
+
editor.addEventListener("pointerdown", handlePointerDown, true);
|
|
1439
2001
|
return () => {
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
document.removeEventListener("mouseup", handleMouseUp);
|
|
1443
|
-
document.removeEventListener("keyup", handleKeyUp);
|
|
1444
|
-
}
|
|
2002
|
+
editor.removeEventListener("click", handleClick, true);
|
|
2003
|
+
editor.removeEventListener("pointerdown", handlePointerDown, true);
|
|
1445
2004
|
};
|
|
1446
|
-
}, []);
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
2005
|
+
}, [editorRef]);
|
|
2006
|
+
// --- Keyboard handlers (called from useEditorEvents) ---
|
|
2007
|
+
/**
|
|
2008
|
+
* Handles checkbox-specific keyboard events.
|
|
2009
|
+
* Returns true if the event was handled and should not propagate.
|
|
2010
|
+
*/
|
|
2011
|
+
const handleCheckboxKeyDown = useCallback((e) => {
|
|
2012
|
+
// Arrow up/down: navigate between checkbox items
|
|
2013
|
+
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
2014
|
+
const activeItem = getActiveCheckListItem();
|
|
2015
|
+
if (activeItem) {
|
|
2016
|
+
const backward = e.key === "ArrowUp";
|
|
2017
|
+
const nextItem = findCheckListItemSibling(activeItem, backward);
|
|
2018
|
+
if (nextItem) {
|
|
2019
|
+
e.preventDefault();
|
|
2020
|
+
nextItem.focus();
|
|
2021
|
+
return true;
|
|
2022
|
+
}
|
|
1451
2023
|
}
|
|
1452
|
-
|
|
1453
|
-
|
|
2024
|
+
}
|
|
2025
|
+
// Space: toggle checkbox
|
|
2026
|
+
if (e.key === " ") {
|
|
2027
|
+
const activeItem = getActiveCheckListItem();
|
|
2028
|
+
if (activeItem) {
|
|
2029
|
+
e.preventDefault();
|
|
2030
|
+
toggleListItemChecked(activeItem);
|
|
2031
|
+
const editorEl = activeItem.closest(".rte-editor");
|
|
2032
|
+
if (editorEl) {
|
|
2033
|
+
editorEl.dispatchEvent(new Event("input", { bubbles: true }));
|
|
2034
|
+
}
|
|
2035
|
+
return true;
|
|
1454
2036
|
}
|
|
1455
|
-
|
|
1456
|
-
|
|
2037
|
+
}
|
|
2038
|
+
// Escape: blur from checkbox item
|
|
2039
|
+
if (e.key === "Escape") {
|
|
2040
|
+
const activeItem = getActiveCheckListItem();
|
|
2041
|
+
if (activeItem) {
|
|
2042
|
+
const editorEl = activeItem.closest(".rte-editor");
|
|
2043
|
+
if (editorEl instanceof HTMLElement) {
|
|
2044
|
+
editorEl.focus();
|
|
2045
|
+
}
|
|
2046
|
+
return true;
|
|
1457
2047
|
}
|
|
1458
|
-
setTimeout(() => setUpdateTrigger((prev) => prev + 1), 50);
|
|
1459
2048
|
}
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
currentValue,
|
|
1482
|
-
};
|
|
1483
|
-
return (jsx(React.Fragment, { children: plugin.renderButton(buttonProps) }, plugin.name));
|
|
1484
|
-
}) }), clearFormattingPlugin && clearFormattingPlugin.renderButton && (jsxs("div", { className: "rte-toolbar-right", children: [jsx("div", { className: "rte-toolbar-divider" }), (() => {
|
|
1485
|
-
const isActive = isClient && clearFormattingPlugin.isActive
|
|
1486
|
-
? clearFormattingPlugin.isActive(editorAPI)
|
|
1487
|
-
: false;
|
|
1488
|
-
const canExecute = isClient && clearFormattingPlugin.canExecute
|
|
1489
|
-
? clearFormattingPlugin.canExecute(editorAPI)
|
|
1490
|
-
: true;
|
|
1491
|
-
const buttonProps = {
|
|
1492
|
-
isActive,
|
|
1493
|
-
onClick: () => handlePluginClick(clearFormattingPlugin),
|
|
1494
|
-
disabled: !canExecute,
|
|
1495
|
-
editorAPI,
|
|
1496
|
-
};
|
|
1497
|
-
return (jsx(React.Fragment, { children: clearFormattingPlugin.renderButton(buttonProps) }, clearFormattingPlugin.name));
|
|
1498
|
-
})()] }))] }));
|
|
1499
|
-
};
|
|
1500
|
-
|
|
1501
|
-
const Editor = ({ initialContent, onChange, plugins: providedPlugins, placeholder = "Text eingeben...", className, toolbarClassName, editorClassName, fontSizes, colors, headings, customLinkComponent, customHeadingRenderer, customRenderer, onEditorAPIReady, theme, onImageUpload, }) => {
|
|
1502
|
-
const plugins = useMemo(() => {
|
|
1503
|
-
const allPlugins = [...(providedPlugins || defaultPlugins)];
|
|
1504
|
-
if (fontSizes && fontSizes.length > 0) {
|
|
1505
|
-
allPlugins.push(createFontSizePlugin(fontSizes));
|
|
2049
|
+
// Arrow left: focus list item when cursor is at the start
|
|
2050
|
+
if (e.key === "ArrowLeft") {
|
|
2051
|
+
const selection = window.getSelection();
|
|
2052
|
+
if (!selection || selection.rangeCount === 0)
|
|
2053
|
+
return false;
|
|
2054
|
+
const range = selection.getRangeAt(0);
|
|
2055
|
+
const container = range.commonAncestorContainer;
|
|
2056
|
+
const listItem = findClosestListItem(container);
|
|
2057
|
+
if (!listItem)
|
|
2058
|
+
return false;
|
|
2059
|
+
const parent = listItem.parentElement;
|
|
2060
|
+
if (parent &&
|
|
2061
|
+
isCheckboxList(parent) &&
|
|
2062
|
+
range.collapsed &&
|
|
2063
|
+
range.startOffset === 0) {
|
|
2064
|
+
if (document.activeElement !== listItem) {
|
|
2065
|
+
listItem.focus();
|
|
2066
|
+
e.preventDefault();
|
|
2067
|
+
return true;
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
1506
2070
|
}
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
2071
|
+
return false;
|
|
2072
|
+
}, []);
|
|
2073
|
+
/**
|
|
2074
|
+
* Handles Enter key in checkbox lists.
|
|
2075
|
+
* Creates a new unchecked item after the current one.
|
|
2076
|
+
* Returns true if the event was handled.
|
|
2077
|
+
*/
|
|
2078
|
+
const handleCheckboxEnter = useCallback((e) => {
|
|
2079
|
+
if (e.key !== "Enter")
|
|
2080
|
+
return false;
|
|
2081
|
+
const selection = window.getSelection();
|
|
2082
|
+
if (!selection || selection.rangeCount === 0)
|
|
2083
|
+
return false;
|
|
2084
|
+
const range = selection.getRangeAt(0);
|
|
2085
|
+
const container = range.commonAncestorContainer;
|
|
2086
|
+
const listItem = findClosestListItem(container);
|
|
2087
|
+
if (!listItem)
|
|
2088
|
+
return false;
|
|
2089
|
+
const checkboxList = findClosestCheckboxList(listItem);
|
|
2090
|
+
if (!checkboxList)
|
|
2091
|
+
return false;
|
|
2092
|
+
e.preventDefault();
|
|
2093
|
+
const newLi = document.createElement("li");
|
|
2094
|
+
updateListItemChecked(newLi, false);
|
|
2095
|
+
const textNode = document.createTextNode(" ");
|
|
2096
|
+
newLi.appendChild(textNode);
|
|
2097
|
+
if (listItem.nextSibling) {
|
|
2098
|
+
checkboxList.insertBefore(newLi, listItem.nextSibling);
|
|
1510
2099
|
}
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
if (headings && headings.length > 0) {
|
|
1514
|
-
// Entferne das Standard-BlockFormat-Plugin
|
|
1515
|
-
const blockFormatIndex = allPlugins.findIndex((p) => p.name === "blockFormat");
|
|
1516
|
-
if (blockFormatIndex !== -1) {
|
|
1517
|
-
allPlugins.splice(blockFormatIndex, 1);
|
|
1518
|
-
}
|
|
1519
|
-
// Füge das Plugin mit custom Headlines hinzu
|
|
1520
|
-
allPlugins.push(createBlockFormatPlugin(headings));
|
|
2100
|
+
else {
|
|
2101
|
+
checkboxList.appendChild(newLi);
|
|
1521
2102
|
}
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
2103
|
+
const editor = editorRef.current;
|
|
2104
|
+
if (editor)
|
|
2105
|
+
ensureAllCheckboxes(editor);
|
|
2106
|
+
const newRange = document.createRange();
|
|
2107
|
+
newRange.setStart(textNode, 0);
|
|
2108
|
+
newRange.collapse(true);
|
|
2109
|
+
selection.removeAllRanges();
|
|
2110
|
+
selection.addRange(newRange);
|
|
2111
|
+
if (editor) {
|
|
2112
|
+
const content = getDomContent();
|
|
2113
|
+
pushToHistory(content);
|
|
2114
|
+
notifyChange(content);
|
|
1531
2115
|
}
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
2116
|
+
return true;
|
|
2117
|
+
}, [editorRef, getDomContent, pushToHistory, notifyChange]);
|
|
2118
|
+
/**
|
|
2119
|
+
* Inserts a new checkbox list at the current cursor position,
|
|
2120
|
+
* or removes it if already inside one.
|
|
2121
|
+
* Returns true on success.
|
|
2122
|
+
*/
|
|
2123
|
+
const insertCheckboxList = useCallback((editor) => {
|
|
2124
|
+
isUpdatingRef.current = true;
|
|
1537
2125
|
const selection = window.getSelection();
|
|
1538
|
-
if (
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
selection?.removeAllRanges();
|
|
1542
|
-
selection?.addRange(range);
|
|
2126
|
+
if (!selection || selection.rangeCount === 0) {
|
|
2127
|
+
isUpdatingRef.current = false;
|
|
2128
|
+
return false;
|
|
1543
2129
|
}
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
const
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
2130
|
+
const range = selection.getRangeAt(0);
|
|
2131
|
+
const container = range.commonAncestorContainer;
|
|
2132
|
+
const element = container.nodeType === Node.TEXT_NODE
|
|
2133
|
+
? container.parentElement
|
|
2134
|
+
: container;
|
|
2135
|
+
if (!element) {
|
|
2136
|
+
isUpdatingRef.current = false;
|
|
2137
|
+
return false;
|
|
2138
|
+
}
|
|
2139
|
+
// Already in a checkbox list? Remove it.
|
|
2140
|
+
const existingList = findClosestCheckboxList(element);
|
|
2141
|
+
if (existingList) {
|
|
2142
|
+
existingList.classList.remove("rte-checkbox-list");
|
|
2143
|
+
existingList
|
|
2144
|
+
.querySelectorAll("li[role='checkbox']")
|
|
2145
|
+
.forEach((li) => {
|
|
2146
|
+
li.removeAttribute("role");
|
|
2147
|
+
li.removeAttribute("tabIndex");
|
|
2148
|
+
li.removeAttribute("aria-checked");
|
|
2149
|
+
});
|
|
2150
|
+
isUpdatingRef.current = false;
|
|
2151
|
+
return true;
|
|
2152
|
+
}
|
|
2153
|
+
// Create new checkbox list
|
|
2154
|
+
const ul = document.createElement("ul");
|
|
2155
|
+
ul.classList.add("rte-checkbox-list");
|
|
2156
|
+
const li = document.createElement("li");
|
|
2157
|
+
updateListItemChecked(li, false);
|
|
2158
|
+
const textNode = document.createTextNode(" ");
|
|
2159
|
+
li.appendChild(textNode);
|
|
2160
|
+
ul.appendChild(li);
|
|
2161
|
+
// Find block element to replace
|
|
2162
|
+
const blockElement = element.closest("p, div, h1, h2, h3, h4, h5, h6, blockquote");
|
|
2163
|
+
const isValidBlockElement = blockElement &&
|
|
2164
|
+
blockElement !== editor &&
|
|
2165
|
+
editor.contains(blockElement) &&
|
|
2166
|
+
blockElement.parentElement;
|
|
2167
|
+
if (isValidBlockElement) {
|
|
2168
|
+
const textContent = blockElement.textContent || "";
|
|
2169
|
+
blockElement.parentElement.replaceChild(ul, blockElement);
|
|
2170
|
+
const finalTextNode = li.firstChild;
|
|
2171
|
+
if (finalTextNode) {
|
|
2172
|
+
finalTextNode.textContent = textContent || " ";
|
|
2173
|
+
const cursorPos = textContent ? textContent.length : 0;
|
|
2174
|
+
setCursorInTextNode(finalTextNode, cursorPos, editor);
|
|
1577
2175
|
}
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
const range = document.createRange();
|
|
1587
|
-
if (editor.childNodes.length > 0) {
|
|
1588
|
-
const lastChild = editor.childNodes[editor.childNodes.length - 1];
|
|
1589
|
-
range.setStartAfter(lastChild);
|
|
1590
|
-
range.collapse(true);
|
|
1591
|
-
}
|
|
1592
|
-
else {
|
|
1593
|
-
const img = document.createElement("img");
|
|
1594
|
-
img.setAttribute("src", value);
|
|
1595
|
-
img.setAttribute("alt", "");
|
|
1596
|
-
img.style.maxWidth = "100%";
|
|
1597
|
-
img.style.height = "auto";
|
|
1598
|
-
img.style.display = "block";
|
|
1599
|
-
img.style.margin = "16px 0";
|
|
1600
|
-
editor.appendChild(img);
|
|
1601
|
-
const newRange = document.createRange();
|
|
1602
|
-
newRange.setStartAfter(img);
|
|
1603
|
-
newRange.collapse(true);
|
|
1604
|
-
selection.removeAllRanges();
|
|
1605
|
-
selection.addRange(newRange);
|
|
1606
|
-
isUpdatingRef.current = true;
|
|
1607
|
-
setTimeout(() => {
|
|
1608
|
-
if (editor) {
|
|
1609
|
-
const currentContent = domToContent(editor);
|
|
1610
|
-
historyRef.current.push(currentContent);
|
|
1611
|
-
isUpdatingRef.current = false;
|
|
1612
|
-
notifyChange(currentContent);
|
|
1613
|
-
}
|
|
1614
|
-
}, 0);
|
|
1615
|
-
return true;
|
|
1616
|
-
}
|
|
1617
|
-
selection.removeAllRanges();
|
|
1618
|
-
selection.addRange(range);
|
|
1619
|
-
}
|
|
1620
|
-
if (selection.rangeCount === 0)
|
|
1621
|
-
return false;
|
|
1622
|
-
const range = selection.getRangeAt(0);
|
|
1623
|
-
const container = range.commonAncestorContainer;
|
|
1624
|
-
let parentElement = null;
|
|
1625
|
-
if (container.nodeType === Node.TEXT_NODE) {
|
|
1626
|
-
parentElement = container.parentElement;
|
|
1627
|
-
}
|
|
1628
|
-
else if (container.nodeType === Node.ELEMENT_NODE) {
|
|
1629
|
-
parentElement = container;
|
|
1630
|
-
}
|
|
1631
|
-
const img = document.createElement("img");
|
|
1632
|
-
img.setAttribute("src", value);
|
|
1633
|
-
img.setAttribute("alt", "");
|
|
1634
|
-
img.style.maxWidth = "100%";
|
|
1635
|
-
img.style.height = "auto";
|
|
1636
|
-
img.style.display = "block";
|
|
1637
|
-
img.style.margin = "16px 0";
|
|
1638
|
-
if (parentElement &&
|
|
1639
|
-
parentElement !== editor &&
|
|
1640
|
-
(parentElement.tagName === "P" ||
|
|
1641
|
-
parentElement.tagName === "DIV" ||
|
|
1642
|
-
parentElement.tagName === "H1" ||
|
|
1643
|
-
parentElement.tagName === "H2" ||
|
|
1644
|
-
parentElement.tagName === "H3" ||
|
|
1645
|
-
parentElement.tagName === "H4" ||
|
|
1646
|
-
parentElement.tagName === "H5" ||
|
|
1647
|
-
parentElement.tagName === "H6")) {
|
|
1648
|
-
if (parentElement.nextSibling) {
|
|
1649
|
-
editor.insertBefore(img, parentElement.nextSibling);
|
|
1650
|
-
}
|
|
1651
|
-
else {
|
|
1652
|
-
editor.appendChild(img);
|
|
1653
|
-
}
|
|
1654
|
-
}
|
|
1655
|
-
else {
|
|
1656
|
-
try {
|
|
1657
|
-
range.insertNode(img);
|
|
1658
|
-
}
|
|
1659
|
-
catch (e) {
|
|
1660
|
-
editor.appendChild(img);
|
|
1661
|
-
}
|
|
2176
|
+
}
|
|
2177
|
+
else {
|
|
2178
|
+
try {
|
|
2179
|
+
range.deleteContents();
|
|
2180
|
+
range.insertNode(ul);
|
|
2181
|
+
const finalTextNode = li.firstChild;
|
|
2182
|
+
if (finalTextNode) {
|
|
2183
|
+
setCursorInTextNode(finalTextNode, 0, editor);
|
|
1662
2184
|
}
|
|
1663
|
-
const newRange = document.createRange();
|
|
1664
|
-
newRange.setStartAfter(img);
|
|
1665
|
-
newRange.collapse(true);
|
|
1666
|
-
selection.removeAllRanges();
|
|
1667
|
-
selection.addRange(newRange);
|
|
1668
|
-
isUpdatingRef.current = true;
|
|
1669
|
-
setTimeout(() => {
|
|
1670
|
-
if (editor) {
|
|
1671
|
-
const currentContent = domToContent(editor);
|
|
1672
|
-
historyRef.current.push(currentContent);
|
|
1673
|
-
isUpdatingRef.current = false;
|
|
1674
|
-
notifyChange(currentContent);
|
|
1675
|
-
}
|
|
1676
|
-
}, 0);
|
|
1677
|
-
return true;
|
|
1678
|
-
}
|
|
1679
|
-
const selection = window.getSelection();
|
|
1680
|
-
let savedRange = null;
|
|
1681
|
-
if (selection && selection.rangeCount > 0) {
|
|
1682
|
-
savedRange = selection.getRangeAt(0).cloneRange();
|
|
1683
|
-
}
|
|
1684
|
-
if (document.activeElement !== editor) {
|
|
1685
|
-
editor.focus();
|
|
1686
2185
|
}
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
range.setStart(lastChild, lastChild.textContent?.length || 0);
|
|
1693
|
-
range.setEnd(lastChild, lastChild.textContent?.length || 0);
|
|
1694
|
-
}
|
|
1695
|
-
else {
|
|
1696
|
-
range.selectNodeContents(lastChild);
|
|
1697
|
-
range.collapse(false);
|
|
1698
|
-
}
|
|
1699
|
-
}
|
|
1700
|
-
else {
|
|
1701
|
-
const p = document.createElement("p");
|
|
1702
|
-
editor.appendChild(p);
|
|
1703
|
-
const textNode = document.createTextNode("");
|
|
1704
|
-
p.appendChild(textNode);
|
|
1705
|
-
range.setStart(textNode, 0);
|
|
1706
|
-
range.setEnd(textNode, 0);
|
|
2186
|
+
catch (_) {
|
|
2187
|
+
editor.appendChild(ul);
|
|
2188
|
+
const finalTextNode = li.firstChild;
|
|
2189
|
+
if (finalTextNode) {
|
|
2190
|
+
setCursorInTextNode(finalTextNode, 0, editor);
|
|
1707
2191
|
}
|
|
1708
|
-
selection?.removeAllRanges();
|
|
1709
|
-
selection?.addRange(range);
|
|
1710
2192
|
}
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
2193
|
+
}
|
|
2194
|
+
// After insertion: ensure attributes and save to history
|
|
2195
|
+
setTimeout(() => {
|
|
2196
|
+
if (!editor)
|
|
2197
|
+
return;
|
|
2198
|
+
ensureAllCheckboxes(editor);
|
|
2199
|
+
const content = getDomContent();
|
|
2200
|
+
pushToHistory(content);
|
|
2201
|
+
isUpdatingRef.current = false;
|
|
2202
|
+
notifyChange(content);
|
|
2203
|
+
}, 100);
|
|
2204
|
+
return true;
|
|
2205
|
+
}, [isUpdatingRef, getDomContent, pushToHistory, notifyChange]);
|
|
2206
|
+
return {
|
|
2207
|
+
ensureAllCheckboxes,
|
|
2208
|
+
insertCheckboxList,
|
|
2209
|
+
handleCheckboxKeyDown,
|
|
2210
|
+
handleCheckboxEnter,
|
|
2211
|
+
updateListItemChecked,
|
|
2212
|
+
};
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
/**
|
|
2216
|
+
* Table DOM manipulation utilities for contentEditable.
|
|
2217
|
+
* Pure DOM functions — no React dependency.
|
|
2218
|
+
*/
|
|
2219
|
+
/* ── Helpers ──────────────────────────────────────────────────────────── */
|
|
2220
|
+
/** Get the <td>/<th> cell that contains the current selection. */
|
|
2221
|
+
function getActiveCell() {
|
|
2222
|
+
const sel = document.getSelection();
|
|
2223
|
+
if (!sel || sel.rangeCount === 0)
|
|
2224
|
+
return null;
|
|
2225
|
+
const range = sel.getRangeAt(0);
|
|
2226
|
+
const container = range.commonAncestorContainer;
|
|
2227
|
+
const el = container.nodeType === Node.TEXT_NODE
|
|
2228
|
+
? container.parentElement
|
|
2229
|
+
: container;
|
|
2230
|
+
return el?.closest("td, th");
|
|
2231
|
+
}
|
|
2232
|
+
/** Get the <table> that contains the current selection. */
|
|
2233
|
+
function getActiveTable() {
|
|
2234
|
+
const cell = getActiveCell();
|
|
2235
|
+
return cell?.closest("table") ?? null;
|
|
2236
|
+
}
|
|
2237
|
+
/** Get the <tr> that contains the current selection. */
|
|
2238
|
+
function getActiveRow() {
|
|
2239
|
+
const cell = getActiveCell();
|
|
2240
|
+
return cell?.closest("tr") ?? null;
|
|
2241
|
+
}
|
|
2242
|
+
/** Place the cursor inside a cell (at the start). */
|
|
2243
|
+
function focusCell(cell) {
|
|
2244
|
+
const range = document.createRange();
|
|
2245
|
+
const sel = document.getSelection();
|
|
2246
|
+
if (!sel)
|
|
2247
|
+
return;
|
|
2248
|
+
if (cell.firstChild) {
|
|
2249
|
+
range.setStart(cell.firstChild, 0);
|
|
2250
|
+
}
|
|
2251
|
+
else {
|
|
2252
|
+
// Empty cell — add a <br> so the cursor has something to land on
|
|
2253
|
+
const br = document.createElement("br");
|
|
2254
|
+
cell.appendChild(br);
|
|
2255
|
+
range.setStart(cell, 0);
|
|
2256
|
+
}
|
|
2257
|
+
range.collapse(true);
|
|
2258
|
+
sel.removeAllRanges();
|
|
2259
|
+
sel.addRange(range);
|
|
2260
|
+
}
|
|
2261
|
+
/** Count the number of columns in a table (from the first row). */
|
|
2262
|
+
function getColumnCount(table) {
|
|
2263
|
+
const firstRow = table.querySelector("tr");
|
|
2264
|
+
return firstRow ? firstRow.cells.length : 0;
|
|
2265
|
+
}
|
|
2266
|
+
/* ── Create ───────────────────────────────────────────────────────────── */
|
|
2267
|
+
/** Create a new table element with the given dimensions. */
|
|
2268
|
+
function createTable(rows, cols, withHeader = false) {
|
|
2269
|
+
const table = document.createElement("table");
|
|
2270
|
+
table.classList.add("rte-table");
|
|
2271
|
+
if (withHeader) {
|
|
2272
|
+
const thead = document.createElement("thead");
|
|
2273
|
+
const tr = document.createElement("tr");
|
|
2274
|
+
for (let c = 0; c < cols; c++) {
|
|
2275
|
+
const th = document.createElement("th");
|
|
2276
|
+
th.innerHTML = "<br>";
|
|
2277
|
+
tr.appendChild(th);
|
|
2278
|
+
}
|
|
2279
|
+
thead.appendChild(tr);
|
|
2280
|
+
table.appendChild(thead);
|
|
2281
|
+
}
|
|
2282
|
+
const tbody = document.createElement("tbody");
|
|
2283
|
+
const dataRows = withHeader ? rows - 1 : rows;
|
|
2284
|
+
for (let r = 0; r < dataRows; r++) {
|
|
2285
|
+
const tr = document.createElement("tr");
|
|
2286
|
+
for (let c = 0; c < cols; c++) {
|
|
2287
|
+
const td = document.createElement("td");
|
|
2288
|
+
td.innerHTML = "<br>";
|
|
2289
|
+
tr.appendChild(td);
|
|
2290
|
+
}
|
|
2291
|
+
tbody.appendChild(tr);
|
|
2292
|
+
}
|
|
2293
|
+
table.appendChild(tbody);
|
|
2294
|
+
return table;
|
|
2295
|
+
}
|
|
2296
|
+
/* ── Row operations ───────────────────────────────────────────────────── */
|
|
2297
|
+
/** Insert a row above or below the currently selected row. */
|
|
2298
|
+
function insertRow(position) {
|
|
2299
|
+
const row = getActiveRow();
|
|
2300
|
+
const table = getActiveTable();
|
|
2301
|
+
if (!row || !table)
|
|
2302
|
+
return;
|
|
2303
|
+
const cols = row.cells.length;
|
|
2304
|
+
const newRow = document.createElement("tr");
|
|
2305
|
+
for (let c = 0; c < cols; c++) {
|
|
2306
|
+
const td = document.createElement("td");
|
|
2307
|
+
td.innerHTML = "<br>";
|
|
2308
|
+
newRow.appendChild(td);
|
|
2309
|
+
}
|
|
2310
|
+
if (position === "above") {
|
|
2311
|
+
row.parentNode?.insertBefore(newRow, row);
|
|
2312
|
+
}
|
|
2313
|
+
else {
|
|
2314
|
+
row.parentNode?.insertBefore(newRow, row.nextSibling);
|
|
2315
|
+
}
|
|
2316
|
+
focusCell(newRow.cells[0]);
|
|
2317
|
+
}
|
|
2318
|
+
/** Delete the row that contains the current selection. */
|
|
2319
|
+
function deleteRow() {
|
|
2320
|
+
const row = getActiveRow();
|
|
2321
|
+
const table = getActiveTable();
|
|
2322
|
+
if (!row || !table)
|
|
2323
|
+
return;
|
|
2324
|
+
const allRows = table.querySelectorAll("tr");
|
|
2325
|
+
if (allRows.length <= 1) {
|
|
2326
|
+
// Last row — remove the whole table
|
|
2327
|
+
deleteTable();
|
|
2328
|
+
return;
|
|
2329
|
+
}
|
|
2330
|
+
// Focus adjacent row before deleting
|
|
2331
|
+
const nextRow = (row.nextElementSibling ||
|
|
2332
|
+
row.previousElementSibling);
|
|
2333
|
+
row.remove();
|
|
2334
|
+
if (nextRow?.cells[0]) {
|
|
2335
|
+
focusCell(nextRow.cells[0]);
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
/* ── Column operations ────────────────────────────────────────────────── */
|
|
2339
|
+
/** Insert a column to the left or right of the currently selected cell. */
|
|
2340
|
+
function insertColumn(position) {
|
|
2341
|
+
const cell = getActiveCell();
|
|
2342
|
+
const table = getActiveTable();
|
|
2343
|
+
if (!cell || !table)
|
|
2344
|
+
return;
|
|
2345
|
+
const cellIndex = cell.cellIndex;
|
|
2346
|
+
const rows = table.querySelectorAll("tr");
|
|
2347
|
+
rows.forEach((row) => {
|
|
2348
|
+
const tag = row.parentElement?.tagName === "THEAD" ? "th" : "td";
|
|
2349
|
+
const newCell = document.createElement(tag);
|
|
2350
|
+
newCell.innerHTML = "<br>";
|
|
2351
|
+
const refCell = row.cells[cellIndex];
|
|
2352
|
+
if (!refCell) {
|
|
2353
|
+
row.appendChild(newCell);
|
|
2354
|
+
}
|
|
2355
|
+
else if (position === "left") {
|
|
2356
|
+
row.insertBefore(newCell, refCell);
|
|
2357
|
+
}
|
|
2358
|
+
else {
|
|
2359
|
+
row.insertBefore(newCell, refCell.nextSibling);
|
|
2360
|
+
}
|
|
2361
|
+
});
|
|
2362
|
+
}
|
|
2363
|
+
/** Delete the column that contains the currently selected cell. */
|
|
2364
|
+
function deleteColumn() {
|
|
2365
|
+
const cell = getActiveCell();
|
|
2366
|
+
const table = getActiveTable();
|
|
2367
|
+
if (!cell || !table)
|
|
2368
|
+
return;
|
|
2369
|
+
const cellIndex = cell.cellIndex;
|
|
2370
|
+
const colCount = getColumnCount(table);
|
|
2371
|
+
if (colCount <= 1) {
|
|
2372
|
+
// Last column — remove the whole table
|
|
2373
|
+
deleteTable();
|
|
2374
|
+
return;
|
|
2375
|
+
}
|
|
2376
|
+
const rows = table.querySelectorAll("tr");
|
|
2377
|
+
rows.forEach((row) => {
|
|
2378
|
+
if (row.cells[cellIndex]) {
|
|
2379
|
+
row.cells[cellIndex].remove();
|
|
2380
|
+
}
|
|
2381
|
+
});
|
|
2382
|
+
// Focus an adjacent cell
|
|
2383
|
+
const activeRow = getActiveRow();
|
|
2384
|
+
if (activeRow) {
|
|
2385
|
+
const idx = Math.min(cellIndex, activeRow.cells.length - 1);
|
|
2386
|
+
if (activeRow.cells[idx])
|
|
2387
|
+
focusCell(activeRow.cells[idx]);
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
/* ── Table delete ─────────────────────────────────────────────────────── */
|
|
2391
|
+
/** Remove the entire table and place cursor after it. */
|
|
2392
|
+
function deleteTable() {
|
|
2393
|
+
const table = getActiveTable();
|
|
2394
|
+
if (!table)
|
|
2395
|
+
return;
|
|
2396
|
+
const parent = table.parentNode;
|
|
2397
|
+
const nextSibling = table.nextSibling;
|
|
2398
|
+
// Create a paragraph to place the cursor
|
|
2399
|
+
const p = document.createElement("p");
|
|
2400
|
+
p.innerHTML = "<br>";
|
|
2401
|
+
if (nextSibling) {
|
|
2402
|
+
parent?.insertBefore(p, nextSibling);
|
|
2403
|
+
}
|
|
2404
|
+
else {
|
|
2405
|
+
parent?.appendChild(p);
|
|
2406
|
+
}
|
|
2407
|
+
table.remove();
|
|
2408
|
+
// Focus the new paragraph
|
|
2409
|
+
const range = document.createRange();
|
|
2410
|
+
const sel = document.getSelection();
|
|
2411
|
+
if (sel && p.firstChild) {
|
|
2412
|
+
range.setStart(p, 0);
|
|
2413
|
+
range.collapse(true);
|
|
2414
|
+
sel.removeAllRanges();
|
|
2415
|
+
sel.addRange(range);
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
/* ── Tab navigation ───────────────────────────────────────────────────── */
|
|
2419
|
+
/**
|
|
2420
|
+
* Move focus to the next or previous table cell.
|
|
2421
|
+
* Returns true if navigation happened (caller should preventDefault).
|
|
2422
|
+
*/
|
|
2423
|
+
function navigateTableCell(direction) {
|
|
2424
|
+
const cell = getActiveCell();
|
|
2425
|
+
if (!cell)
|
|
2426
|
+
return false;
|
|
2427
|
+
const row = cell.closest("tr");
|
|
2428
|
+
const table = cell.closest("table");
|
|
2429
|
+
if (!row || !table)
|
|
2430
|
+
return false;
|
|
2431
|
+
const cellIndex = cell.cellIndex;
|
|
2432
|
+
const allRows = Array.from(table.querySelectorAll("tr"));
|
|
2433
|
+
const rowIndex = allRows.indexOf(row);
|
|
2434
|
+
let targetCell = null;
|
|
2435
|
+
if (direction === "next") {
|
|
2436
|
+
if (cellIndex < row.cells.length - 1) {
|
|
2437
|
+
targetCell = row.cells[cellIndex + 1];
|
|
2438
|
+
}
|
|
2439
|
+
else if (rowIndex < allRows.length - 1) {
|
|
2440
|
+
targetCell = allRows[rowIndex + 1].cells[0];
|
|
2441
|
+
}
|
|
2442
|
+
else {
|
|
2443
|
+
// Last cell of last row — add a new row
|
|
2444
|
+
insertRow("below");
|
|
2445
|
+
return true;
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
else {
|
|
2449
|
+
if (cellIndex > 0) {
|
|
2450
|
+
targetCell = row.cells[cellIndex - 1];
|
|
2451
|
+
}
|
|
2452
|
+
else if (rowIndex > 0) {
|
|
2453
|
+
const prevRow = allRows[rowIndex - 1];
|
|
2454
|
+
targetCell = prevRow.cells[prevRow.cells.length - 1];
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
if (targetCell) {
|
|
2458
|
+
focusCell(targetCell);
|
|
2459
|
+
return true;
|
|
2460
|
+
}
|
|
2461
|
+
return false;
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
/**
|
|
2465
|
+
* Hook that sets up input, keyup, and keydown event listeners on the editor.
|
|
2466
|
+
*/
|
|
2467
|
+
function useEditorEvents({ editorRef, historyRef, isUpdatingRef, notifyChange, handleCheckboxKeyDown, handleCheckboxEnter, undo, redo, }) {
|
|
2468
|
+
useEffect(() => {
|
|
2469
|
+
const editor = editorRef.current;
|
|
2470
|
+
if (!editor)
|
|
2471
|
+
return;
|
|
2472
|
+
let inputTimeout = null;
|
|
2473
|
+
const handleInput = () => {
|
|
2474
|
+
setTimeout(() => {
|
|
2475
|
+
const content = domToContent(editor);
|
|
2476
|
+
notifyChange(content);
|
|
2477
|
+
if (inputTimeout)
|
|
2478
|
+
clearTimeout(inputTimeout);
|
|
2479
|
+
inputTimeout = setTimeout(() => {
|
|
2480
|
+
historyRef.current.push(content);
|
|
2481
|
+
inputTimeout = null;
|
|
2482
|
+
}, HISTORY_DEBOUNCE_MS);
|
|
2483
|
+
}, 0);
|
|
2484
|
+
};
|
|
2485
|
+
const handleKeyDown = (e) => {
|
|
2486
|
+
const isModifierPressed = e.metaKey || e.ctrlKey;
|
|
2487
|
+
// Checkbox Enter: create new checkbox item
|
|
2488
|
+
if (handleCheckboxEnter(e))
|
|
2489
|
+
return;
|
|
2490
|
+
// Checkbox keyboard navigation
|
|
2491
|
+
if (handleCheckboxKeyDown(e))
|
|
2492
|
+
return;
|
|
2493
|
+
// Tab: navigate table cells OR indent/outdent in lists
|
|
2494
|
+
if (e.key === "Tab" && !isModifierPressed && !e.altKey) {
|
|
2495
|
+
// Table tab navigation takes priority
|
|
2496
|
+
if (getActiveCell()) {
|
|
2497
|
+
e.preventDefault();
|
|
2498
|
+
e.stopPropagation();
|
|
2499
|
+
e.stopImmediatePropagation();
|
|
2500
|
+
navigateTableCell(e.shiftKey ? "prev" : "next");
|
|
2501
|
+
setTimeout(() => {
|
|
2502
|
+
if (editor) {
|
|
2503
|
+
const content = domToContent(editor);
|
|
2504
|
+
notifyChange(content);
|
|
2505
|
+
}
|
|
2506
|
+
}, 0);
|
|
2507
|
+
return;
|
|
2508
|
+
}
|
|
2509
|
+
e.preventDefault();
|
|
2510
|
+
e.stopPropagation();
|
|
2511
|
+
e.stopImmediatePropagation();
|
|
2512
|
+
const selection = window.getSelection();
|
|
2513
|
+
if (!selection || selection.rangeCount === 0)
|
|
2514
|
+
return;
|
|
2515
|
+
const range = selection.getRangeAt(0);
|
|
2516
|
+
const container = range.commonAncestorContainer;
|
|
2517
|
+
if (!editor.contains(container))
|
|
2518
|
+
return;
|
|
2519
|
+
const listItem = container.nodeType === Node.TEXT_NODE
|
|
2520
|
+
? container.parentElement?.closest("li")
|
|
2521
|
+
: container.closest("li");
|
|
2522
|
+
if (listItem && editor.contains(listItem)) {
|
|
2523
|
+
const currentContent = domToContent(editor);
|
|
2524
|
+
historyRef.current.push(currentContent);
|
|
2525
|
+
if (e.shiftKey) {
|
|
2526
|
+
outdentListItem(selection);
|
|
2527
|
+
}
|
|
2528
|
+
else {
|
|
2529
|
+
indentListItem(selection);
|
|
2530
|
+
}
|
|
2531
|
+
setTimeout(() => {
|
|
2532
|
+
if (editor) {
|
|
2533
|
+
const content = domToContent(editor);
|
|
2534
|
+
notifyChange(content);
|
|
2535
|
+
}
|
|
2536
|
+
}, 0);
|
|
2537
|
+
return;
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
// Cmd/Ctrl+K: trigger link button click (if present in toolbar)
|
|
2541
|
+
if (isModifierPressed && e.key === "k") {
|
|
2542
|
+
e.preventDefault();
|
|
2543
|
+
e.stopPropagation();
|
|
2544
|
+
// Find and click the link button in the toolbar
|
|
2545
|
+
const editorContainer = editor.closest(".rte-container");
|
|
2546
|
+
if (editorContainer) {
|
|
2547
|
+
const linkBtn = editorContainer.querySelector('button[aria-label="Link"], button[aria-label="Link einfügen"]');
|
|
2548
|
+
if (linkBtn)
|
|
2549
|
+
linkBtn.click();
|
|
2550
|
+
}
|
|
2551
|
+
return;
|
|
2552
|
+
}
|
|
2553
|
+
// Undo/Redo shortcuts
|
|
2554
|
+
if (isModifierPressed && e.key === "z" && !e.shiftKey) {
|
|
2555
|
+
e.preventDefault();
|
|
2556
|
+
e.stopPropagation();
|
|
2557
|
+
undo();
|
|
2558
|
+
}
|
|
2559
|
+
else if (isModifierPressed &&
|
|
2560
|
+
(e.key === "y" || (e.key === "z" && e.shiftKey))) {
|
|
2561
|
+
e.preventDefault();
|
|
2562
|
+
e.stopPropagation();
|
|
2563
|
+
redo();
|
|
2564
|
+
}
|
|
2565
|
+
};
|
|
2566
|
+
editor.addEventListener("input", handleInput);
|
|
2567
|
+
editor.addEventListener("keyup", handleInput);
|
|
2568
|
+
editor.addEventListener("keydown", handleKeyDown, true);
|
|
2569
|
+
return () => {
|
|
2570
|
+
editor.removeEventListener("input", handleInput);
|
|
2571
|
+
editor.removeEventListener("keyup", handleInput);
|
|
2572
|
+
editor.removeEventListener("keydown", handleKeyDown, true);
|
|
2573
|
+
if (inputTimeout)
|
|
2574
|
+
clearTimeout(inputTimeout);
|
|
2575
|
+
};
|
|
2576
|
+
}, [
|
|
2577
|
+
editorRef,
|
|
2578
|
+
historyRef,
|
|
2579
|
+
isUpdatingRef,
|
|
2580
|
+
notifyChange,
|
|
2581
|
+
handleCheckboxKeyDown,
|
|
2582
|
+
handleCheckboxEnter,
|
|
2583
|
+
undo,
|
|
2584
|
+
redo,
|
|
2585
|
+
]);
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
/**
|
|
2589
|
+
* Hook that initializes the editor with initial content and sets up the MutationObserver.
|
|
2590
|
+
* Runs once on mount.
|
|
2591
|
+
*/
|
|
2592
|
+
function useEditorInit({ editorRef, historyRef, isUpdatingRef, initialContent, customLinkComponent, customHeadingRenderer, }) {
|
|
2593
|
+
const isInitializedRef = useRef(false);
|
|
2594
|
+
useEffect(() => {
|
|
2595
|
+
const editor = editorRef.current;
|
|
2596
|
+
if (!editor || isInitializedRef.current)
|
|
2597
|
+
return;
|
|
2598
|
+
// Load initial content
|
|
2599
|
+
const content = initialContent || createEmptyContent();
|
|
2600
|
+
isUpdatingRef.current = true;
|
|
2601
|
+
contentToDOM(content, editor, customLinkComponent);
|
|
2602
|
+
historyRef.current.push(content);
|
|
2603
|
+
isUpdatingRef.current = false;
|
|
2604
|
+
isInitializedRef.current = true;
|
|
2605
|
+
// Normalize all checkbox lists (own + Lexical + GitHub formats)
|
|
2606
|
+
ensureAllCheckboxes(editor);
|
|
2607
|
+
// MutationObserver: ensure new checkbox list items get correct attributes
|
|
2608
|
+
const observer = new MutationObserver((mutations) => {
|
|
2609
|
+
if (isUpdatingRef.current)
|
|
2610
|
+
return;
|
|
2611
|
+
let needsUpdate = false;
|
|
2612
|
+
for (const mutation of mutations) {
|
|
2613
|
+
for (let i = 0; i < mutation.addedNodes.length; i++) {
|
|
2614
|
+
const node = mutation.addedNodes[i];
|
|
2615
|
+
if (node.nodeType !== Node.ELEMENT_NODE)
|
|
2616
|
+
continue;
|
|
2617
|
+
const el = node;
|
|
2618
|
+
if (el.tagName === "LI" &&
|
|
2619
|
+
el.closest("ul.rte-checkbox-list")) {
|
|
2620
|
+
needsUpdate = true;
|
|
2621
|
+
break;
|
|
2622
|
+
}
|
|
2623
|
+
if (el.tagName === "UL" &&
|
|
2624
|
+
el.classList.contains("rte-checkbox-list")) {
|
|
2625
|
+
needsUpdate = true;
|
|
2626
|
+
break;
|
|
2627
|
+
}
|
|
2628
|
+
if (el.querySelector("ul.rte-checkbox-list li")) {
|
|
2629
|
+
needsUpdate = true;
|
|
2630
|
+
break;
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
if (needsUpdate)
|
|
2634
|
+
break;
|
|
2635
|
+
}
|
|
2636
|
+
if (needsUpdate) {
|
|
2637
|
+
setTimeout(() => ensureAllCheckboxes(editor), 0);
|
|
2638
|
+
}
|
|
2639
|
+
});
|
|
2640
|
+
observer.observe(editor, { childList: true, subtree: true });
|
|
2641
|
+
return () => {
|
|
2642
|
+
observer.disconnect();
|
|
2643
|
+
};
|
|
2644
|
+
// Only run once on mount
|
|
2645
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2646
|
+
}, []);
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
/**
|
|
2650
|
+
* Hook for editor selection management.
|
|
2651
|
+
*/
|
|
2652
|
+
function useEditorSelection() {
|
|
2653
|
+
const restoreSelection = useCallback((editor) => {
|
|
2654
|
+
if (typeof window === "undefined" ||
|
|
2655
|
+
typeof document === "undefined")
|
|
2656
|
+
return;
|
|
2657
|
+
const range = document.createRange();
|
|
2658
|
+
const selection = window.getSelection();
|
|
2659
|
+
if (editor.firstChild) {
|
|
2660
|
+
range.setStart(editor.firstChild, 0);
|
|
2661
|
+
range.collapse(true);
|
|
2662
|
+
selection?.removeAllRanges();
|
|
2663
|
+
selection?.addRange(range);
|
|
2664
|
+
}
|
|
2665
|
+
}, []);
|
|
2666
|
+
return { restoreSelection };
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
const Toolbar = ({ plugins, editorAPI, className, }) => {
|
|
2670
|
+
const [updateTrigger, setUpdateTrigger] = useState(0);
|
|
2671
|
+
const [isClient, setIsClient] = useState(false);
|
|
2672
|
+
useEffect(() => {
|
|
2673
|
+
setIsClient(true);
|
|
2674
|
+
const handleSelectionChange = () => {
|
|
2675
|
+
setUpdateTrigger((prev) => prev + 1);
|
|
2676
|
+
};
|
|
2677
|
+
const handleMouseUp = () => {
|
|
2678
|
+
setTimeout(handleSelectionChange, 10);
|
|
2679
|
+
};
|
|
2680
|
+
const handleKeyUp = () => {
|
|
2681
|
+
setTimeout(handleSelectionChange, 10);
|
|
2682
|
+
};
|
|
2683
|
+
if (typeof document !== 'undefined') {
|
|
2684
|
+
document.addEventListener("selectionchange", handleSelectionChange);
|
|
2685
|
+
document.addEventListener("mouseup", handleMouseUp);
|
|
2686
|
+
document.addEventListener("keyup", handleKeyUp);
|
|
2687
|
+
}
|
|
2688
|
+
return () => {
|
|
2689
|
+
if (typeof document !== 'undefined') {
|
|
2690
|
+
document.removeEventListener("selectionchange", handleSelectionChange);
|
|
2691
|
+
document.removeEventListener("mouseup", handleMouseUp);
|
|
2692
|
+
document.removeEventListener("keyup", handleKeyUp);
|
|
2693
|
+
}
|
|
2694
|
+
};
|
|
2695
|
+
}, []);
|
|
2696
|
+
const handlePluginClick = (plugin, value) => {
|
|
2697
|
+
if (plugin.canExecute?.(editorAPI) !== false) {
|
|
2698
|
+
if (plugin.execute) {
|
|
2699
|
+
plugin.execute(editorAPI, value);
|
|
2700
|
+
}
|
|
2701
|
+
else if (plugin.command && value !== undefined) {
|
|
2702
|
+
editorAPI.executeCommand(plugin.command, value);
|
|
2703
|
+
}
|
|
2704
|
+
else if (plugin.command) {
|
|
2705
|
+
editorAPI.executeCommand(plugin.command);
|
|
2706
|
+
}
|
|
2707
|
+
setTimeout(() => setUpdateTrigger((prev) => prev + 1), 50);
|
|
2708
|
+
}
|
|
2709
|
+
};
|
|
2710
|
+
const leftPlugins = plugins.filter((p) => p.name !== "clearFormatting");
|
|
2711
|
+
const clearFormattingPlugin = plugins.find((p) => p.name === "clearFormatting");
|
|
2712
|
+
return (jsxs("div", { className: `rte-toolbar rte-toolbar-sticky ${className || ""}`, children: [jsx("div", { className: "rte-toolbar-left", children: leftPlugins.map((plugin) => {
|
|
2713
|
+
if (!plugin.renderButton)
|
|
2714
|
+
return null;
|
|
2715
|
+
const isActive = isClient && plugin.isActive
|
|
2716
|
+
? plugin.isActive(editorAPI)
|
|
2717
|
+
: false;
|
|
2718
|
+
const canExecute = isClient && plugin.canExecute
|
|
2719
|
+
? plugin.canExecute(editorAPI)
|
|
2720
|
+
: true;
|
|
2721
|
+
const currentValue = isClient && plugin.getCurrentValue
|
|
2722
|
+
? plugin.getCurrentValue(editorAPI)
|
|
2723
|
+
: undefined;
|
|
2724
|
+
const buttonProps = {
|
|
2725
|
+
isActive,
|
|
2726
|
+
onClick: () => handlePluginClick(plugin),
|
|
2727
|
+
disabled: !canExecute,
|
|
2728
|
+
onSelect: (value) => handlePluginClick(plugin, value),
|
|
2729
|
+
editorAPI,
|
|
2730
|
+
currentValue,
|
|
2731
|
+
};
|
|
2732
|
+
return (jsx(React.Fragment, { children: plugin.renderButton(buttonProps) }, plugin.name));
|
|
2733
|
+
}) }), clearFormattingPlugin && clearFormattingPlugin.renderButton && (jsxs("div", { className: "rte-toolbar-right", children: [jsx("div", { className: "rte-toolbar-divider" }), (() => {
|
|
2734
|
+
const isActive = isClient && clearFormattingPlugin.isActive
|
|
2735
|
+
? clearFormattingPlugin.isActive(editorAPI)
|
|
2736
|
+
: false;
|
|
2737
|
+
const canExecute = isClient && clearFormattingPlugin.canExecute
|
|
2738
|
+
? clearFormattingPlugin.canExecute(editorAPI)
|
|
2739
|
+
: true;
|
|
2740
|
+
const buttonProps = {
|
|
2741
|
+
isActive,
|
|
2742
|
+
onClick: () => handlePluginClick(clearFormattingPlugin),
|
|
2743
|
+
disabled: !canExecute,
|
|
2744
|
+
editorAPI,
|
|
2745
|
+
};
|
|
2746
|
+
return (jsx(React.Fragment, { children: clearFormattingPlugin.renderButton(buttonProps) }, clearFormattingPlugin.name));
|
|
2747
|
+
})()] }))] }));
|
|
2748
|
+
};
|
|
2749
|
+
|
|
2750
|
+
const Editor = ({ initialContent, onChange, plugins: providedPlugins, placeholder = "Text eingeben...", className, toolbarClassName, editorClassName, fontSizes, colors, headings, customLinkComponent, customHeadingRenderer, customRenderer, onEditorAPIReady, theme, onImageUpload, }) => {
|
|
2751
|
+
// --- Shared Refs ---
|
|
2752
|
+
const editorRef = useRef(null);
|
|
2753
|
+
const historyRef = useRef(new HistoryManager());
|
|
2754
|
+
const isUpdatingRef = useRef(false);
|
|
2755
|
+
// --- Plugins ---
|
|
2756
|
+
const plugins = useMemo(() => {
|
|
2757
|
+
const allPlugins = [...(providedPlugins || defaultPlugins)];
|
|
2758
|
+
if (headings && headings.length > 0) {
|
|
2759
|
+
const blockFormatIndex = allPlugins.findIndex((p) => p.name === "blockFormat");
|
|
2760
|
+
if (blockFormatIndex !== -1) {
|
|
2761
|
+
allPlugins[blockFormatIndex] =
|
|
2762
|
+
createBlockFormatPlugin(headings);
|
|
2763
|
+
}
|
|
2764
|
+
else {
|
|
2765
|
+
const redoIndex = allPlugins.findIndex((p) => p.name === "redo");
|
|
2766
|
+
if (redoIndex !== -1) {
|
|
2767
|
+
allPlugins.splice(redoIndex + 1, 0, createBlockFormatPlugin(headings));
|
|
2768
|
+
}
|
|
2769
|
+
else {
|
|
2770
|
+
allPlugins.push(createBlockFormatPlugin(headings));
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2774
|
+
if (fontSizes && fontSizes.length > 0) {
|
|
2775
|
+
const blockFormatIndex = allPlugins.findIndex((p) => p.name === "blockFormat");
|
|
2776
|
+
if (blockFormatIndex !== -1) {
|
|
2777
|
+
allPlugins.splice(blockFormatIndex + 1, 0, createFontSizePlugin(fontSizes));
|
|
2778
|
+
}
|
|
2779
|
+
else {
|
|
2780
|
+
allPlugins.push(createFontSizePlugin(fontSizes));
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
if (colors && colors.length > 0) {
|
|
2784
|
+
allPlugins.push(createTextColorPlugin(colors));
|
|
2785
|
+
allPlugins.push(createBackgroundColorPlugin(colors));
|
|
2786
|
+
}
|
|
2787
|
+
allPlugins.push(createImagePlugin(onImageUpload));
|
|
2788
|
+
return allPlugins;
|
|
2789
|
+
}, [providedPlugins, fontSizes, colors, headings, onImageUpload]);
|
|
2790
|
+
// --- Callbacks ---
|
|
2791
|
+
const notifyChange = useCallback((content) => {
|
|
2792
|
+
if (onChange)
|
|
2793
|
+
onChange(content);
|
|
2794
|
+
}, [onChange]);
|
|
2795
|
+
const getDomContent = useCallback(() => {
|
|
2796
|
+
const editor = editorRef.current;
|
|
2797
|
+
if (!editor)
|
|
2798
|
+
return createEmptyContent();
|
|
2799
|
+
return domToContent(editor);
|
|
2800
|
+
}, []);
|
|
2801
|
+
const pushToHistory = useCallback((content) => {
|
|
2802
|
+
historyRef.current.push(content);
|
|
2803
|
+
}, []);
|
|
2804
|
+
// --- Hooks ---
|
|
2805
|
+
const { restoreSelection } = useEditorSelection();
|
|
2806
|
+
const checkbox = useCheckbox({
|
|
2807
|
+
editorRef,
|
|
2808
|
+
isUpdatingRef,
|
|
2809
|
+
pushToHistory,
|
|
2810
|
+
notifyChange,
|
|
2811
|
+
getDomContent,
|
|
2812
|
+
});
|
|
2813
|
+
// --- Undo / Redo ---
|
|
2814
|
+
const undo = useCallback(() => {
|
|
2815
|
+
const content = historyRef.current.undo();
|
|
2816
|
+
const editor = editorRef.current;
|
|
2817
|
+
if (content && editor) {
|
|
2818
|
+
isUpdatingRef.current = true;
|
|
2819
|
+
contentToDOM(content, editor, customLinkComponent);
|
|
2820
|
+
restoreSelection(editor);
|
|
2821
|
+
isUpdatingRef.current = false;
|
|
2822
|
+
notifyChange(content);
|
|
2823
|
+
}
|
|
2824
|
+
}, [
|
|
2825
|
+
customLinkComponent,
|
|
2826
|
+
customHeadingRenderer,
|
|
2827
|
+
restoreSelection,
|
|
2828
|
+
notifyChange,
|
|
2829
|
+
]);
|
|
2830
|
+
const redo = useCallback(() => {
|
|
2831
|
+
const content = historyRef.current.redo();
|
|
2832
|
+
const editor = editorRef.current;
|
|
2833
|
+
if (content && editor) {
|
|
2834
|
+
isUpdatingRef.current = true;
|
|
2835
|
+
contentToDOM(content, editor, customLinkComponent);
|
|
2836
|
+
restoreSelection(editor);
|
|
2837
|
+
isUpdatingRef.current = false;
|
|
2838
|
+
notifyChange(content);
|
|
2839
|
+
}
|
|
2840
|
+
}, [
|
|
2841
|
+
customLinkComponent,
|
|
2842
|
+
customHeadingRenderer,
|
|
2843
|
+
restoreSelection,
|
|
2844
|
+
notifyChange,
|
|
2845
|
+
]);
|
|
2846
|
+
// --- Editor API ---
|
|
2847
|
+
const editorAPI = useMemo(() => {
|
|
2848
|
+
const executeCommand = (command, value) => {
|
|
2849
|
+
const editor = editorRef.current;
|
|
2850
|
+
if (!editor)
|
|
2851
|
+
return false;
|
|
2852
|
+
// Save history before non-history commands
|
|
2853
|
+
if (command !== "undo" &&
|
|
2854
|
+
command !== "redo" &&
|
|
2855
|
+
command !== "insertImage" &&
|
|
2856
|
+
command !== "insertCheckboxList") {
|
|
2857
|
+
const currentContent = domToContent(editor);
|
|
2858
|
+
historyRef.current.push(currentContent);
|
|
2859
|
+
}
|
|
2860
|
+
if (command === "undo") {
|
|
2861
|
+
undo();
|
|
2862
|
+
return true;
|
|
2863
|
+
}
|
|
2864
|
+
if (command === "redo") {
|
|
2865
|
+
redo();
|
|
2866
|
+
return true;
|
|
2867
|
+
}
|
|
2868
|
+
if (command === "insertCheckboxList") {
|
|
2869
|
+
return checkbox.insertCheckboxList(editor);
|
|
2870
|
+
}
|
|
2871
|
+
if (command === "insertImage" && value) {
|
|
2872
|
+
return handleInsertImage(editor, value, isUpdatingRef, historyRef, notifyChange);
|
|
2873
|
+
}
|
|
2874
|
+
// General commands via document.execCommand
|
|
2875
|
+
ensureEditorFocused(editor);
|
|
2876
|
+
document.execCommand(command, false, value);
|
|
2877
|
+
setTimeout(() => {
|
|
2878
|
+
if (editor && !isUpdatingRef.current) {
|
|
2879
|
+
ensureAllCheckboxes(editor);
|
|
2880
|
+
const content = domToContent(editor);
|
|
2881
|
+
notifyChange(content);
|
|
2882
|
+
}
|
|
2883
|
+
}, 0);
|
|
2884
|
+
return true;
|
|
2885
|
+
};
|
|
2886
|
+
return {
|
|
2887
|
+
executeCommand,
|
|
2888
|
+
getSelection: () => {
|
|
2889
|
+
if (typeof window === "undefined")
|
|
2890
|
+
return null;
|
|
1729
2891
|
return window.getSelection();
|
|
1730
2892
|
},
|
|
1731
2893
|
getContent: () => {
|
|
1732
2894
|
const editor = editorRef.current;
|
|
1733
2895
|
if (!editor)
|
|
1734
2896
|
return createEmptyContent();
|
|
2897
|
+
ensureAllCheckboxes(editor);
|
|
1735
2898
|
return domToContent(editor);
|
|
1736
2899
|
},
|
|
1737
2900
|
setContent: (content) => {
|
|
@@ -1751,23 +2914,20 @@ const Editor = ({ initialContent, onChange, plugins: providedPlugins, placeholde
|
|
|
1751
2914
|
const range = selection.getRangeAt(0);
|
|
1752
2915
|
const block = document.createElement(type);
|
|
1753
2916
|
if (attributes) {
|
|
1754
|
-
Object.entries(attributes).forEach(([key,
|
|
1755
|
-
block.setAttribute(key,
|
|
2917
|
+
Object.entries(attributes).forEach(([key, val]) => {
|
|
2918
|
+
block.setAttribute(key, val);
|
|
1756
2919
|
});
|
|
1757
2920
|
}
|
|
1758
2921
|
range.insertNode(block);
|
|
1759
|
-
const textNode = document.createTextNode("\u200B");
|
|
2922
|
+
const textNode = document.createTextNode("\u200B");
|
|
1760
2923
|
block.appendChild(textNode);
|
|
1761
|
-
// Cursor setzen
|
|
1762
2924
|
range.setStartAfter(textNode);
|
|
1763
2925
|
range.collapse(true);
|
|
1764
2926
|
selection.removeAllRanges();
|
|
1765
2927
|
selection.addRange(range);
|
|
1766
2928
|
const editor = editorRef.current;
|
|
1767
|
-
if (editor)
|
|
1768
|
-
|
|
1769
|
-
notifyChange(content);
|
|
1770
|
-
}
|
|
2929
|
+
if (editor)
|
|
2930
|
+
notifyChange(domToContent(editor));
|
|
1771
2931
|
},
|
|
1772
2932
|
insertInline: (type, attributes) => {
|
|
1773
2933
|
const selection = window.getSelection();
|
|
@@ -1776,282 +2936,120 @@ const Editor = ({ initialContent, onChange, plugins: providedPlugins, placeholde
|
|
|
1776
2936
|
const range = selection.getRangeAt(0);
|
|
1777
2937
|
const inline = document.createElement(type);
|
|
1778
2938
|
if (attributes) {
|
|
1779
|
-
Object.entries(attributes).forEach(([key,
|
|
1780
|
-
inline.setAttribute(key,
|
|
2939
|
+
Object.entries(attributes).forEach(([key, val]) => {
|
|
2940
|
+
inline.setAttribute(key, val);
|
|
1781
2941
|
});
|
|
1782
2942
|
}
|
|
1783
2943
|
try {
|
|
1784
2944
|
range.surroundContents(inline);
|
|
1785
2945
|
}
|
|
1786
|
-
catch (
|
|
1787
|
-
// Falls surroundContents fehlschlägt, versuche es anders
|
|
2946
|
+
catch (_) {
|
|
1788
2947
|
const contents = range.extractContents();
|
|
1789
2948
|
inline.appendChild(contents);
|
|
1790
2949
|
range.insertNode(inline);
|
|
1791
2950
|
}
|
|
1792
|
-
|
|
1793
|
-
range.
|
|
1794
|
-
|
|
1795
|
-
selection.
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
const editor = editorRef.current;
|
|
1818
|
-
if (editor)
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
},
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
setTimeout(() => {
|
|
1844
|
-
if (editor) {
|
|
1845
|
-
const content = domToContent(editor);
|
|
1846
|
-
notifyChange(content);
|
|
1847
|
-
}
|
|
1848
|
-
}, 0);
|
|
1849
|
-
}
|
|
1850
|
-
},
|
|
1851
|
-
clearTextColor: () => {
|
|
1852
|
-
const editor = editorRef.current;
|
|
1853
|
-
if (!editor)
|
|
1854
|
-
return;
|
|
1855
|
-
const selection = window.getSelection();
|
|
1856
|
-
if (selection && selection.rangeCount > 0) {
|
|
1857
|
-
const currentContent = domToContent(editor);
|
|
1858
|
-
historyRef.current.push(currentContent);
|
|
1859
|
-
clearTextColor(selection);
|
|
1860
|
-
setTimeout(() => {
|
|
1861
|
-
if (editor) {
|
|
1862
|
-
const content = domToContent(editor);
|
|
1863
|
-
notifyChange(content);
|
|
1864
|
-
}
|
|
1865
|
-
}, 0);
|
|
1866
|
-
}
|
|
1867
|
-
},
|
|
1868
|
-
clearBackgroundColor: () => {
|
|
1869
|
-
const editor = editorRef.current;
|
|
1870
|
-
if (!editor)
|
|
1871
|
-
return;
|
|
1872
|
-
const selection = window.getSelection();
|
|
1873
|
-
if (selection && selection.rangeCount > 0) {
|
|
1874
|
-
const currentContent = domToContent(editor);
|
|
1875
|
-
historyRef.current.push(currentContent);
|
|
1876
|
-
clearBackgroundColor(selection);
|
|
1877
|
-
setTimeout(() => {
|
|
1878
|
-
if (editor) {
|
|
1879
|
-
const content = domToContent(editor);
|
|
1880
|
-
notifyChange(content);
|
|
1881
|
-
}
|
|
1882
|
-
}, 0);
|
|
1883
|
-
}
|
|
1884
|
-
},
|
|
1885
|
-
clearFontSize: () => {
|
|
1886
|
-
const editor = editorRef.current;
|
|
1887
|
-
if (!editor)
|
|
1888
|
-
return;
|
|
1889
|
-
const selection = window.getSelection();
|
|
1890
|
-
if (selection && selection.rangeCount > 0) {
|
|
1891
|
-
const currentContent = domToContent(editor);
|
|
1892
|
-
historyRef.current.push(currentContent);
|
|
1893
|
-
clearFontSize(selection);
|
|
1894
|
-
setTimeout(() => {
|
|
1895
|
-
if (editor) {
|
|
1896
|
-
const content = domToContent(editor);
|
|
1897
|
-
notifyChange(content);
|
|
1898
|
-
}
|
|
1899
|
-
}, 0);
|
|
1900
|
-
}
|
|
1901
|
-
},
|
|
1902
|
-
clearLinks: () => {
|
|
1903
|
-
const editor = editorRef.current;
|
|
1904
|
-
if (!editor)
|
|
1905
|
-
return;
|
|
1906
|
-
const selection = window.getSelection();
|
|
1907
|
-
if (selection && selection.rangeCount > 0) {
|
|
1908
|
-
const currentContent = domToContent(editor);
|
|
1909
|
-
historyRef.current.push(currentContent);
|
|
1910
|
-
clearLinks(selection);
|
|
1911
|
-
setTimeout(() => {
|
|
1912
|
-
if (editor) {
|
|
1913
|
-
const content = domToContent(editor);
|
|
1914
|
-
notifyChange(content);
|
|
1915
|
-
}
|
|
1916
|
-
}, 0);
|
|
1917
|
-
}
|
|
1918
|
-
},
|
|
1919
|
-
indentListItem: () => {
|
|
1920
|
-
const editor = editorRef.current;
|
|
1921
|
-
if (!editor)
|
|
1922
|
-
return;
|
|
1923
|
-
const selection = window.getSelection();
|
|
1924
|
-
if (selection && selection.rangeCount > 0) {
|
|
1925
|
-
const currentContent = domToContent(editor);
|
|
1926
|
-
historyRef.current.push(currentContent);
|
|
1927
|
-
indentListItem(selection);
|
|
1928
|
-
setTimeout(() => {
|
|
1929
|
-
if (editor) {
|
|
1930
|
-
const content = domToContent(editor);
|
|
1931
|
-
notifyChange(content);
|
|
1932
|
-
}
|
|
1933
|
-
}, 0);
|
|
1934
|
-
}
|
|
1935
|
-
},
|
|
1936
|
-
outdentListItem: () => {
|
|
1937
|
-
const editor = editorRef.current;
|
|
1938
|
-
if (!editor)
|
|
1939
|
-
return;
|
|
1940
|
-
const selection = window.getSelection();
|
|
1941
|
-
if (selection && selection.rangeCount > 0) {
|
|
1942
|
-
const currentContent = domToContent(editor);
|
|
1943
|
-
historyRef.current.push(currentContent);
|
|
1944
|
-
outdentListItem(selection);
|
|
1945
|
-
setTimeout(() => {
|
|
1946
|
-
if (editor) {
|
|
1947
|
-
const content = domToContent(editor);
|
|
1948
|
-
notifyChange(content);
|
|
1949
|
-
}
|
|
1950
|
-
}, 0);
|
|
1951
|
-
}
|
|
1952
|
-
},
|
|
1953
|
-
};
|
|
1954
|
-
}, [
|
|
1955
|
-
notifyChange,
|
|
1956
|
-
restoreSelection,
|
|
1957
|
-
customLinkComponent,
|
|
1958
|
-
customHeadingRenderer,
|
|
1959
|
-
]);
|
|
1960
|
-
useEffect(() => {
|
|
1961
|
-
if (onEditorAPIReady) {
|
|
1962
|
-
onEditorAPIReady(editorAPI);
|
|
1963
|
-
}
|
|
1964
|
-
}, [editorAPI, onEditorAPIReady]);
|
|
1965
|
-
const isInitializedRef = useRef(false);
|
|
1966
|
-
useEffect(() => {
|
|
1967
|
-
const editor = editorRef.current;
|
|
1968
|
-
if (!editor || isInitializedRef.current)
|
|
1969
|
-
return;
|
|
1970
|
-
const content = initialContent || createEmptyContent();
|
|
1971
|
-
isUpdatingRef.current = true;
|
|
1972
|
-
contentToDOM(content, editor, customLinkComponent);
|
|
1973
|
-
historyRef.current.push(content);
|
|
1974
|
-
isUpdatingRef.current = false;
|
|
1975
|
-
isInitializedRef.current = true;
|
|
1976
|
-
let inputTimeout = null;
|
|
1977
|
-
const handleInput = () => {
|
|
1978
|
-
if (isUpdatingRef.current)
|
|
1979
|
-
return;
|
|
1980
|
-
const content = domToContent(editor);
|
|
1981
|
-
notifyChange(content);
|
|
1982
|
-
if (inputTimeout) {
|
|
1983
|
-
clearTimeout(inputTimeout);
|
|
1984
|
-
}
|
|
1985
|
-
inputTimeout = setTimeout(() => {
|
|
1986
|
-
historyRef.current.push(content);
|
|
1987
|
-
inputTimeout = null;
|
|
1988
|
-
}, 300);
|
|
1989
|
-
};
|
|
1990
|
-
const handleKeyDown = (e) => {
|
|
1991
|
-
const isModifierPressed = e.metaKey || e.ctrlKey;
|
|
1992
|
-
if (e.key === "Tab" && !isModifierPressed && !e.altKey) {
|
|
1993
|
-
// Immer preventDefault aufrufen (wie Lexical), damit Tab den Fokus nicht aus dem Editor entfernt
|
|
1994
|
-
e.preventDefault();
|
|
1995
|
-
e.stopPropagation();
|
|
1996
|
-
e.stopImmediatePropagation();
|
|
1997
|
-
const selection = window.getSelection();
|
|
1998
|
-
if (!selection || selection.rangeCount === 0) {
|
|
1999
|
-
// Keine Selection: Tab verhindern, Fokus bleibt im Editor
|
|
2000
|
-
return;
|
|
2001
|
-
}
|
|
2002
|
-
const range = selection.getRangeAt(0);
|
|
2003
|
-
const container = range.commonAncestorContainer;
|
|
2004
|
-
if (!editor.contains(container)) {
|
|
2005
|
-
// Container nicht im Editor: Tab verhindern
|
|
2006
|
-
return;
|
|
2007
|
-
}
|
|
2008
|
-
// Prüfe ob wir in einer Liste sind
|
|
2009
|
-
const listItem = container.nodeType === Node.TEXT_NODE
|
|
2010
|
-
? container.parentElement?.closest("li")
|
|
2011
|
-
: container.closest("li");
|
|
2012
|
-
if (listItem && editor.contains(listItem)) {
|
|
2013
|
-
// In Liste: Indent/Outdent durchführen
|
|
2014
|
-
const currentContent = domToContent(editor);
|
|
2015
|
-
historyRef.current.push(currentContent);
|
|
2016
|
-
if (e.shiftKey) {
|
|
2017
|
-
outdentListItem(selection);
|
|
2018
|
-
}
|
|
2019
|
-
else {
|
|
2020
|
-
indentListItem(selection);
|
|
2021
|
-
}
|
|
2022
|
-
setTimeout(() => {
|
|
2023
|
-
if (editor) {
|
|
2024
|
-
const content = domToContent(editor);
|
|
2025
|
-
notifyChange(content);
|
|
2026
|
-
}
|
|
2027
|
-
}, 0);
|
|
2028
|
-
return;
|
|
2029
|
-
}
|
|
2030
|
-
// Nicht in Liste: Tab verhindern, aber kein Tab-Zeichen einfügen
|
|
2031
|
-
// Der Fokus bleibt im Editor (durch preventDefault)
|
|
2032
|
-
}
|
|
2033
|
-
if (isModifierPressed && e.key === "z" && !e.shiftKey) {
|
|
2034
|
-
e.preventDefault();
|
|
2035
|
-
e.stopPropagation();
|
|
2036
|
-
editorAPI.undo();
|
|
2037
|
-
}
|
|
2038
|
-
else if (isModifierPressed &&
|
|
2039
|
-
(e.key === "y" || (e.key === "z" && e.shiftKey))) {
|
|
2040
|
-
e.preventDefault();
|
|
2041
|
-
e.stopPropagation();
|
|
2042
|
-
editorAPI.redo();
|
|
2043
|
-
}
|
|
2044
|
-
};
|
|
2045
|
-
editor.addEventListener("input", handleInput);
|
|
2046
|
-
editor.addEventListener("keydown", handleKeyDown, true);
|
|
2047
|
-
return () => {
|
|
2048
|
-
editor.removeEventListener("input", handleInput);
|
|
2049
|
-
editor.removeEventListener("keydown", handleKeyDown, true);
|
|
2050
|
-
if (inputTimeout) {
|
|
2051
|
-
clearTimeout(inputTimeout);
|
|
2052
|
-
}
|
|
2951
|
+
range.setStartAfter(inline);
|
|
2952
|
+
range.collapse(true);
|
|
2953
|
+
selection.removeAllRanges();
|
|
2954
|
+
selection.addRange(range);
|
|
2955
|
+
const editor = editorRef.current;
|
|
2956
|
+
if (editor)
|
|
2957
|
+
notifyChange(domToContent(editor));
|
|
2958
|
+
},
|
|
2959
|
+
undo: () => undo(),
|
|
2960
|
+
redo: () => redo(),
|
|
2961
|
+
canUndo: () => historyRef.current.canUndo(),
|
|
2962
|
+
canRedo: () => historyRef.current.canRedo(),
|
|
2963
|
+
importHtml: (htmlString) => {
|
|
2964
|
+
const content = htmlToContent(htmlString);
|
|
2965
|
+
const editor = editorRef.current;
|
|
2966
|
+
if (editor) {
|
|
2967
|
+
isUpdatingRef.current = true;
|
|
2968
|
+
contentToDOM(content, editor, customLinkComponent);
|
|
2969
|
+
historyRef.current.push(content);
|
|
2970
|
+
isUpdatingRef.current = false;
|
|
2971
|
+
notifyChange(content);
|
|
2972
|
+
}
|
|
2973
|
+
return content;
|
|
2974
|
+
},
|
|
2975
|
+
exportHtml: () => {
|
|
2976
|
+
const editor = editorRef.current;
|
|
2977
|
+
if (!editor)
|
|
2978
|
+
return "";
|
|
2979
|
+
return contentToHTML(domToContent(editor));
|
|
2980
|
+
},
|
|
2981
|
+
clearFormatting: () => {
|
|
2982
|
+
executeWithHistory((selection) => clearFormatting(selection));
|
|
2983
|
+
},
|
|
2984
|
+
clearTextColor: () => {
|
|
2985
|
+
executeWithHistory((selection) => clearTextColor(selection));
|
|
2986
|
+
},
|
|
2987
|
+
clearBackgroundColor: () => {
|
|
2988
|
+
executeWithHistory((selection) => clearBackgroundColor(selection));
|
|
2989
|
+
},
|
|
2990
|
+
clearFontSize: () => {
|
|
2991
|
+
executeWithHistory((selection) => clearFontSize(selection));
|
|
2992
|
+
},
|
|
2993
|
+
clearLinks: () => {
|
|
2994
|
+
executeWithHistory((selection) => clearLinks(selection));
|
|
2995
|
+
},
|
|
2996
|
+
indentListItem: () => {
|
|
2997
|
+
executeWithHistory((selection) => indentListItem(selection));
|
|
2998
|
+
},
|
|
2999
|
+
outdentListItem: () => {
|
|
3000
|
+
executeWithHistory((selection) => outdentListItem(selection));
|
|
3001
|
+
},
|
|
2053
3002
|
};
|
|
2054
|
-
|
|
3003
|
+
/** Helper: push history, execute operation, then notify change. */
|
|
3004
|
+
function executeWithHistory(operation) {
|
|
3005
|
+
const editor = editorRef.current;
|
|
3006
|
+
if (!editor)
|
|
3007
|
+
return;
|
|
3008
|
+
const selection = window.getSelection();
|
|
3009
|
+
if (!selection || selection.rangeCount === 0)
|
|
3010
|
+
return;
|
|
3011
|
+
const currentContent = domToContent(editor);
|
|
3012
|
+
historyRef.current.push(currentContent);
|
|
3013
|
+
operation(selection);
|
|
3014
|
+
setTimeout(() => {
|
|
3015
|
+
if (editor)
|
|
3016
|
+
notifyChange(domToContent(editor));
|
|
3017
|
+
}, 0);
|
|
3018
|
+
}
|
|
3019
|
+
}, [
|
|
3020
|
+
undo,
|
|
3021
|
+
redo,
|
|
3022
|
+
checkbox,
|
|
3023
|
+
notifyChange,
|
|
3024
|
+
customLinkComponent,
|
|
3025
|
+
customHeadingRenderer,
|
|
3026
|
+
]);
|
|
3027
|
+
// --- Initialize editor ---
|
|
3028
|
+
useEditorInit({
|
|
3029
|
+
editorRef,
|
|
3030
|
+
historyRef,
|
|
3031
|
+
isUpdatingRef,
|
|
3032
|
+
initialContent,
|
|
3033
|
+
customLinkComponent,
|
|
3034
|
+
customHeadingRenderer,
|
|
3035
|
+
});
|
|
3036
|
+
// --- Set up event listeners ---
|
|
3037
|
+
useEditorEvents({
|
|
3038
|
+
editorRef,
|
|
3039
|
+
historyRef,
|
|
3040
|
+
isUpdatingRef,
|
|
3041
|
+
notifyChange,
|
|
3042
|
+
handleCheckboxKeyDown: checkbox.handleCheckboxKeyDown,
|
|
3043
|
+
handleCheckboxEnter: checkbox.handleCheckboxEnter,
|
|
3044
|
+
undo,
|
|
3045
|
+
redo,
|
|
3046
|
+
});
|
|
3047
|
+
// --- Expose editor API ---
|
|
3048
|
+
useEffect(() => {
|
|
3049
|
+
if (onEditorAPIReady)
|
|
3050
|
+
onEditorAPIReady(editorAPI);
|
|
3051
|
+
}, [editorAPI, onEditorAPIReady]);
|
|
3052
|
+
// --- Paste handler ---
|
|
2055
3053
|
const handlePaste = (e) => {
|
|
2056
3054
|
e.preventDefault();
|
|
2057
3055
|
const html = e.clipboardData.getData("text/html");
|
|
@@ -2079,11 +3077,10 @@ const Editor = ({ initialContent, onChange, plugins: providedPlugins, placeholde
|
|
|
2079
3077
|
}
|
|
2080
3078
|
selection.removeAllRanges();
|
|
2081
3079
|
selection.addRange(range);
|
|
2082
|
-
|
|
2083
|
-
notifyChange(content);
|
|
3080
|
+
notifyChange(domToContent(editor));
|
|
2084
3081
|
}
|
|
2085
3082
|
}
|
|
2086
|
-
catch (
|
|
3083
|
+
catch (_) {
|
|
2087
3084
|
document.execCommand("insertText", false, text);
|
|
2088
3085
|
}
|
|
2089
3086
|
}
|
|
@@ -2091,6 +3088,7 @@ const Editor = ({ initialContent, onChange, plugins: providedPlugins, placeholde
|
|
|
2091
3088
|
document.execCommand("insertText", false, text);
|
|
2092
3089
|
}
|
|
2093
3090
|
};
|
|
3091
|
+
// --- Theme styles ---
|
|
2094
3092
|
const containerStyle = theme
|
|
2095
3093
|
? {
|
|
2096
3094
|
...(theme.borderColor &&
|
|
@@ -2121,6 +3119,131 @@ const Editor = ({ initialContent, onChange, plugins: providedPlugins, placeholde
|
|
|
2121
3119
|
: {};
|
|
2122
3120
|
return (jsxs("div", { className: `rte-container ${className || ""}`, style: containerStyle, children: [jsx(Toolbar, { plugins: plugins, editorAPI: editorAPI, className: toolbarClassName }), jsx("div", { ref: editorRef, contentEditable: true, className: `rte-editor ${editorClassName || ""}`, "data-placeholder": placeholder, onPaste: handlePaste, suppressContentEditableWarning: true })] }));
|
|
2123
3121
|
};
|
|
3122
|
+
// --- Helper: Insert Image ---
|
|
3123
|
+
function handleInsertImage(editor, value, isUpdatingRef, historyRef, notifyChange) {
|
|
3124
|
+
let selection = window.getSelection();
|
|
3125
|
+
if (!selection)
|
|
3126
|
+
return false;
|
|
3127
|
+
if (document.activeElement !== editor) {
|
|
3128
|
+
editor.focus();
|
|
3129
|
+
}
|
|
3130
|
+
if (selection.rangeCount === 0) {
|
|
3131
|
+
const range = document.createRange();
|
|
3132
|
+
if (editor.childNodes.length > 0) {
|
|
3133
|
+
const lastChild = editor.childNodes[editor.childNodes.length - 1];
|
|
3134
|
+
range.setStartAfter(lastChild);
|
|
3135
|
+
range.collapse(true);
|
|
3136
|
+
}
|
|
3137
|
+
else {
|
|
3138
|
+
const img = createImageElement(value);
|
|
3139
|
+
editor.appendChild(img);
|
|
3140
|
+
const newRange = document.createRange();
|
|
3141
|
+
newRange.setStartAfter(img);
|
|
3142
|
+
newRange.collapse(true);
|
|
3143
|
+
selection.removeAllRanges();
|
|
3144
|
+
selection.addRange(newRange);
|
|
3145
|
+
saveAndNotify(editor, isUpdatingRef, historyRef, notifyChange);
|
|
3146
|
+
return true;
|
|
3147
|
+
}
|
|
3148
|
+
selection.removeAllRanges();
|
|
3149
|
+
selection.addRange(range);
|
|
3150
|
+
}
|
|
3151
|
+
if (selection.rangeCount === 0)
|
|
3152
|
+
return false;
|
|
3153
|
+
const range = selection.getRangeAt(0);
|
|
3154
|
+
const container = range.commonAncestorContainer;
|
|
3155
|
+
let parentElement = null;
|
|
3156
|
+
if (container.nodeType === Node.TEXT_NODE) {
|
|
3157
|
+
parentElement = container.parentElement;
|
|
3158
|
+
}
|
|
3159
|
+
else if (container.nodeType === Node.ELEMENT_NODE) {
|
|
3160
|
+
parentElement = container;
|
|
3161
|
+
}
|
|
3162
|
+
const img = createImageElement(value);
|
|
3163
|
+
if (parentElement &&
|
|
3164
|
+
parentElement !== editor &&
|
|
3165
|
+
/^(P|DIV|H[1-6])$/.test(parentElement.tagName)) {
|
|
3166
|
+
if (parentElement.nextSibling) {
|
|
3167
|
+
editor.insertBefore(img, parentElement.nextSibling);
|
|
3168
|
+
}
|
|
3169
|
+
else {
|
|
3170
|
+
editor.appendChild(img);
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
else {
|
|
3174
|
+
try {
|
|
3175
|
+
range.insertNode(img);
|
|
3176
|
+
}
|
|
3177
|
+
catch (_) {
|
|
3178
|
+
editor.appendChild(img);
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
const newRange = document.createRange();
|
|
3182
|
+
newRange.setStartAfter(img);
|
|
3183
|
+
newRange.collapse(true);
|
|
3184
|
+
selection.removeAllRanges();
|
|
3185
|
+
selection.addRange(newRange);
|
|
3186
|
+
saveAndNotify(editor, isUpdatingRef, historyRef, notifyChange);
|
|
3187
|
+
return true;
|
|
3188
|
+
}
|
|
3189
|
+
function createImageElement(src) {
|
|
3190
|
+
const img = document.createElement("img");
|
|
3191
|
+
img.setAttribute("src", src);
|
|
3192
|
+
img.setAttribute("alt", "");
|
|
3193
|
+
img.style.maxWidth = "100%";
|
|
3194
|
+
img.style.height = "auto";
|
|
3195
|
+
img.style.display = "block";
|
|
3196
|
+
img.style.margin = "16px 0";
|
|
3197
|
+
return img;
|
|
3198
|
+
}
|
|
3199
|
+
function saveAndNotify(editor, isUpdatingRef, historyRef, notifyChange) {
|
|
3200
|
+
isUpdatingRef.current = true;
|
|
3201
|
+
setTimeout(() => {
|
|
3202
|
+
const content = domToContent(editor);
|
|
3203
|
+
historyRef.current.push(content);
|
|
3204
|
+
isUpdatingRef.current = false;
|
|
3205
|
+
notifyChange(content);
|
|
3206
|
+
}, 0);
|
|
3207
|
+
}
|
|
3208
|
+
/** Ensures the editor is focused and has a valid selection. */
|
|
3209
|
+
function ensureEditorFocused(editor) {
|
|
3210
|
+
const selection = window.getSelection();
|
|
3211
|
+
let savedRange = null;
|
|
3212
|
+
if (selection && selection.rangeCount > 0) {
|
|
3213
|
+
savedRange = selection.getRangeAt(0).cloneRange();
|
|
3214
|
+
}
|
|
3215
|
+
if (document.activeElement !== editor) {
|
|
3216
|
+
editor.focus();
|
|
3217
|
+
}
|
|
3218
|
+
if (!selection || selection.rangeCount === 0) {
|
|
3219
|
+
const range = document.createRange();
|
|
3220
|
+
if (editor.childNodes.length > 0) {
|
|
3221
|
+
const lastChild = editor.childNodes[editor.childNodes.length - 1];
|
|
3222
|
+
if (lastChild.nodeType === Node.TEXT_NODE) {
|
|
3223
|
+
range.setStart(lastChild, lastChild.textContent?.length || 0);
|
|
3224
|
+
range.setEnd(lastChild, lastChild.textContent?.length || 0);
|
|
3225
|
+
}
|
|
3226
|
+
else {
|
|
3227
|
+
range.selectNodeContents(lastChild);
|
|
3228
|
+
range.collapse(false);
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
else {
|
|
3232
|
+
const p = document.createElement("p");
|
|
3233
|
+
editor.appendChild(p);
|
|
3234
|
+
const textNode = document.createTextNode("");
|
|
3235
|
+
p.appendChild(textNode);
|
|
3236
|
+
range.setStart(textNode, 0);
|
|
3237
|
+
range.setEnd(textNode, 0);
|
|
3238
|
+
}
|
|
3239
|
+
selection?.removeAllRanges();
|
|
3240
|
+
selection?.addRange(range);
|
|
3241
|
+
}
|
|
3242
|
+
else if (savedRange) {
|
|
3243
|
+
selection.removeAllRanges();
|
|
3244
|
+
selection.addRange(savedRange);
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
2124
3247
|
|
|
2125
3248
|
const defaultHeadings = ["h1", "h2", "h3"];
|
|
2126
3249
|
const headingLabels = {
|
|
@@ -2349,5 +3472,465 @@ const orderedListPlugin = {
|
|
|
2349
3472
|
},
|
|
2350
3473
|
};
|
|
2351
3474
|
|
|
2352
|
-
|
|
3475
|
+
const alignmentLabels = {
|
|
3476
|
+
left: "Links",
|
|
3477
|
+
center: "Zentriert",
|
|
3478
|
+
right: "Rechts",
|
|
3479
|
+
justify: "Blocksatz",
|
|
3480
|
+
};
|
|
3481
|
+
const alignmentIcons = {
|
|
3482
|
+
left: "mdi:format-align-left",
|
|
3483
|
+
center: "mdi:format-align-center",
|
|
3484
|
+
right: "mdi:format-align-right",
|
|
3485
|
+
justify: "mdi:format-align-justify",
|
|
3486
|
+
};
|
|
3487
|
+
const alignmentCommands = {
|
|
3488
|
+
left: "justifyLeft",
|
|
3489
|
+
center: "justifyCenter",
|
|
3490
|
+
right: "justifyRight",
|
|
3491
|
+
justify: "justifyFull",
|
|
3492
|
+
};
|
|
3493
|
+
/**
|
|
3494
|
+
* Detects current text alignment at cursor position.
|
|
3495
|
+
*/
|
|
3496
|
+
function detectCurrentAlignment() {
|
|
3497
|
+
if (typeof document === "undefined")
|
|
3498
|
+
return undefined;
|
|
3499
|
+
const selection = document.getSelection();
|
|
3500
|
+
if (!selection || selection.rangeCount === 0)
|
|
3501
|
+
return undefined;
|
|
3502
|
+
const range = selection.getRangeAt(0);
|
|
3503
|
+
const container = range.commonAncestorContainer;
|
|
3504
|
+
const element = container.nodeType === Node.TEXT_NODE
|
|
3505
|
+
? container.parentElement
|
|
3506
|
+
: container;
|
|
3507
|
+
if (!element)
|
|
3508
|
+
return undefined;
|
|
3509
|
+
// Walk up to find the block-level element
|
|
3510
|
+
let block = element;
|
|
3511
|
+
const blockTags = new Set([
|
|
3512
|
+
"P",
|
|
3513
|
+
"DIV",
|
|
3514
|
+
"H1",
|
|
3515
|
+
"H2",
|
|
3516
|
+
"H3",
|
|
3517
|
+
"H4",
|
|
3518
|
+
"H5",
|
|
3519
|
+
"H6",
|
|
3520
|
+
"LI",
|
|
3521
|
+
"BLOCKQUOTE",
|
|
3522
|
+
"TD",
|
|
3523
|
+
"TH",
|
|
3524
|
+
]);
|
|
3525
|
+
while (block && !blockTags.has(block.tagName)) {
|
|
3526
|
+
block = block.parentElement;
|
|
3527
|
+
}
|
|
3528
|
+
if (!block)
|
|
3529
|
+
return undefined;
|
|
3530
|
+
const align = block.style.textAlign || window.getComputedStyle(block).textAlign;
|
|
3531
|
+
if (align === "center")
|
|
3532
|
+
return "center";
|
|
3533
|
+
if (align === "right")
|
|
3534
|
+
return "right";
|
|
3535
|
+
if (align === "justify")
|
|
3536
|
+
return "justify";
|
|
3537
|
+
return "left";
|
|
3538
|
+
}
|
|
3539
|
+
/**
|
|
3540
|
+
* Creates an alignment plugin with a dropdown.
|
|
3541
|
+
* @param alignments - Which alignments to offer, defaults to all four.
|
|
3542
|
+
*/
|
|
3543
|
+
function createAlignmentPlugin(alignments = ["left", "center", "right", "justify"]) {
|
|
3544
|
+
const options = alignments.map((a) => ({
|
|
3545
|
+
value: a,
|
|
3546
|
+
label: alignmentLabels[a] || a,
|
|
3547
|
+
icon: alignmentIcons[a],
|
|
3548
|
+
}));
|
|
3549
|
+
return {
|
|
3550
|
+
name: "alignment",
|
|
3551
|
+
type: "block",
|
|
3552
|
+
renderButton: (props) => {
|
|
3553
|
+
const onSelect = props.onSelect;
|
|
3554
|
+
const currentValue = props.currentValue ||
|
|
3555
|
+
detectCurrentAlignment() ||
|
|
3556
|
+
"left";
|
|
3557
|
+
return (jsx(Dropdown, { icon: alignmentIcons[currentValue] || "mdi:format-align-left", label: "Ausrichtung", options: options, onSelect: (value) => {
|
|
3558
|
+
if (onSelect)
|
|
3559
|
+
onSelect(value);
|
|
3560
|
+
}, currentValue: currentValue, disabled: props.disabled }));
|
|
3561
|
+
},
|
|
3562
|
+
getCurrentValue: () => detectCurrentAlignment(),
|
|
3563
|
+
execute: (_editor, value) => {
|
|
3564
|
+
if (!value)
|
|
3565
|
+
return;
|
|
3566
|
+
const command = alignmentCommands[value];
|
|
3567
|
+
if (command) {
|
|
3568
|
+
document.execCommand(command, false);
|
|
3569
|
+
}
|
|
3570
|
+
},
|
|
3571
|
+
isActive: () => {
|
|
3572
|
+
const align = detectCurrentAlignment();
|
|
3573
|
+
return align !== undefined && align !== "left";
|
|
3574
|
+
},
|
|
3575
|
+
canExecute: () => true,
|
|
3576
|
+
};
|
|
3577
|
+
}
|
|
3578
|
+
/** Pre-built alignment plugin with all four options */
|
|
3579
|
+
const alignmentPlugin = createAlignmentPlugin();
|
|
3580
|
+
|
|
3581
|
+
const InsertTableDialog = ({ onInsert, onClose, }) => {
|
|
3582
|
+
const [rows, setRows] = useState(3);
|
|
3583
|
+
const [cols, setCols] = useState(3);
|
|
3584
|
+
const dialogRef = useRef(null);
|
|
3585
|
+
useEffect(() => {
|
|
3586
|
+
const handler = (e) => {
|
|
3587
|
+
if (dialogRef.current &&
|
|
3588
|
+
!dialogRef.current.contains(e.target)) {
|
|
3589
|
+
onClose();
|
|
3590
|
+
}
|
|
3591
|
+
};
|
|
3592
|
+
document.addEventListener("mousedown", handler);
|
|
3593
|
+
return () => document.removeEventListener("mousedown", handler);
|
|
3594
|
+
}, [onClose]);
|
|
3595
|
+
return (jsxs("div", { className: "rte-table-insert-dialog", ref: dialogRef, children: [jsx("div", { className: "rte-table-insert-title", children: "Tabelle einf\u00FCgen" }), jsxs("div", { className: "rte-table-insert-fields", children: [jsxs("label", { className: "rte-table-insert-label", children: [jsx("span", { children: "Zeilen" }), jsx("input", { type: "number", min: 1, max: 20, value: rows, onChange: (e) => setRows(Math.max(1, Math.min(20, parseInt(e.target.value) || 1))), className: "rte-table-insert-input" })] }), jsxs("label", { className: "rte-table-insert-label", children: [jsx("span", { children: "Spalten" }), jsx("input", { type: "number", min: 1, max: 10, value: cols, onChange: (e) => setCols(Math.max(1, Math.min(10, parseInt(e.target.value) || 1))), className: "rte-table-insert-input" })] })] }), jsx("button", { type: "button", className: "rte-table-insert-btn", onClick: () => onInsert(rows, cols), children: "Einf\u00FCgen" })] }));
|
|
3596
|
+
};
|
|
3597
|
+
const TableContextMenu = ({ x, y, onClose, }) => {
|
|
3598
|
+
const ref = useRef(null);
|
|
3599
|
+
useEffect(() => {
|
|
3600
|
+
const handler = (e) => {
|
|
3601
|
+
if (ref.current && !ref.current.contains(e.target)) {
|
|
3602
|
+
onClose();
|
|
3603
|
+
}
|
|
3604
|
+
};
|
|
3605
|
+
document.addEventListener("mousedown", handler);
|
|
3606
|
+
return () => document.removeEventListener("mousedown", handler);
|
|
3607
|
+
}, [onClose]);
|
|
3608
|
+
const action = (fn) => {
|
|
3609
|
+
fn();
|
|
3610
|
+
onClose();
|
|
3611
|
+
};
|
|
3612
|
+
return (jsxs("div", { ref: ref, className: "rte-table-context-menu", style: { position: "fixed", left: x, top: y }, children: [jsx("button", { type: "button", onClick: () => action(() => insertRow("above")), children: "Zeile oben einf\u00FCgen" }), jsx("button", { type: "button", onClick: () => action(() => insertRow("below")), children: "Zeile unten einf\u00FCgen" }), jsx("div", { className: "rte-table-context-divider" }), jsx("button", { type: "button", onClick: () => action(() => insertColumn("left")), children: "Spalte links einf\u00FCgen" }), jsx("button", { type: "button", onClick: () => action(() => insertColumn("right")), children: "Spalte rechts einf\u00FCgen" }), jsx("div", { className: "rte-table-context-divider" }), jsx("button", { type: "button", className: "rte-table-context-danger", onClick: () => action(deleteRow), children: "Zeile l\u00F6schen" }), jsx("button", { type: "button", className: "rte-table-context-danger", onClick: () => action(deleteColumn), children: "Spalte l\u00F6schen" }), jsx("button", { type: "button", className: "rte-table-context-danger", onClick: () => action(deleteTable), children: "Tabelle l\u00F6schen" })] }));
|
|
3613
|
+
};
|
|
3614
|
+
const TableToolbarButton = (props) => {
|
|
3615
|
+
const [showDialog, setShowDialog] = useState(false);
|
|
3616
|
+
const handleInsert = useCallback((rows, cols) => {
|
|
3617
|
+
setShowDialog(false);
|
|
3618
|
+
if (!props.editorAPI)
|
|
3619
|
+
return;
|
|
3620
|
+
const sel = document.getSelection();
|
|
3621
|
+
if (!sel || sel.rangeCount === 0)
|
|
3622
|
+
return;
|
|
3623
|
+
const range = sel.getRangeAt(0);
|
|
3624
|
+
// Find the editor's contentEditable root
|
|
3625
|
+
const container = range.commonAncestorContainer;
|
|
3626
|
+
const editorEl = container.nodeType === Node.TEXT_NODE
|
|
3627
|
+
? container.parentElement
|
|
3628
|
+
: container;
|
|
3629
|
+
const editorRoot = editorEl?.closest("[contenteditable]");
|
|
3630
|
+
if (!editorRoot)
|
|
3631
|
+
return;
|
|
3632
|
+
const table = createTable(rows, cols);
|
|
3633
|
+
// Insert after the current block element
|
|
3634
|
+
let block = editorEl;
|
|
3635
|
+
while (block &&
|
|
3636
|
+
block !== editorRoot &&
|
|
3637
|
+
block.parentElement !== editorRoot) {
|
|
3638
|
+
block = block.parentElement;
|
|
3639
|
+
}
|
|
3640
|
+
if (block && block !== editorRoot) {
|
|
3641
|
+
block.parentNode?.insertBefore(table, block.nextSibling);
|
|
3642
|
+
}
|
|
3643
|
+
else {
|
|
3644
|
+
editorRoot.appendChild(table);
|
|
3645
|
+
}
|
|
3646
|
+
// Add a paragraph after the table so the user can continue typing
|
|
3647
|
+
const p = document.createElement("p");
|
|
3648
|
+
p.innerHTML = "<br>";
|
|
3649
|
+
table.parentNode?.insertBefore(p, table.nextSibling);
|
|
3650
|
+
// Focus the first cell
|
|
3651
|
+
const firstCell = table.querySelector("td, th");
|
|
3652
|
+
if (firstCell) {
|
|
3653
|
+
const newRange = document.createRange();
|
|
3654
|
+
newRange.setStart(firstCell, 0);
|
|
3655
|
+
newRange.collapse(true);
|
|
3656
|
+
sel.removeAllRanges();
|
|
3657
|
+
sel.addRange(newRange);
|
|
3658
|
+
}
|
|
3659
|
+
}, [props.editorAPI]);
|
|
3660
|
+
return (jsxs("div", { style: { position: "relative" }, children: [jsx("button", { type: "button", onClick: () => setShowDialog(!showDialog), disabled: props.disabled, className: `rte-toolbar-button ${props.isActive ? "rte-toolbar-button-active" : ""}`, title: "Tabelle", "aria-label": "Tabelle", children: jsx(IconWrapper, { icon: "mdi:table", width: 18, height: 18 }) }), showDialog && (jsx(InsertTableDialog, { onInsert: handleInsert, onClose: () => setShowDialog(false) }))] }));
|
|
3661
|
+
};
|
|
3662
|
+
/* ══════════════════════════════════════════════════════════════════════════
|
|
3663
|
+
Table Plugin export
|
|
3664
|
+
══════════════════════════════════════════════════════════════════════ */
|
|
3665
|
+
const tablePlugin = {
|
|
3666
|
+
name: "table",
|
|
3667
|
+
type: "command",
|
|
3668
|
+
renderButton: (props) => (jsx(TableToolbarButton, { ...props, editorAPI: props.editorAPI })),
|
|
3669
|
+
execute: () => {
|
|
3670
|
+
// Insertion is handled by the dialog component
|
|
3671
|
+
},
|
|
3672
|
+
isActive: () => getActiveTable() !== null,
|
|
3673
|
+
canExecute: () => true,
|
|
3674
|
+
};
|
|
3675
|
+
/* ══════════════════════════════════════════════════════════════════════════
|
|
3676
|
+
TableContextMenuProvider — wrap the editor to enable right-click menu
|
|
3677
|
+
══════════════════════════════════════════════════════════════════════ */
|
|
3678
|
+
const TableContextMenuProvider = ({ children }) => {
|
|
3679
|
+
const [menu, setMenu] = useState(null);
|
|
3680
|
+
useEffect(() => {
|
|
3681
|
+
const handler = (e) => {
|
|
3682
|
+
const target = e.target;
|
|
3683
|
+
const cell = target.closest("td, th");
|
|
3684
|
+
const editorRoot = target.closest("[contenteditable]");
|
|
3685
|
+
if (cell && editorRoot) {
|
|
3686
|
+
e.preventDefault();
|
|
3687
|
+
setMenu({ x: e.clientX, y: e.clientY });
|
|
3688
|
+
}
|
|
3689
|
+
};
|
|
3690
|
+
document.addEventListener("contextmenu", handler);
|
|
3691
|
+
return () => document.removeEventListener("contextmenu", handler);
|
|
3692
|
+
}, []);
|
|
3693
|
+
return (jsxs(Fragment, { children: [children, menu && (jsx(TableContextMenu, { x: menu.x, y: menu.y, onClose: () => setMenu(null) }))] }));
|
|
3694
|
+
};
|
|
3695
|
+
|
|
3696
|
+
const EMPTY_LINK = {
|
|
3697
|
+
url: "",
|
|
3698
|
+
target: "_self",
|
|
3699
|
+
rel: "",
|
|
3700
|
+
title: "",
|
|
3701
|
+
pageRef: "",
|
|
3702
|
+
urlExtra: "",
|
|
3703
|
+
};
|
|
3704
|
+
const LinkDialog = ({ initialData, options, onSave, onRemove, onClose, isEditing, }) => {
|
|
3705
|
+
const [data, setData] = useState(initialData);
|
|
3706
|
+
const [showAdvanced, setShowAdvanced] = useState(false);
|
|
3707
|
+
const dialogRef = useRef(null);
|
|
3708
|
+
const urlInputRef = useRef(null);
|
|
3709
|
+
useEffect(() => {
|
|
3710
|
+
urlInputRef.current?.focus();
|
|
3711
|
+
}, []);
|
|
3712
|
+
useEffect(() => {
|
|
3713
|
+
const handler = (e) => {
|
|
3714
|
+
if (dialogRef.current &&
|
|
3715
|
+
!dialogRef.current.contains(e.target)) {
|
|
3716
|
+
onClose();
|
|
3717
|
+
}
|
|
3718
|
+
};
|
|
3719
|
+
// Delay to avoid closing immediately on the same click
|
|
3720
|
+
const timer = setTimeout(() => {
|
|
3721
|
+
document.addEventListener("mousedown", handler);
|
|
3722
|
+
}, 50);
|
|
3723
|
+
return () => {
|
|
3724
|
+
clearTimeout(timer);
|
|
3725
|
+
document.removeEventListener("mousedown", handler);
|
|
3726
|
+
};
|
|
3727
|
+
}, [onClose]);
|
|
3728
|
+
const handleKeyDown = (e) => {
|
|
3729
|
+
if (e.key === "Enter") {
|
|
3730
|
+
e.preventDefault();
|
|
3731
|
+
onSave(data);
|
|
3732
|
+
}
|
|
3733
|
+
else if (e.key === "Escape") {
|
|
3734
|
+
e.preventDefault();
|
|
3735
|
+
onClose();
|
|
3736
|
+
}
|
|
3737
|
+
};
|
|
3738
|
+
const set = (key, value) => setData((prev) => ({ ...prev, [key]: value }));
|
|
3739
|
+
const hasAdvancedOptions = options.enableRel ||
|
|
3740
|
+
options.enableTitle ||
|
|
3741
|
+
options.enableUrlExtra ||
|
|
3742
|
+
options.enablePageRef;
|
|
3743
|
+
return (jsxs("div", { ref: dialogRef, className: "rte-link-dialog", onKeyDown: handleKeyDown, children: [jsx("div", { className: "rte-link-dialog-title", children: isEditing ? "Link bearbeiten" : "Link einfügen" }), jsxs("div", { className: "rte-link-dialog-field", children: [jsx("label", { className: "rte-link-dialog-label", children: "URL" }), jsx("input", { ref: urlInputRef, type: "url", className: "rte-link-dialog-input", value: data.url, onChange: (e) => set("url", e.target.value), placeholder: "https://..." })] }), options.enableTarget && (jsxs("div", { className: "rte-link-dialog-field", children: [jsx("label", { className: "rte-link-dialog-label", children: "Ziel" }), jsxs("select", { className: "rte-link-dialog-select", value: data.target, onChange: (e) => set("target", e.target.value), children: [jsx("option", { value: "_self", children: "Gleiches Fenster" }), jsx("option", { value: "_blank", children: "Neues Fenster" })] })] })), hasAdvancedOptions && (jsxs(Fragment, { children: [jsx("button", { type: "button", className: "rte-link-dialog-toggle", onClick: () => setShowAdvanced(!showAdvanced), children: showAdvanced ? "Erweitert ausblenden" : "Erweitert anzeigen" }), showAdvanced && (jsxs("div", { className: "rte-link-dialog-advanced", children: [options.enablePageRef && (jsxs("div", { className: "rte-link-dialog-field", children: [jsx("label", { className: "rte-link-dialog-label", children: "Seitenreferenz" }), jsx("input", { type: "text", className: "rte-link-dialog-input", value: data.pageRef, onChange: (e) => set("pageRef", e.target.value), placeholder: "page-id" })] })), options.enableUrlExtra && (jsxs("div", { className: "rte-link-dialog-field", children: [jsx("label", { className: "rte-link-dialog-label", children: "URL Extra" }), jsx("input", { type: "text", className: "rte-link-dialog-input", value: data.urlExtra, onChange: (e) => set("urlExtra", e.target.value), placeholder: "?param=value oder #anchor" })] })), options.enableRel && (jsxs("div", { className: "rte-link-dialog-field", children: [jsx("label", { className: "rte-link-dialog-label", children: "Rel" }), jsx("input", { type: "text", className: "rte-link-dialog-input", value: data.rel, onChange: (e) => set("rel", e.target.value), placeholder: "noopener noreferrer" })] })), options.enableTitle && (jsxs("div", { className: "rte-link-dialog-field", children: [jsx("label", { className: "rte-link-dialog-label", children: "Titel" }), jsx("input", { type: "text", className: "rte-link-dialog-input", value: data.title, onChange: (e) => set("title", e.target.value), placeholder: "Link-Titel" })] }))] }))] })), jsxs("div", { className: "rte-link-dialog-actions", children: [jsx("button", { type: "button", className: "rte-link-dialog-btn rte-link-dialog-btn-primary", onClick: () => onSave(data), disabled: !data.url.trim(), children: isEditing ? "Speichern" : "Einfügen" }), isEditing && (jsx("button", { type: "button", className: "rte-link-dialog-btn rte-link-dialog-btn-danger", onClick: onRemove, children: "Entfernen" })), jsx("button", { type: "button", className: "rte-link-dialog-btn", onClick: onClose, children: "Abbrechen" })] })] }));
|
|
3744
|
+
};
|
|
3745
|
+
const LinkToolbarButton = (props) => {
|
|
3746
|
+
const [showDialog, setShowDialog] = useState(false);
|
|
3747
|
+
const [linkData, setLinkData] = useState(EMPTY_LINK);
|
|
3748
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
3749
|
+
const savedRangeRef = useRef(null);
|
|
3750
|
+
const openDialog = useCallback(() => {
|
|
3751
|
+
const sel = document.getSelection();
|
|
3752
|
+
if (!sel || sel.rangeCount === 0)
|
|
3753
|
+
return;
|
|
3754
|
+
// Save the current selection
|
|
3755
|
+
savedRangeRef.current = sel.getRangeAt(0).cloneRange();
|
|
3756
|
+
const range = sel.getRangeAt(0);
|
|
3757
|
+
const container = range.commonAncestorContainer;
|
|
3758
|
+
const element = container.nodeType === Node.TEXT_NODE
|
|
3759
|
+
? container.parentElement
|
|
3760
|
+
: container;
|
|
3761
|
+
const existingLink = element?.closest("a");
|
|
3762
|
+
if (existingLink) {
|
|
3763
|
+
setIsEditing(true);
|
|
3764
|
+
setLinkData({
|
|
3765
|
+
url: existingLink.getAttribute("href") || "",
|
|
3766
|
+
target: existingLink.getAttribute("target") || "_self",
|
|
3767
|
+
rel: existingLink.getAttribute("rel") || "",
|
|
3768
|
+
title: existingLink.getAttribute("title") || "",
|
|
3769
|
+
pageRef: existingLink.getAttribute("data-page-ref") || "",
|
|
3770
|
+
urlExtra: existingLink.getAttribute("data-url-extra") || "",
|
|
3771
|
+
});
|
|
3772
|
+
}
|
|
3773
|
+
else {
|
|
3774
|
+
setIsEditing(false);
|
|
3775
|
+
setLinkData(EMPTY_LINK);
|
|
3776
|
+
}
|
|
3777
|
+
setShowDialog(true);
|
|
3778
|
+
}, []);
|
|
3779
|
+
const restoreSelection = useCallback(() => {
|
|
3780
|
+
if (savedRangeRef.current) {
|
|
3781
|
+
const sel = document.getSelection();
|
|
3782
|
+
if (sel) {
|
|
3783
|
+
sel.removeAllRanges();
|
|
3784
|
+
sel.addRange(savedRangeRef.current);
|
|
3785
|
+
}
|
|
3786
|
+
}
|
|
3787
|
+
}, []);
|
|
3788
|
+
const handleSave = useCallback((data) => {
|
|
3789
|
+
setShowDialog(false);
|
|
3790
|
+
restoreSelection();
|
|
3791
|
+
const sel = document.getSelection();
|
|
3792
|
+
if (!sel || sel.rangeCount === 0)
|
|
3793
|
+
return;
|
|
3794
|
+
const range = sel.getRangeAt(0);
|
|
3795
|
+
const container = range.commonAncestorContainer;
|
|
3796
|
+
const element = container.nodeType === Node.TEXT_NODE
|
|
3797
|
+
? container.parentElement
|
|
3798
|
+
: container;
|
|
3799
|
+
const existingLink = element?.closest("a");
|
|
3800
|
+
if (existingLink) {
|
|
3801
|
+
// Update existing link
|
|
3802
|
+
existingLink.setAttribute("href", data.url);
|
|
3803
|
+
if (data.target && data.target !== "_self") {
|
|
3804
|
+
existingLink.setAttribute("target", data.target);
|
|
3805
|
+
}
|
|
3806
|
+
else {
|
|
3807
|
+
existingLink.removeAttribute("target");
|
|
3808
|
+
}
|
|
3809
|
+
if (data.rel) {
|
|
3810
|
+
existingLink.setAttribute("rel", data.rel);
|
|
3811
|
+
}
|
|
3812
|
+
else {
|
|
3813
|
+
existingLink.removeAttribute("rel");
|
|
3814
|
+
}
|
|
3815
|
+
if (data.title) {
|
|
3816
|
+
existingLink.setAttribute("title", data.title);
|
|
3817
|
+
}
|
|
3818
|
+
else {
|
|
3819
|
+
existingLink.removeAttribute("title");
|
|
3820
|
+
}
|
|
3821
|
+
if (data.pageRef) {
|
|
3822
|
+
existingLink.setAttribute("data-page-ref", data.pageRef);
|
|
3823
|
+
}
|
|
3824
|
+
else {
|
|
3825
|
+
existingLink.removeAttribute("data-page-ref");
|
|
3826
|
+
}
|
|
3827
|
+
if (data.urlExtra) {
|
|
3828
|
+
existingLink.setAttribute("data-url-extra", data.urlExtra);
|
|
3829
|
+
}
|
|
3830
|
+
else {
|
|
3831
|
+
existingLink.removeAttribute("data-url-extra");
|
|
3832
|
+
}
|
|
3833
|
+
}
|
|
3834
|
+
else {
|
|
3835
|
+
// Create new link
|
|
3836
|
+
document.execCommand("createLink", false, data.url);
|
|
3837
|
+
// Now find the newly created link and set extra attributes
|
|
3838
|
+
const newSel = document.getSelection();
|
|
3839
|
+
if (newSel && newSel.rangeCount > 0) {
|
|
3840
|
+
const newRange = newSel.getRangeAt(0);
|
|
3841
|
+
const newContainer = newRange.commonAncestorContainer;
|
|
3842
|
+
const newElement = newContainer.nodeType === Node.TEXT_NODE
|
|
3843
|
+
? newContainer.parentElement
|
|
3844
|
+
: newContainer;
|
|
3845
|
+
const newLink = newElement?.closest("a");
|
|
3846
|
+
if (newLink) {
|
|
3847
|
+
if (data.target && data.target !== "_self") {
|
|
3848
|
+
newLink.setAttribute("target", data.target);
|
|
3849
|
+
}
|
|
3850
|
+
if (data.rel) {
|
|
3851
|
+
newLink.setAttribute("rel", data.rel);
|
|
3852
|
+
}
|
|
3853
|
+
if (data.title) {
|
|
3854
|
+
newLink.setAttribute("title", data.title);
|
|
3855
|
+
}
|
|
3856
|
+
if (data.pageRef) {
|
|
3857
|
+
newLink.setAttribute("data-page-ref", data.pageRef);
|
|
3858
|
+
}
|
|
3859
|
+
if (data.urlExtra) {
|
|
3860
|
+
newLink.setAttribute("data-url-extra", data.urlExtra);
|
|
3861
|
+
}
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3864
|
+
}
|
|
3865
|
+
}, [restoreSelection]);
|
|
3866
|
+
const handleRemove = useCallback(() => {
|
|
3867
|
+
setShowDialog(false);
|
|
3868
|
+
restoreSelection();
|
|
3869
|
+
const sel = document.getSelection();
|
|
3870
|
+
if (!sel || sel.rangeCount === 0)
|
|
3871
|
+
return;
|
|
3872
|
+
const range = sel.getRangeAt(0);
|
|
3873
|
+
const container = range.commonAncestorContainer;
|
|
3874
|
+
const element = container.nodeType === Node.TEXT_NODE
|
|
3875
|
+
? container.parentElement
|
|
3876
|
+
: container;
|
|
3877
|
+
const existingLink = element?.closest("a");
|
|
3878
|
+
if (existingLink) {
|
|
3879
|
+
const parent = existingLink.parentNode;
|
|
3880
|
+
if (parent) {
|
|
3881
|
+
while (existingLink.firstChild) {
|
|
3882
|
+
parent.insertBefore(existingLink.firstChild, existingLink);
|
|
3883
|
+
}
|
|
3884
|
+
parent.removeChild(existingLink);
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
}, [restoreSelection]);
|
|
3888
|
+
return (jsxs("div", { style: { position: "relative" }, children: [jsx("button", { type: "button", onClick: openDialog, disabled: props.disabled, className: `rte-toolbar-button ${props.isActive ? "rte-toolbar-button-active" : ""}`, title: "Link", "aria-label": "Link", children: jsx(IconWrapper, { icon: "mdi:link", width: 18, height: 18 }) }), showDialog && (jsx(LinkDialog, { initialData: linkData, options: props.options, onSave: handleSave, onRemove: handleRemove, onClose: () => setShowDialog(false), isEditing: isEditing }))] }));
|
|
3889
|
+
};
|
|
3890
|
+
/* ══════════════════════════════════════════════════════════════════════════
|
|
3891
|
+
Plugin factory + export
|
|
3892
|
+
══════════════════════════════════════════════════════════════════════ */
|
|
3893
|
+
/**
|
|
3894
|
+
* Creates an advanced link plugin with a floating dialog.
|
|
3895
|
+
* Supports URL, target, rel, title, page reference, and URL extra.
|
|
3896
|
+
*/
|
|
3897
|
+
function createAdvancedLinkPlugin(options = {}) {
|
|
3898
|
+
const opts = {
|
|
3899
|
+
enablePageRef: false,
|
|
3900
|
+
enableTarget: true,
|
|
3901
|
+
enableRel: true,
|
|
3902
|
+
enableTitle: true,
|
|
3903
|
+
enableUrlExtra: false,
|
|
3904
|
+
...options,
|
|
3905
|
+
};
|
|
3906
|
+
return {
|
|
3907
|
+
name: "advancedLink",
|
|
3908
|
+
type: "inline",
|
|
3909
|
+
renderButton: (props) => (jsx(LinkToolbarButton, { ...props, editorAPI: props.editorAPI, options: opts })),
|
|
3910
|
+
execute: () => {
|
|
3911
|
+
// Handled by the dialog component
|
|
3912
|
+
},
|
|
3913
|
+
isActive: () => {
|
|
3914
|
+
if (typeof document === "undefined")
|
|
3915
|
+
return false;
|
|
3916
|
+
const sel = document.getSelection();
|
|
3917
|
+
if (!sel || sel.rangeCount === 0)
|
|
3918
|
+
return false;
|
|
3919
|
+
const range = sel.getRangeAt(0);
|
|
3920
|
+
const container = range.commonAncestorContainer;
|
|
3921
|
+
const element = container.nodeType === Node.TEXT_NODE
|
|
3922
|
+
? container.parentElement
|
|
3923
|
+
: container;
|
|
3924
|
+
return element?.closest("a") !== null;
|
|
3925
|
+
},
|
|
3926
|
+
canExecute: () => {
|
|
3927
|
+
const sel = document.getSelection();
|
|
3928
|
+
return sel !== null && sel.rangeCount > 0;
|
|
3929
|
+
},
|
|
3930
|
+
};
|
|
3931
|
+
}
|
|
3932
|
+
/** Pre-built advanced link plugin with target + rel + title enabled */
|
|
3933
|
+
const advancedLinkPlugin = createAdvancedLinkPlugin();
|
|
3934
|
+
|
|
3935
|
+
export { Dropdown, Editor, HistoryManager, TableContextMenuProvider, Toolbar, advancedLinkPlugin, alignmentPlugin, blockquotePlugin, boldPlugin, clearFormattingPlugin, codeInlinePlugin, contentToDOM, contentToHTML, createAdvancedLinkPlugin, createAlignmentPlugin, createBackgroundColorPlugin, createBlockFormatPlugin, createEmptyContent, createFontSizePlugin, createHeadingsPlugin, createImagePlugin, createLinkPlugin, createTextColorPlugin, Editor as default, defaultPlugins, domToContent, ensureAllCheckboxes, findClosestCheckboxList, getCurrentBackgroundColor, getCurrentFontSize, getCurrentHeading, getCurrentTextColor, htmlToContent, indentListItem, indentListItemPlugin, isCheckboxList, italicPlugin, linkPlugin, orderedListPlugin, outdentListItem, outdentListItemPlugin, redoPlugin, strikethroughPlugin, subscriptPlugin, superscriptPlugin, tablePlugin, underlinePlugin, undoPlugin, unorderedListPlugin };
|
|
2353
3936
|
//# sourceMappingURL=index.esm.js.map
|