@overlap/rte 0.1.4 → 0.1.6
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 +2 -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 +45 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +1294 -628
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1295 -626
- package/dist/index.js.map +1 -1
- package/dist/plugins/blockFormat.d.ts +2 -2
- package/dist/plugins/blockFormat.d.ts.map +1 -1
- package/dist/styles.css +476 -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/package.json +1 -7
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,7 @@ 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" }) }));
|
|
24
26
|
const iconMap = {
|
|
25
27
|
"mdi:format-bold": BoldIcon,
|
|
26
28
|
"mdi:format-italic": ItalicIcon,
|
|
@@ -35,6 +37,7 @@ const iconMap = {
|
|
|
35
37
|
"mdi:format-color-text": TextColorIcon,
|
|
36
38
|
"mdi:format-color-fill": BackgroundColorIcon,
|
|
37
39
|
"mdi:format-header-1": HeadingIcon,
|
|
40
|
+
"mdi:format-paragraph": FormatIcon,
|
|
38
41
|
"mdi:format-size": FontSizeIcon,
|
|
39
42
|
"mdi:image": ImageIcon,
|
|
40
43
|
"mdi:close": CloseIcon,
|
|
@@ -42,6 +45,7 @@ const iconMap = {
|
|
|
42
45
|
"mdi:upload": UploadIcon,
|
|
43
46
|
"mdi:format-indent-increase": IndentIcon,
|
|
44
47
|
"mdi:format-indent-decrease": OutdentIcon,
|
|
48
|
+
"mdi:checkbox-marked-outline": CheckboxIcon,
|
|
45
49
|
};
|
|
46
50
|
const Icon = ({ icon, width = 18, height = 18, className }) => {
|
|
47
51
|
const IconComponent = iconMap[icon];
|
|
@@ -135,7 +139,91 @@ function createCommandPlugin(name, command, icon, label) {
|
|
|
135
139
|
};
|
|
136
140
|
}
|
|
137
141
|
|
|
138
|
-
|
|
142
|
+
/**
|
|
143
|
+
* Pure DOM utility functions.
|
|
144
|
+
* No React dependencies - only native browser APIs.
|
|
145
|
+
*/
|
|
146
|
+
/**
|
|
147
|
+
* Checks if a UL element is a checkbox list.
|
|
148
|
+
* Detects: own format, Lexical format, and GitHub format.
|
|
149
|
+
*/
|
|
150
|
+
function isCheckboxList(element) {
|
|
151
|
+
if (element.tagName !== "UL")
|
|
152
|
+
return false;
|
|
153
|
+
// Own format
|
|
154
|
+
if (element.classList.contains("rte-checkbox-list"))
|
|
155
|
+
return true;
|
|
156
|
+
// Lexical format (attribute)
|
|
157
|
+
if (element.getAttribute("__lexicallisttype") === "check")
|
|
158
|
+
return true;
|
|
159
|
+
// Lexical theme class (any class containing "checklist")
|
|
160
|
+
const hasChecklistClass = Array.from(element.classList).some((cls) => cls.toLowerCase().includes("checklist"));
|
|
161
|
+
if (hasChecklistClass)
|
|
162
|
+
return true;
|
|
163
|
+
// GitHub format
|
|
164
|
+
if (element.classList.contains("contains-task-list"))
|
|
165
|
+
return true;
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Finds the closest checkbox list ancestor from an element.
|
|
170
|
+
* Works with all supported formats (own, Lexical, GitHub).
|
|
171
|
+
*/
|
|
172
|
+
function findClosestCheckboxList(element) {
|
|
173
|
+
let current = element;
|
|
174
|
+
while (current) {
|
|
175
|
+
if (current.tagName === "UL" && isCheckboxList(current)) {
|
|
176
|
+
return current;
|
|
177
|
+
}
|
|
178
|
+
current = current.parentElement;
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Checks if a list item has a nested list as its first child (not a leaf item).
|
|
184
|
+
*/
|
|
185
|
+
function isNestedListItem(li) {
|
|
186
|
+
const firstChild = li.firstChild;
|
|
187
|
+
return (firstChild instanceof HTMLElement &&
|
|
188
|
+
(firstChild.tagName === "UL" || firstChild.tagName === "OL"));
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Finds the closest list item from a node.
|
|
192
|
+
*/
|
|
193
|
+
function findClosestListItem(node) {
|
|
194
|
+
const element = node.nodeType === Node.TEXT_NODE
|
|
195
|
+
? node.parentElement
|
|
196
|
+
: node;
|
|
197
|
+
return element?.closest("li") || null;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Sets cursor position in a text node after an async DOM update.
|
|
201
|
+
*/
|
|
202
|
+
function setCursorInTextNode(textNode, position, editor) {
|
|
203
|
+
requestAnimationFrame(() => {
|
|
204
|
+
requestAnimationFrame(() => {
|
|
205
|
+
try {
|
|
206
|
+
const range = document.createRange();
|
|
207
|
+
const maxPos = textNode.textContent?.length || 0;
|
|
208
|
+
const safePos = Math.min(Math.max(0, position), maxPos);
|
|
209
|
+
range.setStart(textNode, safePos);
|
|
210
|
+
range.collapse(true);
|
|
211
|
+
const selection = window.getSelection();
|
|
212
|
+
if (selection) {
|
|
213
|
+
selection.removeAllRanges();
|
|
214
|
+
selection.addRange(range);
|
|
215
|
+
if (editor)
|
|
216
|
+
editor.focus();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch (_) {
|
|
220
|
+
// Silently fail - cursor positioning is best-effort
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const defaultHeadings$2 = ["h1", "h2", "h3", "h4", "h5", "h6"];
|
|
139
227
|
const headingLabels$1 = {
|
|
140
228
|
h1: "Überschrift 1",
|
|
141
229
|
h2: "Überschrift 2",
|
|
@@ -145,8 +233,8 @@ const headingLabels$1 = {
|
|
|
145
233
|
h6: "Überschrift 6",
|
|
146
234
|
};
|
|
147
235
|
/**
|
|
148
|
-
*
|
|
149
|
-
* @param headings - Array
|
|
236
|
+
* Creates a Block Format plugin that combines headings, lists, and quote in a dropdown.
|
|
237
|
+
* @param headings - Array of heading levels (e.g. ["h1", "h2", "h3"])
|
|
150
238
|
*/
|
|
151
239
|
function createBlockFormatPlugin(headings = defaultHeadings$2) {
|
|
152
240
|
const options = [
|
|
@@ -166,16 +254,64 @@ function createBlockFormatPlugin(headings = defaultHeadings$2) {
|
|
|
166
254
|
label: "Nummerierte Liste",
|
|
167
255
|
icon: "mdi:format-list-numbered",
|
|
168
256
|
},
|
|
169
|
-
{
|
|
257
|
+
{
|
|
258
|
+
value: "checkbox-list",
|
|
259
|
+
label: "Checkbox-Liste",
|
|
260
|
+
icon: "mdi:checkbox-marked-outline",
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
value: "blockquote",
|
|
264
|
+
label: "Zitat",
|
|
265
|
+
icon: "mdi:format-quote-close",
|
|
266
|
+
},
|
|
170
267
|
];
|
|
268
|
+
/** Detects the current block format at the cursor position. */
|
|
269
|
+
function detectCurrentFormat(editor) {
|
|
270
|
+
const selection = editor.getSelection();
|
|
271
|
+
if (!selection || selection.rangeCount === 0)
|
|
272
|
+
return undefined;
|
|
273
|
+
const range = selection.getRangeAt(0);
|
|
274
|
+
const container = range.commonAncestorContainer;
|
|
275
|
+
const element = container.nodeType === Node.TEXT_NODE
|
|
276
|
+
? container.parentElement
|
|
277
|
+
: container;
|
|
278
|
+
if (!element)
|
|
279
|
+
return undefined;
|
|
280
|
+
const tagName = element.tagName.toLowerCase();
|
|
281
|
+
if (headings.includes(tagName))
|
|
282
|
+
return tagName;
|
|
283
|
+
if (element.closest("blockquote"))
|
|
284
|
+
return "blockquote";
|
|
285
|
+
if (findClosestCheckboxList(element))
|
|
286
|
+
return "checkbox-list";
|
|
287
|
+
if (element.closest("ul"))
|
|
288
|
+
return "ul";
|
|
289
|
+
if (element.closest("ol"))
|
|
290
|
+
return "ol";
|
|
291
|
+
if (tagName === "p")
|
|
292
|
+
return "p";
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
171
295
|
return {
|
|
172
296
|
name: "blockFormat",
|
|
173
297
|
type: "block",
|
|
174
298
|
renderButton: (props) => {
|
|
175
|
-
// Aktuelles Format bestimmen
|
|
176
299
|
const editor = props.editorAPI;
|
|
300
|
+
const onSelect = props.onSelect;
|
|
177
301
|
let currentValue = props.currentValue;
|
|
178
302
|
if (!currentValue && editor) {
|
|
303
|
+
currentValue = detectCurrentFormat(editor);
|
|
304
|
+
}
|
|
305
|
+
return (jsx(Dropdown, { icon: "mdi:format-paragraph", label: "Format", options: options, onSelect: (value) => {
|
|
306
|
+
if (onSelect)
|
|
307
|
+
onSelect(value);
|
|
308
|
+
}, currentValue: currentValue, disabled: props.disabled }));
|
|
309
|
+
},
|
|
310
|
+
getCurrentValue: (editor) => detectCurrentFormat(editor),
|
|
311
|
+
execute: (editor, value) => {
|
|
312
|
+
if (!value)
|
|
313
|
+
return;
|
|
314
|
+
if (value === "checkbox-list") {
|
|
179
315
|
const selection = editor.getSelection();
|
|
180
316
|
if (selection && selection.rangeCount > 0) {
|
|
181
317
|
const range = selection.getRangeAt(0);
|
|
@@ -183,74 +319,26 @@ function createBlockFormatPlugin(headings = defaultHeadings$2) {
|
|
|
183
319
|
const element = container.nodeType === Node.TEXT_NODE
|
|
184
320
|
? container.parentElement
|
|
185
321
|
: 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
|
-
}
|
|
322
|
+
if (!element)
|
|
323
|
+
return;
|
|
324
|
+
const checkboxList = findClosestCheckboxList(element);
|
|
325
|
+
if (checkboxList) {
|
|
326
|
+
// Remove checkbox list: convert to normal list
|
|
327
|
+
checkboxList.classList.remove("rte-checkbox-list");
|
|
328
|
+
checkboxList
|
|
329
|
+
.querySelectorAll("li[role='checkbox']")
|
|
330
|
+
.forEach((li) => {
|
|
331
|
+
li.removeAttribute("role");
|
|
332
|
+
li.removeAttribute("tabIndex");
|
|
333
|
+
li.removeAttribute("aria-checked");
|
|
334
|
+
});
|
|
207
335
|
}
|
|
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);
|
|
336
|
+
else {
|
|
337
|
+
editor.executeCommand("insertCheckboxList");
|
|
214
338
|
}
|
|
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";
|
|
339
|
+
}
|
|
247
340
|
}
|
|
248
|
-
|
|
249
|
-
},
|
|
250
|
-
execute: (editor, value) => {
|
|
251
|
-
if (!value)
|
|
252
|
-
return;
|
|
253
|
-
if (value === "ul") {
|
|
341
|
+
else if (value === "ul") {
|
|
254
342
|
editor.executeCommand("insertUnorderedList");
|
|
255
343
|
}
|
|
256
344
|
else if (value === "ol") {
|
|
@@ -290,6 +378,7 @@ function createBlockFormatPlugin(headings = defaultHeadings$2) {
|
|
|
290
378
|
const tagName = element.tagName.toLowerCase();
|
|
291
379
|
return (headings.includes(tagName) ||
|
|
292
380
|
element.closest("blockquote") !== null ||
|
|
381
|
+
findClosestCheckboxList(element) !== null ||
|
|
293
382
|
element.closest("ul") !== null ||
|
|
294
383
|
element.closest("ol") !== null);
|
|
295
384
|
},
|
|
@@ -313,7 +402,7 @@ const clearFormattingPlugin = {
|
|
|
313
402
|
},
|
|
314
403
|
};
|
|
315
404
|
|
|
316
|
-
const defaultHeadings$1 = ["h1", "h2", "h3"];
|
|
405
|
+
const defaultHeadings$1 = ["h1", "h2", "h3", "h4", "h5", "h6"];
|
|
317
406
|
/**
|
|
318
407
|
* Standard-Plugins
|
|
319
408
|
*/
|
|
@@ -387,10 +476,10 @@ const defaultBlockFormatPlugin = createBlockFormatPlugin(defaultHeadings$1);
|
|
|
387
476
|
const defaultPlugins = [
|
|
388
477
|
undoPlugin,
|
|
389
478
|
redoPlugin,
|
|
479
|
+
defaultBlockFormatPlugin,
|
|
390
480
|
boldPlugin,
|
|
391
481
|
italicPlugin,
|
|
392
482
|
underlinePlugin,
|
|
393
|
-
defaultBlockFormatPlugin,
|
|
394
483
|
clearFormattingPlugin,
|
|
395
484
|
_indentPlugin,
|
|
396
485
|
_outdentPlugin,
|
|
@@ -779,6 +868,138 @@ function createImagePlugin(onImageUpload) {
|
|
|
779
868
|
};
|
|
780
869
|
}
|
|
781
870
|
|
|
871
|
+
/**
|
|
872
|
+
* Pure checkbox utility functions for checkbox lists.
|
|
873
|
+
* Supports detection of own format, Lexical format, and GitHub format.
|
|
874
|
+
* No React dependencies.
|
|
875
|
+
*/
|
|
876
|
+
/**
|
|
877
|
+
* Updates a list item's checkbox attributes (role, aria-checked, tabindex).
|
|
878
|
+
*/
|
|
879
|
+
function updateListItemChecked(li, checked) {
|
|
880
|
+
const parent = li.parentElement;
|
|
881
|
+
if (!parent ||
|
|
882
|
+
parent.tagName !== "UL" ||
|
|
883
|
+
!isCheckboxList(parent) ||
|
|
884
|
+
isNestedListItem(li)) {
|
|
885
|
+
li.removeAttribute("role");
|
|
886
|
+
li.removeAttribute("tabIndex");
|
|
887
|
+
li.removeAttribute("aria-checked");
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
li.setAttribute("role", "checkbox");
|
|
891
|
+
li.setAttribute("tabIndex", "-1");
|
|
892
|
+
li.setAttribute("aria-checked", checked ? "true" : "false");
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Toggles the checked state of a checkbox list item.
|
|
896
|
+
*/
|
|
897
|
+
function toggleListItemChecked(li) {
|
|
898
|
+
const currentChecked = li.getAttribute("aria-checked") === "true";
|
|
899
|
+
li.setAttribute("aria-checked", currentChecked ? "false" : "true");
|
|
900
|
+
if (li.getAttribute("role") !== "checkbox") {
|
|
901
|
+
li.setAttribute("role", "checkbox");
|
|
902
|
+
li.setAttribute("tabIndex", "-1");
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Ensures all checkbox list items in the editor have correct attributes.
|
|
907
|
+
* Normalizes foreign formats (Lexical, GitHub) to internal format.
|
|
908
|
+
*/
|
|
909
|
+
function ensureAllCheckboxes(editor) {
|
|
910
|
+
const allULs = editor.querySelectorAll("ul");
|
|
911
|
+
allULs.forEach((ul) => {
|
|
912
|
+
if (!isCheckboxList(ul))
|
|
913
|
+
return;
|
|
914
|
+
// Normalize: ensure our class is always present
|
|
915
|
+
if (!ul.classList.contains("rte-checkbox-list")) {
|
|
916
|
+
ul.classList.add("rte-checkbox-list");
|
|
917
|
+
}
|
|
918
|
+
// Handle GitHub format: convert <input type="checkbox"> to aria-checked
|
|
919
|
+
ul.querySelectorAll(":scope > li").forEach((li) => {
|
|
920
|
+
const input = li.querySelector('input[type="checkbox"]');
|
|
921
|
+
if (input) {
|
|
922
|
+
const checked = input.checked || input.hasAttribute("checked");
|
|
923
|
+
if (li.getAttribute("aria-checked") === null) {
|
|
924
|
+
li.setAttribute("aria-checked", checked ? "true" : "false");
|
|
925
|
+
}
|
|
926
|
+
input.remove();
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
const listItems = ul.querySelectorAll(":scope > li");
|
|
930
|
+
listItems.forEach((li) => {
|
|
931
|
+
const htmlLi = li;
|
|
932
|
+
if (!isNestedListItem(htmlLi)) {
|
|
933
|
+
const ariaChecked = htmlLi.getAttribute("aria-checked");
|
|
934
|
+
const checked = ariaChecked === "true";
|
|
935
|
+
updateListItemChecked(htmlLi, checked);
|
|
936
|
+
}
|
|
937
|
+
else {
|
|
938
|
+
htmlLi.removeAttribute("role");
|
|
939
|
+
htmlLi.removeAttribute("tabIndex");
|
|
940
|
+
htmlLi.removeAttribute("aria-checked");
|
|
941
|
+
}
|
|
942
|
+
});
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Returns the currently focused checkbox list item, or null.
|
|
947
|
+
*/
|
|
948
|
+
function getActiveCheckListItem() {
|
|
949
|
+
const activeElement = document.activeElement;
|
|
950
|
+
if (activeElement &&
|
|
951
|
+
activeElement.tagName === "LI" &&
|
|
952
|
+
activeElement.parentElement &&
|
|
953
|
+
isCheckboxList(activeElement.parentElement)) {
|
|
954
|
+
return activeElement;
|
|
955
|
+
}
|
|
956
|
+
return null;
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Finds the next or previous checkbox list item sibling.
|
|
960
|
+
*/
|
|
961
|
+
function findCheckListItemSibling(li, backward) {
|
|
962
|
+
let sibling = backward
|
|
963
|
+
? li.previousElementSibling
|
|
964
|
+
: li.nextElementSibling;
|
|
965
|
+
let parent = li;
|
|
966
|
+
// Walk up the tree to find a non-null sibling
|
|
967
|
+
while (sibling == null && parent) {
|
|
968
|
+
const parentList = parent.parentElement;
|
|
969
|
+
if (parentList && parentList.tagName === "UL") {
|
|
970
|
+
const grandParent = parentList.parentElement;
|
|
971
|
+
if (grandParent && grandParent.tagName === "LI") {
|
|
972
|
+
parent = grandParent;
|
|
973
|
+
sibling = backward
|
|
974
|
+
? parent.previousElementSibling
|
|
975
|
+
: parent.nextElementSibling;
|
|
976
|
+
}
|
|
977
|
+
else {
|
|
978
|
+
break;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
else {
|
|
982
|
+
break;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
// Walk down the tree to find the first non-nested list item
|
|
986
|
+
while (sibling && sibling.tagName === "LI") {
|
|
987
|
+
const child = backward
|
|
988
|
+
? sibling.lastElementChild
|
|
989
|
+
: sibling.firstElementChild;
|
|
990
|
+
if (child &&
|
|
991
|
+
(child.tagName === "UL" || child.tagName === "OL")) {
|
|
992
|
+
sibling = backward
|
|
993
|
+
? child.lastElementChild
|
|
994
|
+
: child.firstElementChild;
|
|
995
|
+
}
|
|
996
|
+
else {
|
|
997
|
+
return sibling;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
return null;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
782
1003
|
/**
|
|
783
1004
|
* Entfernt alle Formatierungen aus der aktuellen Selection
|
|
784
1005
|
*/
|
|
@@ -972,143 +1193,229 @@ function clearLinks(selection) {
|
|
|
972
1193
|
});
|
|
973
1194
|
}
|
|
974
1195
|
|
|
1196
|
+
/**
|
|
1197
|
+
* Converts a DOM element (editor root) to EditorContent JSON.
|
|
1198
|
+
* Supports own format, Lexical HTML, and GitHub HTML.
|
|
1199
|
+
*/
|
|
975
1200
|
function domToContent(element) {
|
|
976
1201
|
const blocks = [];
|
|
1202
|
+
// Normalize all checkbox lists before conversion
|
|
1203
|
+
ensureAllCheckboxes(element);
|
|
977
1204
|
function processNode(node) {
|
|
978
1205
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
979
1206
|
const text = node.textContent;
|
|
980
1207
|
if (text === null || text === undefined)
|
|
981
1208
|
return null;
|
|
1209
|
+
// In checkbox lists, skip whitespace-only text nodes
|
|
1210
|
+
// if the LI has other meaningful text
|
|
1211
|
+
const isOnlyWhitespace = /^[\s\u200B]*$/.test(text);
|
|
1212
|
+
const isInCheckboxList = node.parentElement?.closest("ul.rte-checkbox-list") !== null;
|
|
1213
|
+
if (isOnlyWhitespace && isInCheckboxList) {
|
|
1214
|
+
const li = node.parentElement?.closest("li");
|
|
1215
|
+
if (li) {
|
|
1216
|
+
const allText = li.textContent || "";
|
|
1217
|
+
const textWithoutWhitespace = allText.replace(/[\s\u200B]/g, "");
|
|
1218
|
+
if (textWithoutWhitespace.length > 0) {
|
|
1219
|
+
return null;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
982
1223
|
return { type: "text", text };
|
|
983
1224
|
}
|
|
984
|
-
if (node.nodeType
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1225
|
+
if (node.nodeType !== Node.ELEMENT_NODE) {
|
|
1226
|
+
return null;
|
|
1227
|
+
}
|
|
1228
|
+
const el = node;
|
|
1229
|
+
const tagName = el.tagName.toLowerCase();
|
|
1230
|
+
// Skip checkbox input elements (GitHub format remnants)
|
|
1231
|
+
if (tagName === "input" && el.getAttribute("type") === "checkbox") {
|
|
1232
|
+
return null;
|
|
1233
|
+
}
|
|
1234
|
+
// Handle <br> as empty text
|
|
1235
|
+
if (tagName === "br") {
|
|
1236
|
+
return null;
|
|
1237
|
+
}
|
|
1238
|
+
if (tagName === "img") {
|
|
1239
|
+
const attributes = {};
|
|
1240
|
+
const src = el.getAttribute("src");
|
|
1241
|
+
const alt = el.getAttribute("alt");
|
|
1242
|
+
if (src)
|
|
1243
|
+
attributes.src = src;
|
|
1244
|
+
if (alt)
|
|
1245
|
+
attributes.alt = alt;
|
|
1246
|
+
return {
|
|
1247
|
+
type: "image",
|
|
1248
|
+
attributes: Object.keys(attributes).length > 0
|
|
1249
|
+
? attributes
|
|
1250
|
+
: undefined,
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
// Block elements
|
|
1254
|
+
if ([
|
|
1255
|
+
"p",
|
|
1256
|
+
"div",
|
|
1257
|
+
"h1",
|
|
1258
|
+
"h2",
|
|
1259
|
+
"h3",
|
|
1260
|
+
"h4",
|
|
1261
|
+
"h5",
|
|
1262
|
+
"h6",
|
|
1263
|
+
"blockquote",
|
|
1264
|
+
"ul",
|
|
1265
|
+
"ol",
|
|
1266
|
+
"li",
|
|
1267
|
+
].includes(tagName)) {
|
|
1268
|
+
const children = [];
|
|
1269
|
+
const attributes = {};
|
|
1270
|
+
// Detect checkbox lists (own + Lexical + GitHub formats)
|
|
1271
|
+
if (tagName === "ul" && isCheckboxList(el)) {
|
|
1272
|
+
attributes.class = "rte-checkbox-list";
|
|
1001
1273
|
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
"
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
"ul",
|
|
1013
|
-
"ol",
|
|
1014
|
-
"li",
|
|
1015
|
-
].includes(tagName)) {
|
|
1016
|
-
const children = [];
|
|
1017
|
-
Array.from(el.childNodes).forEach((child) => {
|
|
1018
|
-
const processed = processNode(child);
|
|
1019
|
-
if (processed)
|
|
1020
|
-
children.push(processed);
|
|
1021
|
-
});
|
|
1022
|
-
const attributes = {};
|
|
1023
|
-
if (tagName === "a") {
|
|
1024
|
-
const href = el.getAttribute("href");
|
|
1025
|
-
if (href)
|
|
1026
|
-
attributes.href = href;
|
|
1274
|
+
// Detect checkbox list items via aria-checked
|
|
1275
|
+
if (tagName === "li") {
|
|
1276
|
+
const parentUl = el.closest("ul");
|
|
1277
|
+
if (parentUl &&
|
|
1278
|
+
isCheckboxList(parentUl) &&
|
|
1279
|
+
el.getAttribute("role") === "checkbox") {
|
|
1280
|
+
const ariaChecked = el.getAttribute("aria-checked");
|
|
1281
|
+
if (ariaChecked !== null) {
|
|
1282
|
+
attributes.checkboxChecked = ariaChecked;
|
|
1283
|
+
}
|
|
1027
1284
|
}
|
|
1028
|
-
return {
|
|
1029
|
-
type: tagName,
|
|
1030
|
-
children: children.length > 0 ? children : [],
|
|
1031
|
-
attributes: Object.keys(attributes).length > 0
|
|
1032
|
-
? attributes
|
|
1033
|
-
: undefined,
|
|
1034
|
-
};
|
|
1035
1285
|
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
const attributes = {};
|
|
1044
|
-
Array.from(el.attributes).forEach((attr) => {
|
|
1045
|
-
attributes[attr.name] = attr.value;
|
|
1046
|
-
});
|
|
1047
|
-
if (tagName === "a") {
|
|
1048
|
-
const href = el.getAttribute("href");
|
|
1049
|
-
if (href)
|
|
1050
|
-
attributes.href = href;
|
|
1051
|
-
return {
|
|
1052
|
-
type: "link",
|
|
1053
|
-
children: children.length > 0 ? children : undefined,
|
|
1054
|
-
attributes: Object.keys(attributes).length > 0
|
|
1055
|
-
? attributes
|
|
1056
|
-
: undefined,
|
|
1057
|
-
};
|
|
1286
|
+
// Process children recursively
|
|
1287
|
+
Array.from(el.childNodes).forEach((child) => {
|
|
1288
|
+
// Skip checkbox input nodes
|
|
1289
|
+
if (child.nodeType === Node.ELEMENT_NODE &&
|
|
1290
|
+
child.tagName.toLowerCase() === "input" &&
|
|
1291
|
+
child.getAttribute("type") === "checkbox") {
|
|
1292
|
+
return;
|
|
1058
1293
|
}
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
style.split(";").forEach((rule) => {
|
|
1063
|
-
const [key, value] = rule
|
|
1064
|
-
.split(":")
|
|
1065
|
-
.map((s) => s.trim());
|
|
1066
|
-
if (key && value) {
|
|
1067
|
-
if (key === "font-size") {
|
|
1068
|
-
attributes.fontSize = value;
|
|
1069
|
-
}
|
|
1070
|
-
else if (key === "color") {
|
|
1071
|
-
attributes.color = value;
|
|
1072
|
-
}
|
|
1073
|
-
else if (key === "background-color") {
|
|
1074
|
-
attributes.backgroundColor = value;
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
});
|
|
1078
|
-
}
|
|
1294
|
+
const processed = processNode(child);
|
|
1295
|
+
if (processed) {
|
|
1296
|
+
children.push(processed);
|
|
1079
1297
|
}
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1298
|
+
});
|
|
1299
|
+
return {
|
|
1300
|
+
type: tagName,
|
|
1301
|
+
children: children.length > 0 ? children : [],
|
|
1302
|
+
attributes: Object.keys(attributes).length > 0
|
|
1303
|
+
? attributes
|
|
1304
|
+
: undefined,
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
// Inline elements
|
|
1308
|
+
if ([
|
|
1309
|
+
"strong",
|
|
1310
|
+
"b",
|
|
1311
|
+
"em",
|
|
1312
|
+
"i",
|
|
1313
|
+
"u",
|
|
1314
|
+
"s",
|
|
1315
|
+
"del",
|
|
1316
|
+
"strike",
|
|
1317
|
+
"a",
|
|
1318
|
+
"span",
|
|
1319
|
+
].includes(tagName)) {
|
|
1094
1320
|
const children = [];
|
|
1095
1321
|
Array.from(el.childNodes).forEach((child) => {
|
|
1096
1322
|
const processed = processNode(child);
|
|
1097
1323
|
if (processed)
|
|
1098
1324
|
children.push(processed);
|
|
1099
1325
|
});
|
|
1100
|
-
|
|
1326
|
+
const attributes = {};
|
|
1327
|
+
Array.from(el.attributes).forEach((attr) => {
|
|
1328
|
+
attributes[attr.name] = attr.value;
|
|
1329
|
+
});
|
|
1330
|
+
// Links
|
|
1331
|
+
if (tagName === "a") {
|
|
1332
|
+
const href = el.getAttribute("href");
|
|
1333
|
+
if (href)
|
|
1334
|
+
attributes.href = href;
|
|
1101
1335
|
return {
|
|
1102
|
-
type:
|
|
1103
|
-
children,
|
|
1336
|
+
type: "link",
|
|
1337
|
+
children: children.length > 0 ? children : undefined,
|
|
1338
|
+
attributes: Object.keys(attributes).length > 0
|
|
1339
|
+
? attributes
|
|
1340
|
+
: undefined,
|
|
1104
1341
|
};
|
|
1105
1342
|
}
|
|
1343
|
+
// Spans: handle Lexical's white-space: pre-wrap wrapper
|
|
1344
|
+
if (tagName === "span") {
|
|
1345
|
+
const style = el.getAttribute("style") || "";
|
|
1346
|
+
const hasSemanticStyle = style.includes("font-size") ||
|
|
1347
|
+
style.includes("color") ||
|
|
1348
|
+
style.includes("background-color");
|
|
1349
|
+
// Transparent wrapper (e.g. Lexical's <span style="white-space: pre-wrap;">)
|
|
1350
|
+
// Return text content directly
|
|
1351
|
+
if (!hasSemanticStyle) {
|
|
1352
|
+
if (el.childNodes.length === 1 &&
|
|
1353
|
+
el.firstChild?.nodeType === Node.TEXT_NODE) {
|
|
1354
|
+
return {
|
|
1355
|
+
type: "text",
|
|
1356
|
+
text: el.firstChild.textContent || "",
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
// Multiple children: flatten (return each child as-is)
|
|
1360
|
+
if (children.length === 1) {
|
|
1361
|
+
return children[0];
|
|
1362
|
+
}
|
|
1363
|
+
// If multiple children, keep as span but without extra attributes
|
|
1364
|
+
if (children.length > 1) {
|
|
1365
|
+
return {
|
|
1366
|
+
type: "span",
|
|
1367
|
+
children,
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
return null;
|
|
1371
|
+
}
|
|
1372
|
+
// Semantic span: extract meaningful style properties
|
|
1373
|
+
if (style) {
|
|
1374
|
+
style.split(";").forEach((rule) => {
|
|
1375
|
+
const [key, value] = rule
|
|
1376
|
+
.split(":")
|
|
1377
|
+
.map((s) => s.trim());
|
|
1378
|
+
if (key && value) {
|
|
1379
|
+
if (key === "font-size") {
|
|
1380
|
+
attributes.fontSize = value;
|
|
1381
|
+
}
|
|
1382
|
+
else if (key === "color") {
|
|
1383
|
+
attributes.color = value;
|
|
1384
|
+
}
|
|
1385
|
+
else if (key === "background-color") {
|
|
1386
|
+
attributes.backgroundColor = value;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
// Map tag names to semantic types
|
|
1393
|
+
const type = tagName === "strong" || tagName === "b"
|
|
1394
|
+
? "bold"
|
|
1395
|
+
: tagName === "em" || tagName === "i"
|
|
1396
|
+
? "italic"
|
|
1397
|
+
: tagName === "u"
|
|
1398
|
+
? "underline"
|
|
1399
|
+
: tagName === "s" ||
|
|
1400
|
+
tagName === "del" ||
|
|
1401
|
+
tagName === "strike"
|
|
1402
|
+
? "strikethrough"
|
|
1403
|
+
: tagName;
|
|
1404
|
+
return {
|
|
1405
|
+
type,
|
|
1406
|
+
children: children.length > 0 ? children : undefined,
|
|
1407
|
+
attributes: Object.keys(attributes).length > 0
|
|
1408
|
+
? attributes
|
|
1409
|
+
: undefined,
|
|
1410
|
+
};
|
|
1106
1411
|
}
|
|
1107
1412
|
return null;
|
|
1108
1413
|
}
|
|
1414
|
+
// Process all child nodes of the editor element
|
|
1109
1415
|
Array.from(element.childNodes).forEach((node) => {
|
|
1110
1416
|
const processed = processNode(node);
|
|
1111
1417
|
if (processed) {
|
|
1418
|
+
// Wrap bare text nodes in a paragraph
|
|
1112
1419
|
if (processed.type === "text") {
|
|
1113
1420
|
blocks.push({ type: "p", children: [processed] });
|
|
1114
1421
|
}
|
|
@@ -1122,6 +1429,9 @@ function domToContent(element) {
|
|
|
1122
1429
|
}
|
|
1123
1430
|
return { blocks };
|
|
1124
1431
|
}
|
|
1432
|
+
/**
|
|
1433
|
+
* Converts EditorContent JSON to DOM and appends to the container.
|
|
1434
|
+
*/
|
|
1125
1435
|
function contentToDOM(content, container, customLinkComponent, customHeadingRenderer) {
|
|
1126
1436
|
container.innerHTML = "";
|
|
1127
1437
|
function createNode(node) {
|
|
@@ -1149,6 +1459,7 @@ function contentToDOM(content, container, customLinkComponent, customHeadingRend
|
|
|
1149
1459
|
bold: "strong",
|
|
1150
1460
|
italic: "em",
|
|
1151
1461
|
underline: "u",
|
|
1462
|
+
strikethrough: "s",
|
|
1152
1463
|
link: "a",
|
|
1153
1464
|
};
|
|
1154
1465
|
let tagName = tagMap[node.type] || node.type;
|
|
@@ -1161,19 +1472,20 @@ function contentToDOM(content, container, customLinkComponent, customHeadingRend
|
|
|
1161
1472
|
const element = document.createElement(tagName);
|
|
1162
1473
|
if (node.attributes) {
|
|
1163
1474
|
Object.entries(node.attributes).forEach(([key, value]) => {
|
|
1164
|
-
if (key === "fontSize"
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1475
|
+
if (key === "fontSize") {
|
|
1476
|
+
element.style.fontSize = value;
|
|
1477
|
+
}
|
|
1478
|
+
else if (key === "color") {
|
|
1479
|
+
element.style.color = value;
|
|
1480
|
+
}
|
|
1481
|
+
else if (key === "backgroundColor") {
|
|
1482
|
+
element.style.backgroundColor = value;
|
|
1483
|
+
}
|
|
1484
|
+
else if (key === "href" && tagName === "a") {
|
|
1485
|
+
element.setAttribute("href", value);
|
|
1486
|
+
}
|
|
1487
|
+
else if (key === "class") {
|
|
1488
|
+
element.className = value;
|
|
1177
1489
|
}
|
|
1178
1490
|
else {
|
|
1179
1491
|
element.setAttribute(key, value);
|
|
@@ -1182,32 +1494,49 @@ function contentToDOM(content, container, customLinkComponent, customHeadingRend
|
|
|
1182
1494
|
}
|
|
1183
1495
|
if (node.children) {
|
|
1184
1496
|
node.children.forEach((child) => {
|
|
1185
|
-
|
|
1186
|
-
element.appendChild(childNode);
|
|
1497
|
+
element.appendChild(createNode(child));
|
|
1187
1498
|
});
|
|
1188
1499
|
}
|
|
1500
|
+
// Set checkbox attributes on list items
|
|
1501
|
+
if (node.type === "li" &&
|
|
1502
|
+
node.attributes?.checkboxChecked !== undefined) {
|
|
1503
|
+
element.setAttribute("role", "checkbox");
|
|
1504
|
+
element.setAttribute("tabIndex", "-1");
|
|
1505
|
+
element.setAttribute("aria-checked", node.attributes.checkboxChecked);
|
|
1506
|
+
}
|
|
1189
1507
|
return element;
|
|
1190
1508
|
}
|
|
1191
1509
|
content.blocks.forEach((block) => {
|
|
1192
|
-
|
|
1193
|
-
container.appendChild(blockNode);
|
|
1510
|
+
container.appendChild(createNode(block));
|
|
1194
1511
|
});
|
|
1512
|
+
// Normalize all checkbox lists after DOM creation
|
|
1513
|
+
ensureAllCheckboxes(container);
|
|
1195
1514
|
}
|
|
1515
|
+
/**
|
|
1516
|
+
* Creates empty editor content with a single paragraph.
|
|
1517
|
+
*/
|
|
1196
1518
|
function createEmptyContent() {
|
|
1197
1519
|
return {
|
|
1198
1520
|
blocks: [{ type: "p", children: [] }],
|
|
1199
1521
|
};
|
|
1200
1522
|
}
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
return domToContent(tempDiv);
|
|
1205
|
-
}
|
|
1523
|
+
/**
|
|
1524
|
+
* Converts EditorContent to an HTML string.
|
|
1525
|
+
*/
|
|
1206
1526
|
function contentToHTML(content) {
|
|
1207
1527
|
const tempDiv = document.createElement("div");
|
|
1208
1528
|
contentToDOM(content, tempDiv);
|
|
1209
1529
|
return tempDiv.innerHTML;
|
|
1210
1530
|
}
|
|
1531
|
+
/**
|
|
1532
|
+
* Converts an HTML string to EditorContent.
|
|
1533
|
+
* Supports Lexical, GitHub, and standard HTML formats.
|
|
1534
|
+
*/
|
|
1535
|
+
function htmlToContent(htmlString) {
|
|
1536
|
+
const tempDiv = document.createElement("div");
|
|
1537
|
+
tempDiv.innerHTML = htmlString;
|
|
1538
|
+
return domToContent(tempDiv);
|
|
1539
|
+
}
|
|
1211
1540
|
|
|
1212
1541
|
class HistoryManager {
|
|
1213
1542
|
constructor() {
|
|
@@ -1417,21 +1746,484 @@ function outdentListItem(selection) {
|
|
|
1417
1746
|
return true;
|
|
1418
1747
|
}
|
|
1419
1748
|
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1749
|
+
/** Debounce time for pushing to history after input (ms) */
|
|
1750
|
+
const HISTORY_DEBOUNCE_MS = 300;
|
|
1751
|
+
/** Width of the clickable checkbox area in pixels */
|
|
1752
|
+
const CHECKBOX_CLICK_ZONE_PX = 40;
|
|
1753
|
+
|
|
1754
|
+
/**
|
|
1755
|
+
* Hook that manages all checkbox list interactions.
|
|
1756
|
+
* Consolidates click handling, keyboard navigation, and checkbox insertion.
|
|
1757
|
+
* Uses event delegation (single listener on editor root) for all checkbox events.
|
|
1758
|
+
*/
|
|
1759
|
+
function useCheckbox({ editorRef, isUpdatingRef, pushToHistory, notifyChange, getDomContent, }) {
|
|
1760
|
+
// --- Event Listeners (click + pointerdown) ---
|
|
1423
1761
|
useEffect(() => {
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1762
|
+
const editor = editorRef.current;
|
|
1763
|
+
if (!editor)
|
|
1764
|
+
return;
|
|
1765
|
+
const handleClick = (event) => {
|
|
1766
|
+
const clickTarget = event.target;
|
|
1767
|
+
if (!(clickTarget instanceof HTMLElement))
|
|
1768
|
+
return;
|
|
1769
|
+
const listItem = clickTarget.tagName === "LI"
|
|
1770
|
+
? clickTarget
|
|
1771
|
+
: clickTarget.closest("li");
|
|
1772
|
+
if (!listItem)
|
|
1773
|
+
return;
|
|
1774
|
+
const parentNode = listItem.parentElement;
|
|
1775
|
+
if (!parentNode || !isCheckboxList(parentNode))
|
|
1776
|
+
return;
|
|
1777
|
+
if (listItem.getAttribute("role") !== "checkbox")
|
|
1778
|
+
return;
|
|
1779
|
+
if (isNestedListItem(listItem))
|
|
1780
|
+
return;
|
|
1781
|
+
const rect = listItem.getBoundingClientRect();
|
|
1782
|
+
const clientX = event.clientX;
|
|
1783
|
+
const isInCheckboxArea = listItem.dir === "rtl"
|
|
1784
|
+
? clientX >= rect.right - CHECKBOX_CLICK_ZONE_PX
|
|
1785
|
+
: clientX <= rect.left + CHECKBOX_CLICK_ZONE_PX;
|
|
1786
|
+
if (isInCheckboxArea) {
|
|
1787
|
+
event.preventDefault();
|
|
1788
|
+
event.stopPropagation();
|
|
1789
|
+
toggleListItemChecked(listItem);
|
|
1790
|
+
// Trigger change event
|
|
1791
|
+
const changeEvent = new Event("input", { bubbles: true });
|
|
1792
|
+
editor.dispatchEvent(changeEvent);
|
|
1793
|
+
}
|
|
1427
1794
|
};
|
|
1428
|
-
const
|
|
1429
|
-
|
|
1795
|
+
const handlePointerDown = (event) => {
|
|
1796
|
+
const target = event.target;
|
|
1797
|
+
if (!(target instanceof HTMLElement))
|
|
1798
|
+
return;
|
|
1799
|
+
const listItem = target.tagName === "LI"
|
|
1800
|
+
? target
|
|
1801
|
+
: target.closest("li");
|
|
1802
|
+
if (!listItem)
|
|
1803
|
+
return;
|
|
1804
|
+
if (isNestedListItem(listItem))
|
|
1805
|
+
return;
|
|
1806
|
+
const parentNode = listItem.parentElement;
|
|
1807
|
+
if (!parentNode || !isCheckboxList(parentNode))
|
|
1808
|
+
return;
|
|
1809
|
+
const rect = listItem.getBoundingClientRect();
|
|
1810
|
+
const clientX = event.clientX;
|
|
1811
|
+
const isInCheckboxArea = listItem.dir === "rtl"
|
|
1812
|
+
? clientX >= rect.right - CHECKBOX_CLICK_ZONE_PX
|
|
1813
|
+
: clientX <= rect.left + CHECKBOX_CLICK_ZONE_PX;
|
|
1814
|
+
if (isInCheckboxArea) {
|
|
1815
|
+
event.preventDefault();
|
|
1816
|
+
}
|
|
1430
1817
|
};
|
|
1431
|
-
|
|
1432
|
-
|
|
1818
|
+
editor.addEventListener("click", handleClick, true);
|
|
1819
|
+
editor.addEventListener("pointerdown", handlePointerDown, true);
|
|
1820
|
+
return () => {
|
|
1821
|
+
editor.removeEventListener("click", handleClick, true);
|
|
1822
|
+
editor.removeEventListener("pointerdown", handlePointerDown, true);
|
|
1433
1823
|
};
|
|
1434
|
-
|
|
1824
|
+
}, [editorRef]);
|
|
1825
|
+
// --- Keyboard handlers (called from useEditorEvents) ---
|
|
1826
|
+
/**
|
|
1827
|
+
* Handles checkbox-specific keyboard events.
|
|
1828
|
+
* Returns true if the event was handled and should not propagate.
|
|
1829
|
+
*/
|
|
1830
|
+
const handleCheckboxKeyDown = useCallback((e) => {
|
|
1831
|
+
// Arrow up/down: navigate between checkbox items
|
|
1832
|
+
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
1833
|
+
const activeItem = getActiveCheckListItem();
|
|
1834
|
+
if (activeItem) {
|
|
1835
|
+
const backward = e.key === "ArrowUp";
|
|
1836
|
+
const nextItem = findCheckListItemSibling(activeItem, backward);
|
|
1837
|
+
if (nextItem) {
|
|
1838
|
+
e.preventDefault();
|
|
1839
|
+
nextItem.focus();
|
|
1840
|
+
return true;
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
// Space: toggle checkbox
|
|
1845
|
+
if (e.key === " ") {
|
|
1846
|
+
const activeItem = getActiveCheckListItem();
|
|
1847
|
+
if (activeItem) {
|
|
1848
|
+
e.preventDefault();
|
|
1849
|
+
toggleListItemChecked(activeItem);
|
|
1850
|
+
const editorEl = activeItem.closest(".rte-editor");
|
|
1851
|
+
if (editorEl) {
|
|
1852
|
+
editorEl.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1853
|
+
}
|
|
1854
|
+
return true;
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
// Escape: blur from checkbox item
|
|
1858
|
+
if (e.key === "Escape") {
|
|
1859
|
+
const activeItem = getActiveCheckListItem();
|
|
1860
|
+
if (activeItem) {
|
|
1861
|
+
const editorEl = activeItem.closest(".rte-editor");
|
|
1862
|
+
if (editorEl instanceof HTMLElement) {
|
|
1863
|
+
editorEl.focus();
|
|
1864
|
+
}
|
|
1865
|
+
return true;
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
// Arrow left: focus list item when cursor is at the start
|
|
1869
|
+
if (e.key === "ArrowLeft") {
|
|
1870
|
+
const selection = window.getSelection();
|
|
1871
|
+
if (!selection || selection.rangeCount === 0)
|
|
1872
|
+
return false;
|
|
1873
|
+
const range = selection.getRangeAt(0);
|
|
1874
|
+
const container = range.commonAncestorContainer;
|
|
1875
|
+
const listItem = findClosestListItem(container);
|
|
1876
|
+
if (!listItem)
|
|
1877
|
+
return false;
|
|
1878
|
+
const parent = listItem.parentElement;
|
|
1879
|
+
if (parent &&
|
|
1880
|
+
isCheckboxList(parent) &&
|
|
1881
|
+
range.collapsed &&
|
|
1882
|
+
range.startOffset === 0) {
|
|
1883
|
+
if (document.activeElement !== listItem) {
|
|
1884
|
+
listItem.focus();
|
|
1885
|
+
e.preventDefault();
|
|
1886
|
+
return true;
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
return false;
|
|
1891
|
+
}, []);
|
|
1892
|
+
/**
|
|
1893
|
+
* Handles Enter key in checkbox lists.
|
|
1894
|
+
* Creates a new unchecked item after the current one.
|
|
1895
|
+
* Returns true if the event was handled.
|
|
1896
|
+
*/
|
|
1897
|
+
const handleCheckboxEnter = useCallback((e) => {
|
|
1898
|
+
if (e.key !== "Enter")
|
|
1899
|
+
return false;
|
|
1900
|
+
const selection = window.getSelection();
|
|
1901
|
+
if (!selection || selection.rangeCount === 0)
|
|
1902
|
+
return false;
|
|
1903
|
+
const range = selection.getRangeAt(0);
|
|
1904
|
+
const container = range.commonAncestorContainer;
|
|
1905
|
+
const listItem = findClosestListItem(container);
|
|
1906
|
+
if (!listItem)
|
|
1907
|
+
return false;
|
|
1908
|
+
const checkboxList = findClosestCheckboxList(listItem);
|
|
1909
|
+
if (!checkboxList)
|
|
1910
|
+
return false;
|
|
1911
|
+
e.preventDefault();
|
|
1912
|
+
const newLi = document.createElement("li");
|
|
1913
|
+
updateListItemChecked(newLi, false);
|
|
1914
|
+
const textNode = document.createTextNode(" ");
|
|
1915
|
+
newLi.appendChild(textNode);
|
|
1916
|
+
if (listItem.nextSibling) {
|
|
1917
|
+
checkboxList.insertBefore(newLi, listItem.nextSibling);
|
|
1918
|
+
}
|
|
1919
|
+
else {
|
|
1920
|
+
checkboxList.appendChild(newLi);
|
|
1921
|
+
}
|
|
1922
|
+
const editor = editorRef.current;
|
|
1923
|
+
if (editor)
|
|
1924
|
+
ensureAllCheckboxes(editor);
|
|
1925
|
+
const newRange = document.createRange();
|
|
1926
|
+
newRange.setStart(textNode, 0);
|
|
1927
|
+
newRange.collapse(true);
|
|
1928
|
+
selection.removeAllRanges();
|
|
1929
|
+
selection.addRange(newRange);
|
|
1930
|
+
if (editor) {
|
|
1931
|
+
const content = getDomContent();
|
|
1932
|
+
pushToHistory(content);
|
|
1933
|
+
notifyChange(content);
|
|
1934
|
+
}
|
|
1935
|
+
return true;
|
|
1936
|
+
}, [editorRef, getDomContent, pushToHistory, notifyChange]);
|
|
1937
|
+
/**
|
|
1938
|
+
* Inserts a new checkbox list at the current cursor position,
|
|
1939
|
+
* or removes it if already inside one.
|
|
1940
|
+
* Returns true on success.
|
|
1941
|
+
*/
|
|
1942
|
+
const insertCheckboxList = useCallback((editor) => {
|
|
1943
|
+
isUpdatingRef.current = true;
|
|
1944
|
+
const selection = window.getSelection();
|
|
1945
|
+
if (!selection || selection.rangeCount === 0) {
|
|
1946
|
+
isUpdatingRef.current = false;
|
|
1947
|
+
return false;
|
|
1948
|
+
}
|
|
1949
|
+
const range = selection.getRangeAt(0);
|
|
1950
|
+
const container = range.commonAncestorContainer;
|
|
1951
|
+
const element = container.nodeType === Node.TEXT_NODE
|
|
1952
|
+
? container.parentElement
|
|
1953
|
+
: container;
|
|
1954
|
+
if (!element) {
|
|
1955
|
+
isUpdatingRef.current = false;
|
|
1956
|
+
return false;
|
|
1957
|
+
}
|
|
1958
|
+
// Already in a checkbox list? Remove it.
|
|
1959
|
+
const existingList = findClosestCheckboxList(element);
|
|
1960
|
+
if (existingList) {
|
|
1961
|
+
existingList.classList.remove("rte-checkbox-list");
|
|
1962
|
+
existingList
|
|
1963
|
+
.querySelectorAll("li[role='checkbox']")
|
|
1964
|
+
.forEach((li) => {
|
|
1965
|
+
li.removeAttribute("role");
|
|
1966
|
+
li.removeAttribute("tabIndex");
|
|
1967
|
+
li.removeAttribute("aria-checked");
|
|
1968
|
+
});
|
|
1969
|
+
isUpdatingRef.current = false;
|
|
1970
|
+
return true;
|
|
1971
|
+
}
|
|
1972
|
+
// Create new checkbox list
|
|
1973
|
+
const ul = document.createElement("ul");
|
|
1974
|
+
ul.classList.add("rte-checkbox-list");
|
|
1975
|
+
const li = document.createElement("li");
|
|
1976
|
+
updateListItemChecked(li, false);
|
|
1977
|
+
const textNode = document.createTextNode(" ");
|
|
1978
|
+
li.appendChild(textNode);
|
|
1979
|
+
ul.appendChild(li);
|
|
1980
|
+
// Find block element to replace
|
|
1981
|
+
const blockElement = element.closest("p, div, h1, h2, h3, h4, h5, h6, blockquote");
|
|
1982
|
+
const isValidBlockElement = blockElement &&
|
|
1983
|
+
blockElement !== editor &&
|
|
1984
|
+
editor.contains(blockElement) &&
|
|
1985
|
+
blockElement.parentElement;
|
|
1986
|
+
if (isValidBlockElement) {
|
|
1987
|
+
const textContent = blockElement.textContent || "";
|
|
1988
|
+
blockElement.parentElement.replaceChild(ul, blockElement);
|
|
1989
|
+
const finalTextNode = li.firstChild;
|
|
1990
|
+
if (finalTextNode) {
|
|
1991
|
+
finalTextNode.textContent = textContent || " ";
|
|
1992
|
+
const cursorPos = textContent ? textContent.length : 0;
|
|
1993
|
+
setCursorInTextNode(finalTextNode, cursorPos, editor);
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
else {
|
|
1997
|
+
try {
|
|
1998
|
+
range.deleteContents();
|
|
1999
|
+
range.insertNode(ul);
|
|
2000
|
+
const finalTextNode = li.firstChild;
|
|
2001
|
+
if (finalTextNode) {
|
|
2002
|
+
setCursorInTextNode(finalTextNode, 0, editor);
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
catch (_) {
|
|
2006
|
+
editor.appendChild(ul);
|
|
2007
|
+
const finalTextNode = li.firstChild;
|
|
2008
|
+
if (finalTextNode) {
|
|
2009
|
+
setCursorInTextNode(finalTextNode, 0, editor);
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
// After insertion: ensure attributes and save to history
|
|
2014
|
+
setTimeout(() => {
|
|
2015
|
+
if (!editor)
|
|
2016
|
+
return;
|
|
2017
|
+
ensureAllCheckboxes(editor);
|
|
2018
|
+
const content = getDomContent();
|
|
2019
|
+
pushToHistory(content);
|
|
2020
|
+
isUpdatingRef.current = false;
|
|
2021
|
+
notifyChange(content);
|
|
2022
|
+
}, 100);
|
|
2023
|
+
return true;
|
|
2024
|
+
}, [isUpdatingRef, getDomContent, pushToHistory, notifyChange]);
|
|
2025
|
+
return {
|
|
2026
|
+
ensureAllCheckboxes,
|
|
2027
|
+
insertCheckboxList,
|
|
2028
|
+
handleCheckboxKeyDown,
|
|
2029
|
+
handleCheckboxEnter,
|
|
2030
|
+
updateListItemChecked,
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
/**
|
|
2035
|
+
* Hook that sets up input, keyup, and keydown event listeners on the editor.
|
|
2036
|
+
*/
|
|
2037
|
+
function useEditorEvents({ editorRef, historyRef, isUpdatingRef, notifyChange, handleCheckboxKeyDown, handleCheckboxEnter, undo, redo, }) {
|
|
2038
|
+
useEffect(() => {
|
|
2039
|
+
const editor = editorRef.current;
|
|
2040
|
+
if (!editor)
|
|
2041
|
+
return;
|
|
2042
|
+
let inputTimeout = null;
|
|
2043
|
+
const handleInput = () => {
|
|
2044
|
+
setTimeout(() => {
|
|
2045
|
+
const content = domToContent(editor);
|
|
2046
|
+
notifyChange(content);
|
|
2047
|
+
if (inputTimeout)
|
|
2048
|
+
clearTimeout(inputTimeout);
|
|
2049
|
+
inputTimeout = setTimeout(() => {
|
|
2050
|
+
historyRef.current.push(content);
|
|
2051
|
+
inputTimeout = null;
|
|
2052
|
+
}, HISTORY_DEBOUNCE_MS);
|
|
2053
|
+
}, 0);
|
|
2054
|
+
};
|
|
2055
|
+
const handleKeyDown = (e) => {
|
|
2056
|
+
const isModifierPressed = e.metaKey || e.ctrlKey;
|
|
2057
|
+
// Checkbox Enter: create new checkbox item
|
|
2058
|
+
if (handleCheckboxEnter(e))
|
|
2059
|
+
return;
|
|
2060
|
+
// Checkbox keyboard navigation
|
|
2061
|
+
if (handleCheckboxKeyDown(e))
|
|
2062
|
+
return;
|
|
2063
|
+
// Tab: indent/outdent in lists
|
|
2064
|
+
if (e.key === "Tab" && !isModifierPressed && !e.altKey) {
|
|
2065
|
+
e.preventDefault();
|
|
2066
|
+
e.stopPropagation();
|
|
2067
|
+
e.stopImmediatePropagation();
|
|
2068
|
+
const selection = window.getSelection();
|
|
2069
|
+
if (!selection || selection.rangeCount === 0)
|
|
2070
|
+
return;
|
|
2071
|
+
const range = selection.getRangeAt(0);
|
|
2072
|
+
const container = range.commonAncestorContainer;
|
|
2073
|
+
if (!editor.contains(container))
|
|
2074
|
+
return;
|
|
2075
|
+
const listItem = container.nodeType === Node.TEXT_NODE
|
|
2076
|
+
? container.parentElement?.closest("li")
|
|
2077
|
+
: container.closest("li");
|
|
2078
|
+
if (listItem && editor.contains(listItem)) {
|
|
2079
|
+
const currentContent = domToContent(editor);
|
|
2080
|
+
historyRef.current.push(currentContent);
|
|
2081
|
+
if (e.shiftKey) {
|
|
2082
|
+
outdentListItem(selection);
|
|
2083
|
+
}
|
|
2084
|
+
else {
|
|
2085
|
+
indentListItem(selection);
|
|
2086
|
+
}
|
|
2087
|
+
setTimeout(() => {
|
|
2088
|
+
if (editor) {
|
|
2089
|
+
const content = domToContent(editor);
|
|
2090
|
+
notifyChange(content);
|
|
2091
|
+
}
|
|
2092
|
+
}, 0);
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
// Undo/Redo shortcuts
|
|
2097
|
+
if (isModifierPressed && e.key === "z" && !e.shiftKey) {
|
|
2098
|
+
e.preventDefault();
|
|
2099
|
+
e.stopPropagation();
|
|
2100
|
+
undo();
|
|
2101
|
+
}
|
|
2102
|
+
else if (isModifierPressed &&
|
|
2103
|
+
(e.key === "y" || (e.key === "z" && e.shiftKey))) {
|
|
2104
|
+
e.preventDefault();
|
|
2105
|
+
e.stopPropagation();
|
|
2106
|
+
redo();
|
|
2107
|
+
}
|
|
2108
|
+
};
|
|
2109
|
+
editor.addEventListener("input", handleInput);
|
|
2110
|
+
editor.addEventListener("keyup", handleInput);
|
|
2111
|
+
editor.addEventListener("keydown", handleKeyDown, true);
|
|
2112
|
+
return () => {
|
|
2113
|
+
editor.removeEventListener("input", handleInput);
|
|
2114
|
+
editor.removeEventListener("keyup", handleInput);
|
|
2115
|
+
editor.removeEventListener("keydown", handleKeyDown, true);
|
|
2116
|
+
if (inputTimeout)
|
|
2117
|
+
clearTimeout(inputTimeout);
|
|
2118
|
+
};
|
|
2119
|
+
}, [
|
|
2120
|
+
editorRef,
|
|
2121
|
+
historyRef,
|
|
2122
|
+
isUpdatingRef,
|
|
2123
|
+
notifyChange,
|
|
2124
|
+
handleCheckboxKeyDown,
|
|
2125
|
+
handleCheckboxEnter,
|
|
2126
|
+
undo,
|
|
2127
|
+
redo,
|
|
2128
|
+
]);
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
/**
|
|
2132
|
+
* Hook that initializes the editor with initial content and sets up the MutationObserver.
|
|
2133
|
+
* Runs once on mount.
|
|
2134
|
+
*/
|
|
2135
|
+
function useEditorInit({ editorRef, historyRef, isUpdatingRef, initialContent, customLinkComponent, customHeadingRenderer, }) {
|
|
2136
|
+
const isInitializedRef = useRef(false);
|
|
2137
|
+
useEffect(() => {
|
|
2138
|
+
const editor = editorRef.current;
|
|
2139
|
+
if (!editor || isInitializedRef.current)
|
|
2140
|
+
return;
|
|
2141
|
+
// Load initial content
|
|
2142
|
+
const content = initialContent || createEmptyContent();
|
|
2143
|
+
isUpdatingRef.current = true;
|
|
2144
|
+
contentToDOM(content, editor, customLinkComponent);
|
|
2145
|
+
historyRef.current.push(content);
|
|
2146
|
+
isUpdatingRef.current = false;
|
|
2147
|
+
isInitializedRef.current = true;
|
|
2148
|
+
// Normalize all checkbox lists (own + Lexical + GitHub formats)
|
|
2149
|
+
ensureAllCheckboxes(editor);
|
|
2150
|
+
// MutationObserver: ensure new checkbox list items get correct attributes
|
|
2151
|
+
const observer = new MutationObserver((mutations) => {
|
|
2152
|
+
if (isUpdatingRef.current)
|
|
2153
|
+
return;
|
|
2154
|
+
let needsUpdate = false;
|
|
2155
|
+
for (const mutation of mutations) {
|
|
2156
|
+
for (let i = 0; i < mutation.addedNodes.length; i++) {
|
|
2157
|
+
const node = mutation.addedNodes[i];
|
|
2158
|
+
if (node.nodeType !== Node.ELEMENT_NODE)
|
|
2159
|
+
continue;
|
|
2160
|
+
const el = node;
|
|
2161
|
+
if (el.tagName === "LI" &&
|
|
2162
|
+
el.closest("ul.rte-checkbox-list")) {
|
|
2163
|
+
needsUpdate = true;
|
|
2164
|
+
break;
|
|
2165
|
+
}
|
|
2166
|
+
if (el.tagName === "UL" &&
|
|
2167
|
+
el.classList.contains("rte-checkbox-list")) {
|
|
2168
|
+
needsUpdate = true;
|
|
2169
|
+
break;
|
|
2170
|
+
}
|
|
2171
|
+
if (el.querySelector("ul.rte-checkbox-list li")) {
|
|
2172
|
+
needsUpdate = true;
|
|
2173
|
+
break;
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
if (needsUpdate)
|
|
2177
|
+
break;
|
|
2178
|
+
}
|
|
2179
|
+
if (needsUpdate) {
|
|
2180
|
+
setTimeout(() => ensureAllCheckboxes(editor), 0);
|
|
2181
|
+
}
|
|
2182
|
+
});
|
|
2183
|
+
observer.observe(editor, { childList: true, subtree: true });
|
|
2184
|
+
return () => {
|
|
2185
|
+
observer.disconnect();
|
|
2186
|
+
};
|
|
2187
|
+
// Only run once on mount
|
|
2188
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2189
|
+
}, []);
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
/**
|
|
2193
|
+
* Hook for editor selection management.
|
|
2194
|
+
*/
|
|
2195
|
+
function useEditorSelection() {
|
|
2196
|
+
const restoreSelection = useCallback((editor) => {
|
|
2197
|
+
if (typeof window === "undefined" ||
|
|
2198
|
+
typeof document === "undefined")
|
|
2199
|
+
return;
|
|
2200
|
+
const range = document.createRange();
|
|
2201
|
+
const selection = window.getSelection();
|
|
2202
|
+
if (editor.firstChild) {
|
|
2203
|
+
range.setStart(editor.firstChild, 0);
|
|
2204
|
+
range.collapse(true);
|
|
2205
|
+
selection?.removeAllRanges();
|
|
2206
|
+
selection?.addRange(range);
|
|
2207
|
+
}
|
|
2208
|
+
}, []);
|
|
2209
|
+
return { restoreSelection };
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
const Toolbar = ({ plugins, editorAPI, className, }) => {
|
|
2213
|
+
const [updateTrigger, setUpdateTrigger] = useState(0);
|
|
2214
|
+
const [isClient, setIsClient] = useState(false);
|
|
2215
|
+
useEffect(() => {
|
|
2216
|
+
setIsClient(true);
|
|
2217
|
+
const handleSelectionChange = () => {
|
|
2218
|
+
setUpdateTrigger((prev) => prev + 1);
|
|
2219
|
+
};
|
|
2220
|
+
const handleMouseUp = () => {
|
|
2221
|
+
setTimeout(handleSelectionChange, 10);
|
|
2222
|
+
};
|
|
2223
|
+
const handleKeyUp = () => {
|
|
2224
|
+
setTimeout(handleSelectionChange, 10);
|
|
2225
|
+
};
|
|
2226
|
+
if (typeof document !== 'undefined') {
|
|
1435
2227
|
document.addEventListener("selectionchange", handleSelectionChange);
|
|
1436
2228
|
document.addEventListener("mouseup", handleMouseUp);
|
|
1437
2229
|
document.addEventListener("keyup", handleKeyUp);
|
|
@@ -1499,222 +2291,135 @@ const Toolbar = ({ plugins, editorAPI, className, }) => {
|
|
|
1499
2291
|
};
|
|
1500
2292
|
|
|
1501
2293
|
const Editor = ({ initialContent, onChange, plugins: providedPlugins, placeholder = "Text eingeben...", className, toolbarClassName, editorClassName, fontSizes, colors, headings, customLinkComponent, customHeadingRenderer, customRenderer, onEditorAPIReady, theme, onImageUpload, }) => {
|
|
2294
|
+
// --- Shared Refs ---
|
|
2295
|
+
const editorRef = useRef(null);
|
|
2296
|
+
const historyRef = useRef(new HistoryManager());
|
|
2297
|
+
const isUpdatingRef = useRef(false);
|
|
2298
|
+
// --- Plugins ---
|
|
1502
2299
|
const plugins = useMemo(() => {
|
|
1503
2300
|
const allPlugins = [...(providedPlugins || defaultPlugins)];
|
|
2301
|
+
if (headings && headings.length > 0) {
|
|
2302
|
+
const blockFormatIndex = allPlugins.findIndex((p) => p.name === "blockFormat");
|
|
2303
|
+
if (blockFormatIndex !== -1) {
|
|
2304
|
+
allPlugins[blockFormatIndex] =
|
|
2305
|
+
createBlockFormatPlugin(headings);
|
|
2306
|
+
}
|
|
2307
|
+
else {
|
|
2308
|
+
const redoIndex = allPlugins.findIndex((p) => p.name === "redo");
|
|
2309
|
+
if (redoIndex !== -1) {
|
|
2310
|
+
allPlugins.splice(redoIndex + 1, 0, createBlockFormatPlugin(headings));
|
|
2311
|
+
}
|
|
2312
|
+
else {
|
|
2313
|
+
allPlugins.push(createBlockFormatPlugin(headings));
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
1504
2317
|
if (fontSizes && fontSizes.length > 0) {
|
|
1505
|
-
allPlugins.
|
|
2318
|
+
const blockFormatIndex = allPlugins.findIndex((p) => p.name === "blockFormat");
|
|
2319
|
+
if (blockFormatIndex !== -1) {
|
|
2320
|
+
allPlugins.splice(blockFormatIndex + 1, 0, createFontSizePlugin(fontSizes));
|
|
2321
|
+
}
|
|
2322
|
+
else {
|
|
2323
|
+
allPlugins.push(createFontSizePlugin(fontSizes));
|
|
2324
|
+
}
|
|
1506
2325
|
}
|
|
1507
2326
|
if (colors && colors.length > 0) {
|
|
1508
2327
|
allPlugins.push(createTextColorPlugin(colors));
|
|
1509
2328
|
allPlugins.push(createBackgroundColorPlugin(colors));
|
|
1510
2329
|
}
|
|
1511
|
-
// BlockFormat Plugin ist bereits in defaultPlugins enthalten
|
|
1512
|
-
// Wenn custom headings angegeben sind, ersetze das Standard-Plugin
|
|
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));
|
|
1521
|
-
}
|
|
1522
2330
|
allPlugins.push(createImagePlugin(onImageUpload));
|
|
1523
2331
|
return allPlugins;
|
|
1524
2332
|
}, [providedPlugins, fontSizes, colors, headings, onImageUpload]);
|
|
1525
|
-
|
|
1526
|
-
const historyRef = useRef(new HistoryManager());
|
|
1527
|
-
const isUpdatingRef = useRef(false);
|
|
2333
|
+
// --- Callbacks ---
|
|
1528
2334
|
const notifyChange = useCallback((content) => {
|
|
1529
|
-
if (onChange
|
|
2335
|
+
if (onChange)
|
|
1530
2336
|
onChange(content);
|
|
1531
|
-
}
|
|
1532
2337
|
}, [onChange]);
|
|
1533
|
-
const
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
selection?.removeAllRanges();
|
|
1542
|
-
selection?.addRange(range);
|
|
1543
|
-
}
|
|
2338
|
+
const getDomContent = useCallback(() => {
|
|
2339
|
+
const editor = editorRef.current;
|
|
2340
|
+
if (!editor)
|
|
2341
|
+
return createEmptyContent();
|
|
2342
|
+
return domToContent(editor);
|
|
2343
|
+
}, []);
|
|
2344
|
+
const pushToHistory = useCallback((content) => {
|
|
2345
|
+
historyRef.current.push(content);
|
|
1544
2346
|
}, []);
|
|
2347
|
+
// --- Hooks ---
|
|
2348
|
+
const { restoreSelection } = useEditorSelection();
|
|
2349
|
+
const checkbox = useCheckbox({
|
|
2350
|
+
editorRef,
|
|
2351
|
+
isUpdatingRef,
|
|
2352
|
+
pushToHistory,
|
|
2353
|
+
notifyChange,
|
|
2354
|
+
getDomContent,
|
|
2355
|
+
});
|
|
2356
|
+
// --- Undo / Redo ---
|
|
2357
|
+
const undo = useCallback(() => {
|
|
2358
|
+
const content = historyRef.current.undo();
|
|
2359
|
+
const editor = editorRef.current;
|
|
2360
|
+
if (content && editor) {
|
|
2361
|
+
isUpdatingRef.current = true;
|
|
2362
|
+
contentToDOM(content, editor, customLinkComponent);
|
|
2363
|
+
restoreSelection(editor);
|
|
2364
|
+
isUpdatingRef.current = false;
|
|
2365
|
+
notifyChange(content);
|
|
2366
|
+
}
|
|
2367
|
+
}, [
|
|
2368
|
+
customLinkComponent,
|
|
2369
|
+
customHeadingRenderer,
|
|
2370
|
+
restoreSelection,
|
|
2371
|
+
notifyChange,
|
|
2372
|
+
]);
|
|
2373
|
+
const redo = useCallback(() => {
|
|
2374
|
+
const content = historyRef.current.redo();
|
|
2375
|
+
const editor = editorRef.current;
|
|
2376
|
+
if (content && editor) {
|
|
2377
|
+
isUpdatingRef.current = true;
|
|
2378
|
+
contentToDOM(content, editor, customLinkComponent);
|
|
2379
|
+
restoreSelection(editor);
|
|
2380
|
+
isUpdatingRef.current = false;
|
|
2381
|
+
notifyChange(content);
|
|
2382
|
+
}
|
|
2383
|
+
}, [
|
|
2384
|
+
customLinkComponent,
|
|
2385
|
+
customHeadingRenderer,
|
|
2386
|
+
restoreSelection,
|
|
2387
|
+
notifyChange,
|
|
2388
|
+
]);
|
|
2389
|
+
// --- Editor API ---
|
|
1545
2390
|
const editorAPI = useMemo(() => {
|
|
1546
2391
|
const executeCommand = (command, value) => {
|
|
1547
2392
|
const editor = editorRef.current;
|
|
1548
2393
|
if (!editor)
|
|
1549
2394
|
return false;
|
|
2395
|
+
// Save history before non-history commands
|
|
1550
2396
|
if (command !== "undo" &&
|
|
1551
2397
|
command !== "redo" &&
|
|
1552
|
-
command !== "insertImage"
|
|
2398
|
+
command !== "insertImage" &&
|
|
2399
|
+
command !== "insertCheckboxList") {
|
|
1553
2400
|
const currentContent = domToContent(editor);
|
|
1554
2401
|
historyRef.current.push(currentContent);
|
|
1555
2402
|
}
|
|
1556
2403
|
if (command === "undo") {
|
|
1557
|
-
|
|
1558
|
-
if (content && editor) {
|
|
1559
|
-
isUpdatingRef.current = true;
|
|
1560
|
-
contentToDOM(content, editor, customLinkComponent);
|
|
1561
|
-
restoreSelection(editor);
|
|
1562
|
-
isUpdatingRef.current = false;
|
|
1563
|
-
notifyChange(content);
|
|
1564
|
-
}
|
|
2404
|
+
undo();
|
|
1565
2405
|
return true;
|
|
1566
2406
|
}
|
|
1567
2407
|
if (command === "redo") {
|
|
1568
|
-
|
|
1569
|
-
if (content && editor) {
|
|
1570
|
-
isUpdatingRef.current = true;
|
|
1571
|
-
contentToDOM(content, editor, customLinkComponent);
|
|
1572
|
-
restoreSelection(editor);
|
|
1573
|
-
isUpdatingRef.current = false;
|
|
1574
|
-
notifyChange(content);
|
|
1575
|
-
}
|
|
2408
|
+
redo();
|
|
1576
2409
|
return true;
|
|
1577
2410
|
}
|
|
1578
|
-
if (command === "
|
|
1579
|
-
|
|
1580
|
-
if (!selection)
|
|
1581
|
-
return false;
|
|
1582
|
-
if (document.activeElement !== editor) {
|
|
1583
|
-
editor.focus();
|
|
1584
|
-
}
|
|
1585
|
-
if (selection.rangeCount === 0) {
|
|
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
|
-
}
|
|
1662
|
-
}
|
|
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
|
-
}
|
|
1687
|
-
if (!selection || selection.rangeCount === 0) {
|
|
1688
|
-
const range = document.createRange();
|
|
1689
|
-
if (editor.childNodes.length > 0) {
|
|
1690
|
-
const lastChild = editor.childNodes[editor.childNodes.length - 1];
|
|
1691
|
-
if (lastChild.nodeType === Node.TEXT_NODE) {
|
|
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);
|
|
1707
|
-
}
|
|
1708
|
-
selection?.removeAllRanges();
|
|
1709
|
-
selection?.addRange(range);
|
|
2411
|
+
if (command === "insertCheckboxList") {
|
|
2412
|
+
return checkbox.insertCheckboxList(editor);
|
|
1710
2413
|
}
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
selection.addRange(savedRange);
|
|
2414
|
+
if (command === "insertImage" && value) {
|
|
2415
|
+
return handleInsertImage(editor, value, isUpdatingRef, historyRef, notifyChange);
|
|
1714
2416
|
}
|
|
2417
|
+
// General commands via document.execCommand
|
|
2418
|
+
ensureEditorFocused(editor);
|
|
1715
2419
|
document.execCommand(command, false, value);
|
|
1716
2420
|
setTimeout(() => {
|
|
1717
2421
|
if (editor && !isUpdatingRef.current) {
|
|
2422
|
+
ensureAllCheckboxes(editor);
|
|
1718
2423
|
const content = domToContent(editor);
|
|
1719
2424
|
notifyChange(content);
|
|
1720
2425
|
}
|
|
@@ -1732,6 +2437,7 @@ const Editor = ({ initialContent, onChange, plugins: providedPlugins, placeholde
|
|
|
1732
2437
|
const editor = editorRef.current;
|
|
1733
2438
|
if (!editor)
|
|
1734
2439
|
return createEmptyContent();
|
|
2440
|
+
ensureAllCheckboxes(editor);
|
|
1735
2441
|
return domToContent(editor);
|
|
1736
2442
|
},
|
|
1737
2443
|
setContent: (content) => {
|
|
@@ -1751,23 +2457,20 @@ const Editor = ({ initialContent, onChange, plugins: providedPlugins, placeholde
|
|
|
1751
2457
|
const range = selection.getRangeAt(0);
|
|
1752
2458
|
const block = document.createElement(type);
|
|
1753
2459
|
if (attributes) {
|
|
1754
|
-
Object.entries(attributes).forEach(([key,
|
|
1755
|
-
block.setAttribute(key,
|
|
2460
|
+
Object.entries(attributes).forEach(([key, val]) => {
|
|
2461
|
+
block.setAttribute(key, val);
|
|
1756
2462
|
});
|
|
1757
2463
|
}
|
|
1758
2464
|
range.insertNode(block);
|
|
1759
|
-
const textNode = document.createTextNode("\u200B");
|
|
2465
|
+
const textNode = document.createTextNode("\u200B");
|
|
1760
2466
|
block.appendChild(textNode);
|
|
1761
|
-
// Cursor setzen
|
|
1762
2467
|
range.setStartAfter(textNode);
|
|
1763
2468
|
range.collapse(true);
|
|
1764
2469
|
selection.removeAllRanges();
|
|
1765
2470
|
selection.addRange(range);
|
|
1766
2471
|
const editor = editorRef.current;
|
|
1767
|
-
if (editor)
|
|
1768
|
-
|
|
1769
|
-
notifyChange(content);
|
|
1770
|
-
}
|
|
2472
|
+
if (editor)
|
|
2473
|
+
notifyChange(domToContent(editor));
|
|
1771
2474
|
},
|
|
1772
2475
|
insertInline: (type, attributes) => {
|
|
1773
2476
|
const selection = window.getSelection();
|
|
@@ -1776,42 +2479,30 @@ const Editor = ({ initialContent, onChange, plugins: providedPlugins, placeholde
|
|
|
1776
2479
|
const range = selection.getRangeAt(0);
|
|
1777
2480
|
const inline = document.createElement(type);
|
|
1778
2481
|
if (attributes) {
|
|
1779
|
-
Object.entries(attributes).forEach(([key,
|
|
1780
|
-
inline.setAttribute(key,
|
|
2482
|
+
Object.entries(attributes).forEach(([key, val]) => {
|
|
2483
|
+
inline.setAttribute(key, val);
|
|
1781
2484
|
});
|
|
1782
2485
|
}
|
|
1783
2486
|
try {
|
|
1784
2487
|
range.surroundContents(inline);
|
|
1785
2488
|
}
|
|
1786
|
-
catch (
|
|
1787
|
-
// Falls surroundContents fehlschlägt, versuche es anders
|
|
2489
|
+
catch (_) {
|
|
1788
2490
|
const contents = range.extractContents();
|
|
1789
2491
|
inline.appendChild(contents);
|
|
1790
2492
|
range.insertNode(inline);
|
|
1791
2493
|
}
|
|
1792
|
-
// Cursor setzen
|
|
1793
2494
|
range.setStartAfter(inline);
|
|
1794
2495
|
range.collapse(true);
|
|
1795
2496
|
selection.removeAllRanges();
|
|
1796
2497
|
selection.addRange(range);
|
|
1797
2498
|
const editor = editorRef.current;
|
|
1798
|
-
if (editor)
|
|
1799
|
-
|
|
1800
|
-
notifyChange(content);
|
|
1801
|
-
}
|
|
1802
|
-
},
|
|
1803
|
-
undo: () => {
|
|
1804
|
-
executeCommand("undo");
|
|
1805
|
-
},
|
|
1806
|
-
redo: () => {
|
|
1807
|
-
executeCommand("redo");
|
|
1808
|
-
},
|
|
1809
|
-
canUndo: () => {
|
|
1810
|
-
return historyRef.current.canUndo();
|
|
1811
|
-
},
|
|
1812
|
-
canRedo: () => {
|
|
1813
|
-
return historyRef.current.canRedo();
|
|
2499
|
+
if (editor)
|
|
2500
|
+
notifyChange(domToContent(editor));
|
|
1814
2501
|
},
|
|
2502
|
+
undo: () => undo(),
|
|
2503
|
+
redo: () => redo(),
|
|
2504
|
+
canUndo: () => historyRef.current.canUndo(),
|
|
2505
|
+
canRedo: () => historyRef.current.canRedo(),
|
|
1815
2506
|
importHtml: (htmlString) => {
|
|
1816
2507
|
const content = htmlToContent(htmlString);
|
|
1817
2508
|
const editor = editorRef.current;
|
|
@@ -1828,230 +2519,80 @@ const Editor = ({ initialContent, onChange, plugins: providedPlugins, placeholde
|
|
|
1828
2519
|
const editor = editorRef.current;
|
|
1829
2520
|
if (!editor)
|
|
1830
2521
|
return "";
|
|
1831
|
-
|
|
1832
|
-
return contentToHTML(content);
|
|
2522
|
+
return contentToHTML(domToContent(editor));
|
|
1833
2523
|
},
|
|
1834
2524
|
clearFormatting: () => {
|
|
1835
|
-
|
|
1836
|
-
if (!editor)
|
|
1837
|
-
return;
|
|
1838
|
-
const selection = window.getSelection();
|
|
1839
|
-
if (selection && selection.rangeCount > 0) {
|
|
1840
|
-
const currentContent = domToContent(editor);
|
|
1841
|
-
historyRef.current.push(currentContent);
|
|
1842
|
-
clearFormatting(selection);
|
|
1843
|
-
setTimeout(() => {
|
|
1844
|
-
if (editor) {
|
|
1845
|
-
const content = domToContent(editor);
|
|
1846
|
-
notifyChange(content);
|
|
1847
|
-
}
|
|
1848
|
-
}, 0);
|
|
1849
|
-
}
|
|
2525
|
+
executeWithHistory((selection) => clearFormatting(selection));
|
|
1850
2526
|
},
|
|
1851
2527
|
clearTextColor: () => {
|
|
1852
|
-
|
|
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
|
-
}
|
|
2528
|
+
executeWithHistory((selection) => clearTextColor(selection));
|
|
1867
2529
|
},
|
|
1868
2530
|
clearBackgroundColor: () => {
|
|
1869
|
-
|
|
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
|
-
}
|
|
2531
|
+
executeWithHistory((selection) => clearBackgroundColor(selection));
|
|
1884
2532
|
},
|
|
1885
2533
|
clearFontSize: () => {
|
|
1886
|
-
|
|
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
|
-
}
|
|
2534
|
+
executeWithHistory((selection) => clearFontSize(selection));
|
|
1901
2535
|
},
|
|
1902
2536
|
clearLinks: () => {
|
|
1903
|
-
|
|
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
|
-
}
|
|
2537
|
+
executeWithHistory((selection) => clearLinks(selection));
|
|
1918
2538
|
},
|
|
1919
2539
|
indentListItem: () => {
|
|
1920
|
-
|
|
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
|
-
}
|
|
2540
|
+
executeWithHistory((selection) => indentListItem(selection));
|
|
1935
2541
|
},
|
|
1936
2542
|
outdentListItem: () => {
|
|
1937
|
-
|
|
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
|
-
}
|
|
2543
|
+
executeWithHistory((selection) => outdentListItem(selection));
|
|
1952
2544
|
},
|
|
1953
2545
|
};
|
|
2546
|
+
/** Helper: push history, execute operation, then notify change. */
|
|
2547
|
+
function executeWithHistory(operation) {
|
|
2548
|
+
const editor = editorRef.current;
|
|
2549
|
+
if (!editor)
|
|
2550
|
+
return;
|
|
2551
|
+
const selection = window.getSelection();
|
|
2552
|
+
if (!selection || selection.rangeCount === 0)
|
|
2553
|
+
return;
|
|
2554
|
+
const currentContent = domToContent(editor);
|
|
2555
|
+
historyRef.current.push(currentContent);
|
|
2556
|
+
operation(selection);
|
|
2557
|
+
setTimeout(() => {
|
|
2558
|
+
if (editor)
|
|
2559
|
+
notifyChange(domToContent(editor));
|
|
2560
|
+
}, 0);
|
|
2561
|
+
}
|
|
1954
2562
|
}, [
|
|
2563
|
+
undo,
|
|
2564
|
+
redo,
|
|
2565
|
+
checkbox,
|
|
1955
2566
|
notifyChange,
|
|
1956
|
-
restoreSelection,
|
|
1957
2567
|
customLinkComponent,
|
|
1958
2568
|
customHeadingRenderer,
|
|
1959
2569
|
]);
|
|
2570
|
+
// --- Initialize editor ---
|
|
2571
|
+
useEditorInit({
|
|
2572
|
+
editorRef,
|
|
2573
|
+
historyRef,
|
|
2574
|
+
isUpdatingRef,
|
|
2575
|
+
initialContent,
|
|
2576
|
+
customLinkComponent,
|
|
2577
|
+
customHeadingRenderer,
|
|
2578
|
+
});
|
|
2579
|
+
// --- Set up event listeners ---
|
|
2580
|
+
useEditorEvents({
|
|
2581
|
+
editorRef,
|
|
2582
|
+
historyRef,
|
|
2583
|
+
isUpdatingRef,
|
|
2584
|
+
notifyChange,
|
|
2585
|
+
handleCheckboxKeyDown: checkbox.handleCheckboxKeyDown,
|
|
2586
|
+
handleCheckboxEnter: checkbox.handleCheckboxEnter,
|
|
2587
|
+
undo,
|
|
2588
|
+
redo,
|
|
2589
|
+
});
|
|
2590
|
+
// --- Expose editor API ---
|
|
1960
2591
|
useEffect(() => {
|
|
1961
|
-
if (onEditorAPIReady)
|
|
2592
|
+
if (onEditorAPIReady)
|
|
1962
2593
|
onEditorAPIReady(editorAPI);
|
|
1963
|
-
}
|
|
1964
2594
|
}, [editorAPI, onEditorAPIReady]);
|
|
1965
|
-
|
|
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
|
-
}
|
|
2053
|
-
};
|
|
2054
|
-
}, [editorAPI, notifyChange]);
|
|
2595
|
+
// --- Paste handler ---
|
|
2055
2596
|
const handlePaste = (e) => {
|
|
2056
2597
|
e.preventDefault();
|
|
2057
2598
|
const html = e.clipboardData.getData("text/html");
|
|
@@ -2079,11 +2620,10 @@ const Editor = ({ initialContent, onChange, plugins: providedPlugins, placeholde
|
|
|
2079
2620
|
}
|
|
2080
2621
|
selection.removeAllRanges();
|
|
2081
2622
|
selection.addRange(range);
|
|
2082
|
-
|
|
2083
|
-
notifyChange(content);
|
|
2623
|
+
notifyChange(domToContent(editor));
|
|
2084
2624
|
}
|
|
2085
2625
|
}
|
|
2086
|
-
catch (
|
|
2626
|
+
catch (_) {
|
|
2087
2627
|
document.execCommand("insertText", false, text);
|
|
2088
2628
|
}
|
|
2089
2629
|
}
|
|
@@ -2091,6 +2631,7 @@ const Editor = ({ initialContent, onChange, plugins: providedPlugins, placeholde
|
|
|
2091
2631
|
document.execCommand("insertText", false, text);
|
|
2092
2632
|
}
|
|
2093
2633
|
};
|
|
2634
|
+
// --- Theme styles ---
|
|
2094
2635
|
const containerStyle = theme
|
|
2095
2636
|
? {
|
|
2096
2637
|
...(theme.borderColor &&
|
|
@@ -2121,6 +2662,131 @@ const Editor = ({ initialContent, onChange, plugins: providedPlugins, placeholde
|
|
|
2121
2662
|
: {};
|
|
2122
2663
|
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
2664
|
};
|
|
2665
|
+
// --- Helper: Insert Image ---
|
|
2666
|
+
function handleInsertImage(editor, value, isUpdatingRef, historyRef, notifyChange) {
|
|
2667
|
+
let selection = window.getSelection();
|
|
2668
|
+
if (!selection)
|
|
2669
|
+
return false;
|
|
2670
|
+
if (document.activeElement !== editor) {
|
|
2671
|
+
editor.focus();
|
|
2672
|
+
}
|
|
2673
|
+
if (selection.rangeCount === 0) {
|
|
2674
|
+
const range = document.createRange();
|
|
2675
|
+
if (editor.childNodes.length > 0) {
|
|
2676
|
+
const lastChild = editor.childNodes[editor.childNodes.length - 1];
|
|
2677
|
+
range.setStartAfter(lastChild);
|
|
2678
|
+
range.collapse(true);
|
|
2679
|
+
}
|
|
2680
|
+
else {
|
|
2681
|
+
const img = createImageElement(value);
|
|
2682
|
+
editor.appendChild(img);
|
|
2683
|
+
const newRange = document.createRange();
|
|
2684
|
+
newRange.setStartAfter(img);
|
|
2685
|
+
newRange.collapse(true);
|
|
2686
|
+
selection.removeAllRanges();
|
|
2687
|
+
selection.addRange(newRange);
|
|
2688
|
+
saveAndNotify(editor, isUpdatingRef, historyRef, notifyChange);
|
|
2689
|
+
return true;
|
|
2690
|
+
}
|
|
2691
|
+
selection.removeAllRanges();
|
|
2692
|
+
selection.addRange(range);
|
|
2693
|
+
}
|
|
2694
|
+
if (selection.rangeCount === 0)
|
|
2695
|
+
return false;
|
|
2696
|
+
const range = selection.getRangeAt(0);
|
|
2697
|
+
const container = range.commonAncestorContainer;
|
|
2698
|
+
let parentElement = null;
|
|
2699
|
+
if (container.nodeType === Node.TEXT_NODE) {
|
|
2700
|
+
parentElement = container.parentElement;
|
|
2701
|
+
}
|
|
2702
|
+
else if (container.nodeType === Node.ELEMENT_NODE) {
|
|
2703
|
+
parentElement = container;
|
|
2704
|
+
}
|
|
2705
|
+
const img = createImageElement(value);
|
|
2706
|
+
if (parentElement &&
|
|
2707
|
+
parentElement !== editor &&
|
|
2708
|
+
/^(P|DIV|H[1-6])$/.test(parentElement.tagName)) {
|
|
2709
|
+
if (parentElement.nextSibling) {
|
|
2710
|
+
editor.insertBefore(img, parentElement.nextSibling);
|
|
2711
|
+
}
|
|
2712
|
+
else {
|
|
2713
|
+
editor.appendChild(img);
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
else {
|
|
2717
|
+
try {
|
|
2718
|
+
range.insertNode(img);
|
|
2719
|
+
}
|
|
2720
|
+
catch (_) {
|
|
2721
|
+
editor.appendChild(img);
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
const newRange = document.createRange();
|
|
2725
|
+
newRange.setStartAfter(img);
|
|
2726
|
+
newRange.collapse(true);
|
|
2727
|
+
selection.removeAllRanges();
|
|
2728
|
+
selection.addRange(newRange);
|
|
2729
|
+
saveAndNotify(editor, isUpdatingRef, historyRef, notifyChange);
|
|
2730
|
+
return true;
|
|
2731
|
+
}
|
|
2732
|
+
function createImageElement(src) {
|
|
2733
|
+
const img = document.createElement("img");
|
|
2734
|
+
img.setAttribute("src", src);
|
|
2735
|
+
img.setAttribute("alt", "");
|
|
2736
|
+
img.style.maxWidth = "100%";
|
|
2737
|
+
img.style.height = "auto";
|
|
2738
|
+
img.style.display = "block";
|
|
2739
|
+
img.style.margin = "16px 0";
|
|
2740
|
+
return img;
|
|
2741
|
+
}
|
|
2742
|
+
function saveAndNotify(editor, isUpdatingRef, historyRef, notifyChange) {
|
|
2743
|
+
isUpdatingRef.current = true;
|
|
2744
|
+
setTimeout(() => {
|
|
2745
|
+
const content = domToContent(editor);
|
|
2746
|
+
historyRef.current.push(content);
|
|
2747
|
+
isUpdatingRef.current = false;
|
|
2748
|
+
notifyChange(content);
|
|
2749
|
+
}, 0);
|
|
2750
|
+
}
|
|
2751
|
+
/** Ensures the editor is focused and has a valid selection. */
|
|
2752
|
+
function ensureEditorFocused(editor) {
|
|
2753
|
+
const selection = window.getSelection();
|
|
2754
|
+
let savedRange = null;
|
|
2755
|
+
if (selection && selection.rangeCount > 0) {
|
|
2756
|
+
savedRange = selection.getRangeAt(0).cloneRange();
|
|
2757
|
+
}
|
|
2758
|
+
if (document.activeElement !== editor) {
|
|
2759
|
+
editor.focus();
|
|
2760
|
+
}
|
|
2761
|
+
if (!selection || selection.rangeCount === 0) {
|
|
2762
|
+
const range = document.createRange();
|
|
2763
|
+
if (editor.childNodes.length > 0) {
|
|
2764
|
+
const lastChild = editor.childNodes[editor.childNodes.length - 1];
|
|
2765
|
+
if (lastChild.nodeType === Node.TEXT_NODE) {
|
|
2766
|
+
range.setStart(lastChild, lastChild.textContent?.length || 0);
|
|
2767
|
+
range.setEnd(lastChild, lastChild.textContent?.length || 0);
|
|
2768
|
+
}
|
|
2769
|
+
else {
|
|
2770
|
+
range.selectNodeContents(lastChild);
|
|
2771
|
+
range.collapse(false);
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2774
|
+
else {
|
|
2775
|
+
const p = document.createElement("p");
|
|
2776
|
+
editor.appendChild(p);
|
|
2777
|
+
const textNode = document.createTextNode("");
|
|
2778
|
+
p.appendChild(textNode);
|
|
2779
|
+
range.setStart(textNode, 0);
|
|
2780
|
+
range.setEnd(textNode, 0);
|
|
2781
|
+
}
|
|
2782
|
+
selection?.removeAllRanges();
|
|
2783
|
+
selection?.addRange(range);
|
|
2784
|
+
}
|
|
2785
|
+
else if (savedRange) {
|
|
2786
|
+
selection.removeAllRanges();
|
|
2787
|
+
selection.addRange(savedRange);
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2124
2790
|
|
|
2125
2791
|
const defaultHeadings = ["h1", "h2", "h3"];
|
|
2126
2792
|
const headingLabels = {
|
|
@@ -2349,5 +3015,5 @@ const orderedListPlugin = {
|
|
|
2349
3015
|
},
|
|
2350
3016
|
};
|
|
2351
3017
|
|
|
2352
|
-
export { Dropdown, Editor, HistoryManager, Toolbar, blockquotePlugin, boldPlugin, clearFormattingPlugin, contentToDOM, contentToHTML, createBackgroundColorPlugin, createBlockFormatPlugin, createEmptyContent, createFontSizePlugin, createHeadingsPlugin, createImagePlugin, createLinkPlugin, createTextColorPlugin, Editor as default, defaultPlugins, domToContent, getCurrentBackgroundColor, getCurrentFontSize, getCurrentHeading, getCurrentTextColor, htmlToContent, indentListItem, indentListItemPlugin, italicPlugin, linkPlugin, orderedListPlugin, outdentListItem, outdentListItemPlugin, redoPlugin, underlinePlugin, undoPlugin, unorderedListPlugin };
|
|
3018
|
+
export { Dropdown, Editor, HistoryManager, Toolbar, blockquotePlugin, boldPlugin, clearFormattingPlugin, contentToDOM, contentToHTML, 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, underlinePlugin, undoPlugin, unorderedListPlugin };
|
|
2353
3019
|
//# sourceMappingURL=index.esm.js.map
|