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