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