@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.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: "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" }), jsxRuntime.jsx("rect", { x: "4", y: "13", width: "16", height: "6", fill: "currentColor", opacity: "0.5" })] }));
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
- const defaultHeadings$2 = ["h1", "h2", "h3"];
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
- * Erstellt ein Block-Format-Plugin, das Headlines, Listen und Quote in einem Dropdown kombiniert
153
- * @param headings - Array von Heading-Levels (z.B. ["h1", "h2", "h3"])
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
- { value: "blockquote", label: "Zitat", icon: "mdi:format-quote-close" },
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
- const tagName = element.tagName.toLowerCase();
192
- // Prüfe auf Heading
193
- if (headings.includes(tagName)) {
194
- currentValue = tagName;
195
- }
196
- // Prüfe auf Blockquote
197
- else if (element.closest("blockquote")) {
198
- currentValue = "blockquote";
199
- }
200
- // Prüfe auf Liste
201
- else if (element.closest("ul")) {
202
- currentValue = "ul";
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
- }, currentValue: currentValue, disabled: props.disabled }));
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
- return undefined;
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 === Node.ELEMENT_NODE) {
989
- const el = node;
990
- const tagName = el.tagName.toLowerCase();
991
- if (tagName === "img") {
992
- const attributes = {};
993
- const src = el.getAttribute("src");
994
- const alt = el.getAttribute("alt");
995
- if (src)
996
- attributes.src = src;
997
- if (alt)
998
- attributes.alt = alt;
999
- return {
1000
- type: "image",
1001
- attributes: Object.keys(attributes).length > 0
1002
- ? attributes
1003
- : undefined,
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
- if ([
1007
- "p",
1008
- "div",
1009
- "h1",
1010
- "h2",
1011
- "h3",
1012
- "h4",
1013
- "h5",
1014
- "h6",
1015
- "blockquote",
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
- if (["strong", "b", "em", "i", "u", "a", "span"].includes(tagName)) {
1041
- const children = [];
1042
- Array.from(el.childNodes).forEach((child) => {
1043
- const processed = processNode(child);
1044
- if (processed)
1045
- children.push(processed);
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
- if (tagName === "span") {
1064
- const style = el.getAttribute("style");
1065
- if (style) {
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
- return {
1085
- type: tagName === "strong" || tagName === "b"
1086
- ? "bold"
1087
- : tagName === "em" || tagName === "i"
1088
- ? "italic"
1089
- : tagName === "u"
1090
- ? "underline"
1091
- : tagName,
1092
- children: children.length > 0 ? children : undefined,
1093
- attributes: Object.keys(attributes).length > 0
1094
- ? attributes
1095
- : undefined,
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
- if (children.length > 0) {
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: tagName,
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
- key === "color" ||
1170
- key === "backgroundColor") {
1171
- const currentStyle = element.getAttribute("style") || "";
1172
- if (key === "fontSize") {
1173
- element.setAttribute("style", `${currentStyle}font-size: ${value};`.trim());
1174
- }
1175
- else if (key === "color") {
1176
- element.setAttribute("style", `${currentStyle}color: ${value};`.trim());
1177
- }
1178
- else if (key === "backgroundColor") {
1179
- element.setAttribute("style", `${currentStyle}background-color: ${value};`.trim());
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
- const childNode = createNode(child);
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
- const blockNode = createNode(block);
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
- function htmlToContent(htmlString) {
1206
- const tempDiv = document.createElement("div");
1207
- tempDiv.innerHTML = htmlString.trim();
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
- const Toolbar = ({ plugins, editorAPI, className, }) => {
1425
- const [updateTrigger, setUpdateTrigger] = React.useState(0);
1426
- const [isClient, setIsClient] = React.useState(false);
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
- setIsClient(true);
1429
- const handleSelectionChange = () => {
1430
- setUpdateTrigger((prev) => prev + 1);
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 handleMouseUp = () => {
1433
- setTimeout(handleSelectionChange, 10);
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
- const handleKeyUp = () => {
1436
- setTimeout(handleSelectionChange, 10);
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
- if (typeof document !== 'undefined') {
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.push(createFontSizePlugin(fontSizes));
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
- const editorRef = React.useRef(null);
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 && !isUpdatingRef.current) {
2339
+ if (onChange)
1534
2340
  onChange(content);
1535
- }
1536
2341
  }, [onChange]);
1537
- const restoreSelection = React.useCallback((editor) => {
1538
- if (typeof window === "undefined" || typeof document === "undefined")
1539
- return;
1540
- const range = document.createRange();
1541
- const selection = window.getSelection();
1542
- if (editor.firstChild) {
1543
- range.setStart(editor.firstChild, 0);
1544
- range.collapse(true);
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
- const content = historyRef.current.undo();
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
- const content = historyRef.current.redo();
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 === "insertImage" && value) {
1583
- let selection = window.getSelection();
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
- else if (savedRange) {
1716
- selection.removeAllRanges();
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, value]) => {
1759
- block.setAttribute(key, value);
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"); // Zero-width space
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
- const content = domToContent(editor);
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, value]) => {
1784
- inline.setAttribute(key, value);
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 (e) {
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
- const content = domToContent(editor);
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
- const content = domToContent(editor);
1836
- return contentToHTML(content);
2526
+ return contentToHTML(domToContent(editor));
1837
2527
  },
1838
2528
  clearFormatting: () => {
1839
- const editor = editorRef.current;
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
- const editor = editorRef.current;
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
- const editor = editorRef.current;
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
- const editor = editorRef.current;
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
- const editor = editorRef.current;
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
- const editor = editorRef.current;
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
- const editor = editorRef.current;
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
- const isInitializedRef = React.useRef(false);
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
- const content = domToContent(editor);
2087
- notifyChange(content);
2627
+ notifyChange(domToContent(editor));
2088
2628
  }
2089
2629
  }
2090
- catch (error) {
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;