@overlap/rte 1.0.7 → 1.0.9

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.
@@ -1 +1 @@
1
- {"version":3,"file":"Dropdown.d.ts","sourceRoot":"","sources":["../../src/components/Dropdown.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAG3D,UAAU,aAAa;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC3H,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAClC;AAED,eAAO,MAAM,QAAQ,EAAE,KAAK,CAAC,EAAE,CAAC,aAAa,CAsJ5C,CAAC"}
1
+ {"version":3,"file":"Dropdown.d.ts","sourceRoot":"","sources":["../../src/components/Dropdown.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAIxE,UAAU,aAAa;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC3H,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAClC;AAED,eAAO,MAAM,QAAQ,EAAE,KAAK,CAAC,EAAE,CAAC,aAAa,CAwM5C,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"FloatingToolbar.d.ts","sourceRoot":"","sources":["../../src/components/FloatingToolbar.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmD,MAAM,OAAO,CAAC;AACxE,OAAO,EAAe,SAAS,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAE1D,UAAU,oBAAoB;IAC1B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,SAAS,CAAC;IACrB,aAAa,EAAE,WAAW,GAAG,IAAI,CAAC;CACrC;AAED;;;GAGG;AACH,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CA0L1D,CAAC"}
1
+ {"version":3,"file":"FloatingToolbar.d.ts","sourceRoot":"","sources":["../../src/components/FloatingToolbar.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAExE,OAAO,EAAe,SAAS,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAE1D,UAAU,oBAAoB;IAC1B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,SAAS,CAAC;IACrB,aAAa,EAAE,WAAW,GAAG,IAAI,CAAC;CACrC;AAED;;;GAGG;AACH,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CA0L1D,CAAC"}
@@ -1,7 +1,5 @@
1
1
  /** Debounce time for pushing to history after input (ms) */
2
2
  export declare const HISTORY_DEBOUNCE_MS = 300;
3
- /** Width of the clickable checkbox area in pixels */
4
- export declare const CHECKBOX_CLICK_ZONE_PX = 40;
5
3
  /** Maximum nesting depth for lists */
6
4
  export declare const MAX_LIST_DEPTH = 6;
7
5
  /** Maximum number of history entries */
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,4DAA4D;AAC5D,eAAO,MAAM,mBAAmB,MAAM,CAAC;AAEvC,qDAAqD;AACrD,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAEzC,sCAAsC;AACtC,eAAO,MAAM,cAAc,IAAI,CAAC;AAEhC,wCAAwC;AACxC,eAAO,MAAM,gBAAgB,KAAK,CAAC"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,4DAA4D;AAC5D,eAAO,MAAM,mBAAmB,MAAM,CAAC;AAEvC,sCAAsC;AACtC,eAAO,MAAM,cAAc,IAAI,CAAC;AAEhC,wCAAwC;AACxC,eAAO,MAAM,gBAAgB,KAAK,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"useCheckbox.d.ts","sourceRoot":"","sources":["../../src/hooks/useCheckbox.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EACH,mBAAmB,EAInB,qBAAqB,EACxB,MAAM,mBAAmB,CAAC;AAS3B,UAAU,kBAAkB;IACxB,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IAClD,aAAa,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IACpC,aAAa,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAC;IAChD,YAAY,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAC;IAC/C,aAAa,EAAE,MAAM,aAAa,CAAC;CACtC;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,EACxB,SAAS,EACT,aAAa,EACb,aAAa,EACb,YAAY,EACZ,aAAa,GAChB,EAAE,kBAAkB;;iCA4QJ,WAAW,KAAG,OAAO;+BApL1B,aAAa,KAAG,OAAO;6BAkFvB,aAAa,KAAG,OAAO;;EA0MlC"}
1
+ {"version":3,"file":"useCheckbox.d.ts","sourceRoot":"","sources":["../../src/hooks/useCheckbox.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EACH,mBAAmB,EAInB,qBAAqB,EACxB,MAAM,mBAAmB,CAAC;AAS3B,UAAU,kBAAkB;IACxB,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IAClD,aAAa,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IACpC,aAAa,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAC;IAChD,YAAY,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAC;IAC/C,aAAa,EAAE,MAAM,aAAa,CAAC;CACtC;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,EACxB,SAAS,EACT,aAAa,EACb,aAAa,EACb,YAAY,EACZ,aAAa,GAChB,EAAE,kBAAkB;;iCA4QJ,WAAW,KAAG,OAAO;+BApL1B,aAAa,KAAG,OAAO;6BAkFvB,aAAa,KAAG,OAAO;;EA0MlC"}
@@ -1 +1 @@
1
- {"version":3,"file":"useEditorEvents.d.ts","sourceRoot":"","sources":["../../src/hooks/useEditorEvents.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAGzC,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAOlD,UAAU,sBAAsB;IAC5B,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IAClD,UAAU,EAAE;QAAE,OAAO,EAAE,cAAc,CAAA;KAAE,CAAC;IACxC,aAAa,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IACpC,UAAU,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IACjC,YAAY,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAC;IAC/C,qBAAqB,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,OAAO,CAAC;IACrD,mBAAmB,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,OAAO,CAAC;IACnD,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,IAAI,EAAE,MAAM,IAAI,CAAC;CACpB;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,EAC5B,SAAS,EACT,UAAU,EACV,aAAa,EACb,UAAU,EACV,YAAY,EACZ,qBAAqB,EACrB,mBAAmB,EACnB,IAAI,EACJ,IAAI,GACP,EAAE,sBAAsB,QAoOxB"}
1
+ {"version":3,"file":"useEditorEvents.d.ts","sourceRoot":"","sources":["../../src/hooks/useEditorEvents.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAGzC,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAOlD,UAAU,sBAAsB;IAC5B,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IAClD,UAAU,EAAE;QAAE,OAAO,EAAE,cAAc,CAAA;KAAE,CAAC;IACxC,aAAa,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IACpC,UAAU,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IACjC,YAAY,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAC;IAC/C,qBAAqB,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,OAAO,CAAC;IACrD,mBAAmB,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,OAAO,CAAC;IACnD,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,IAAI,EAAE,MAAM,IAAI,CAAC;CACpB;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,EAC5B,SAAS,EACT,UAAU,EACV,aAAa,EACb,UAAU,EACV,YAAY,EACZ,qBAAqB,EACrB,mBAAmB,EACnB,IAAI,EACJ,IAAI,GACP,EAAE,sBAAsB,QA0TxB"}
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, useCallback, useMemo } from 'react';
2
+ import React, { useState, useRef, useCallback, useEffect, useMemo } from 'react';
3
3
  import { createPortal } from 'react-dom';
4
4
 
5
5
  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" }) }));
@@ -83,10 +83,45 @@ const Icon = ({ icon, width = 18, height = 18, className }) => {
83
83
  const Dropdown = ({ icon, label, options, onSelect, currentValue, disabled, showCustomColorInput, }) => {
84
84
  const [isOpen, setIsOpen] = useState(false);
85
85
  const [customColor, setCustomColor] = useState("#000000");
86
+ const [menuPos, setMenuPos] = useState({ top: 0, left: 0 });
86
87
  const dropdownRef = useRef(null);
88
+ const menuRef = useRef(null);
89
+ const buttonRef = useRef(null);
90
+ const updateMenuPosition = useCallback(() => {
91
+ if (!buttonRef.current)
92
+ return;
93
+ const rect = buttonRef.current.getBoundingClientRect();
94
+ const pad = 8;
95
+ let top = rect.bottom + 4;
96
+ let left = rect.left;
97
+ const menuEl = menuRef.current;
98
+ if (menuEl) {
99
+ const menuW = menuEl.offsetWidth;
100
+ const menuH = menuEl.offsetHeight;
101
+ if (left + menuW > window.innerWidth - pad) {
102
+ left = window.innerWidth - menuW - pad;
103
+ }
104
+ if (left < pad)
105
+ left = pad;
106
+ if (top + menuH > window.innerHeight - pad) {
107
+ top = rect.top - menuH - 4;
108
+ }
109
+ if (top < pad)
110
+ top = pad;
111
+ }
112
+ setMenuPos({ top, left });
113
+ }, []);
114
+ useEffect(() => {
115
+ if (!isOpen)
116
+ return;
117
+ updateMenuPosition();
118
+ requestAnimationFrame(updateMenuPosition);
119
+ }, [isOpen, updateMenuPosition]);
87
120
  useEffect(() => {
88
121
  const handleClickOutside = (event) => {
89
- if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
122
+ const target = event.target;
123
+ if (dropdownRef.current && !dropdownRef.current.contains(target) &&
124
+ (!menuRef.current || !menuRef.current.contains(target))) {
90
125
  setIsOpen(false);
91
126
  }
92
127
  };
@@ -102,7 +137,6 @@ const Dropdown = ({ icon, label, options, onSelect, currentValue, disabled, show
102
137
  setIsOpen(false);
103
138
  };
104
139
  const currentOption = options.find(opt => opt.value === currentValue);
105
- // Close on Escape key
106
140
  useEffect(() => {
107
141
  if (!isOpen)
108
142
  return;
@@ -115,28 +149,28 @@ const Dropdown = ({ icon, label, options, onSelect, currentValue, disabled, show
115
149
  document.addEventListener("keydown", handleKeyDown);
116
150
  return () => document.removeEventListener("keydown", handleKeyDown);
117
151
  }, [isOpen]);
118
- return (jsxs("div", { className: "rte-dropdown", ref: dropdownRef, onMouseDown: (e) => e.preventDefault(), children: [jsxs("button", { type: "button", onClick: () => !disabled && setIsOpen(!isOpen), disabled: disabled, className: `rte-toolbar-button rte-dropdown-button ${currentOption ? 'rte-dropdown-button-has-value' : ''}`, title: label, "aria-label": label, "aria-expanded": isOpen, "aria-haspopup": "listbox", children: [jsx(Icon, { icon: icon, width: 18, height: 18 }), currentOption && (jsx("span", { className: "rte-dropdown-value", children: currentOption.label }))] }), isOpen && (jsxs("div", { className: "rte-dropdown-menu", role: "listbox", "aria-label": label, children: [options.map((option) => (jsxs("button", { type: "button", role: "option", "aria-selected": currentValue === option.value, className: `rte-dropdown-item ${currentValue === option.value ? 'rte-dropdown-item-active' : ''}`, onClick: () => handleSelect(option.value), children: [option.color && (jsx("span", { className: `rte-dropdown-color-preview ${currentValue === option.value ? 'active' : ''}`, style: { backgroundColor: option.color } })), option.preview && !option.headingPreview && (jsx("span", { className: "rte-dropdown-fontsize-preview", style: { fontSize: `${option.preview}px` }, children: "Aa" })), option.headingPreview && (jsx("span", { className: `rte-dropdown-heading-preview ${option.headingPreview}`, children: option.headingPreview === 'p' ? 'Normal' : option.headingPreview.toUpperCase() })), option.icon && jsx(Icon, { icon: option.icon, width: 16, height: 16 }), jsx("span", { style: { flex: 1, fontWeight: currentValue === option.value ? 600 : 400 }, children: option.label })] }, option.value))), showCustomColorInput && (jsxs("div", { className: "rte-color-custom-input", onMouseDown: (e) => e.stopPropagation(), children: [jsx("input", { type: "color", value: customColor, onChange: (e) => setCustomColor(e.target.value), title: "Pick a color" }), jsx("input", { type: "text", value: customColor, onChange: (e) => {
119
- const v = e.target.value;
120
- setCustomColor(v);
121
- }, placeholder: "#000000", maxLength: 7, onKeyDown: (e) => {
122
- if (e.key === "Enter") {
123
- e.preventDefault();
124
- if (/^#[0-9a-fA-F]{3,6}$/.test(customColor)) {
125
- handleSelect(customColor);
126
- }
127
- }
128
- } }), jsx("button", { type: "button", className: "rte-color-custom-apply", onClick: () => {
129
- if (/^#[0-9a-fA-F]{3,6}$/.test(customColor)) {
130
- handleSelect(customColor);
131
- }
132
- }, children: "Apply" })] }))] }))] }));
152
+ const menuContent = isOpen ? (jsxs("div", { ref: menuRef, className: "rte-dropdown-menu", role: "listbox", "aria-label": label, style: {
153
+ position: 'fixed',
154
+ top: menuPos.top,
155
+ left: menuPos.left,
156
+ }, onMouseDown: (e) => e.preventDefault(), children: [options.map((option) => (jsxs("button", { type: "button", role: "option", "aria-selected": currentValue === option.value, className: `rte-dropdown-item ${currentValue === option.value ? 'rte-dropdown-item-active' : ''}`, onClick: () => handleSelect(option.value), children: [option.color && (jsx("span", { className: `rte-dropdown-color-preview ${currentValue === option.value ? 'active' : ''}`, style: { backgroundColor: option.color } })), option.preview && !option.headingPreview && (jsx("span", { className: "rte-dropdown-fontsize-preview", style: { fontSize: `${option.preview}px` }, children: "Aa" })), option.headingPreview && (jsx("span", { className: `rte-dropdown-heading-preview ${option.headingPreview}`, children: option.headingPreview === 'p' ? 'Normal' : option.headingPreview.toUpperCase() })), option.icon && jsx(Icon, { icon: option.icon, width: 16, height: 16 }), jsx("span", { style: { flex: 1, fontWeight: currentValue === option.value ? 600 : 400 }, children: option.label })] }, option.value))), showCustomColorInput && (jsxs("div", { className: "rte-color-custom-input", onMouseDown: (e) => e.stopPropagation(), children: [jsx("input", { type: "color", value: customColor, onChange: (e) => setCustomColor(e.target.value), title: "Pick a color" }), jsx("input", { type: "text", value: customColor, onChange: (e) => {
157
+ const v = e.target.value;
158
+ setCustomColor(v);
159
+ }, placeholder: "#000000", maxLength: 7, onKeyDown: (e) => {
160
+ if (e.key === "Enter") {
161
+ e.preventDefault();
162
+ if (/^#[0-9a-fA-F]{3,6}$/.test(customColor)) {
163
+ handleSelect(customColor);
164
+ }
165
+ }
166
+ } }), jsx("button", { type: "button", className: "rte-color-custom-apply", onClick: () => {
167
+ if (/^#[0-9a-fA-F]{3,6}$/.test(customColor)) {
168
+ handleSelect(customColor);
169
+ }
170
+ }, children: "Apply" })] }))] })) : null;
171
+ return (jsxs("div", { className: "rte-dropdown", ref: dropdownRef, onMouseDown: (e) => e.preventDefault(), children: [jsxs("button", { ref: buttonRef, type: "button", onClick: () => !disabled && setIsOpen(!isOpen), disabled: disabled, className: `rte-toolbar-button rte-dropdown-button ${currentOption ? 'rte-dropdown-button-has-value' : ''}`, title: label, "aria-label": label, "aria-expanded": isOpen, "aria-haspopup": "listbox", children: [jsx(Icon, { icon: icon, width: 18, height: 18 }), currentOption && (jsx("span", { className: "rte-dropdown-value", children: currentOption.label }))] }), menuContent && createPortal(menuContent, document.body)] }));
133
172
  };
134
173
 
135
- /** Debounce time for pushing to history after input (ms) */
136
- const HISTORY_DEBOUNCE_MS = 300;
137
- /** Width of the clickable checkbox area in pixels */
138
- const CHECKBOX_CLICK_ZONE_PX = 40;
139
-
140
174
  /**
141
175
  * Pure DOM utility functions.
142
176
  * No React dependencies - only native browser APIs.
@@ -389,8 +423,8 @@ function useCheckbox({ editorRef, isUpdatingRef, pushToHistory, notifyChange, ge
389
423
  const rect = listItem.getBoundingClientRect();
390
424
  const clientX = event.clientX;
391
425
  const isInCheckboxArea = listItem.dir === "rtl"
392
- ? clientX >= rect.right - CHECKBOX_CLICK_ZONE_PX
393
- : clientX <= rect.left + CHECKBOX_CLICK_ZONE_PX;
426
+ ? clientX >= rect.right
427
+ : clientX <= rect.left;
394
428
  if (isInCheckboxArea) {
395
429
  event.preventDefault();
396
430
  event.stopPropagation();
@@ -417,8 +451,8 @@ function useCheckbox({ editorRef, isUpdatingRef, pushToHistory, notifyChange, ge
417
451
  const rect = listItem.getBoundingClientRect();
418
452
  const clientX = event.clientX;
419
453
  const isInCheckboxArea = listItem.dir === "rtl"
420
- ? clientX >= rect.right - CHECKBOX_CLICK_ZONE_PX
421
- : clientX <= rect.left + CHECKBOX_CLICK_ZONE_PX;
454
+ ? clientX >= rect.right
455
+ : clientX <= rect.left;
422
456
  if (isInCheckboxArea) {
423
457
  event.preventDefault();
424
458
  }
@@ -676,6 +710,9 @@ function useCheckbox({ editorRef, isUpdatingRef, pushToHistory, notifyChange, ge
676
710
  };
677
711
  }
678
712
 
713
+ /** Debounce time for pushing to history after input (ms) */
714
+ const HISTORY_DEBOUNCE_MS = 300;
715
+
679
716
  /**
680
717
  * Lightweight HTML sanitizer (no external dependencies).
681
718
  *
@@ -806,7 +843,7 @@ const ALLOWED_CONTENT_TAGS = new Set([
806
843
  "ul", "ol", "li", "a", "strong", "em", "u", "s", "del",
807
844
  "sub", "sup", "code", "pre", "blockquote", "br", "hr",
808
845
  "img", "table", "thead", "tbody", "tr", "th", "td",
809
- "b", "i", "strike",
846
+ "b", "i", "strike", "font",
810
847
  ]);
811
848
  /** Checks if an attribute key is safe to set on a DOM element. */
812
849
  function isSafeAttribute(key) {
@@ -1014,6 +1051,7 @@ function domToContent(element) {
1014
1051
  "sub",
1015
1052
  "sup",
1016
1053
  "code",
1054
+ "font",
1017
1055
  ].includes(tagName)) {
1018
1056
  const children = [];
1019
1057
  Array.from(el.childNodes).forEach((child) => {
@@ -1103,6 +1141,46 @@ function domToContent(element) {
1103
1141
  });
1104
1142
  }
1105
1143
  }
1144
+ // <font> from execCommand('foreColor') or pasted HTML
1145
+ if (tagName === "font") {
1146
+ const fontAttrs = {};
1147
+ const colorAttr = el.getAttribute("color");
1148
+ if (colorAttr)
1149
+ fontAttrs.color = colorAttr;
1150
+ const sizeAttr = el.getAttribute("size");
1151
+ if (sizeAttr) {
1152
+ const sizeMap = {
1153
+ "1": "10px", "2": "13px", "3": "16px", "4": "18px",
1154
+ "5": "24px", "6": "32px", "7": "48px",
1155
+ };
1156
+ const mapped = sizeMap[sizeAttr];
1157
+ if (mapped)
1158
+ fontAttrs.fontSize = mapped;
1159
+ }
1160
+ const style = el.getAttribute("style") || "";
1161
+ if (style) {
1162
+ style.split(";").forEach((rule) => {
1163
+ const [key, value] = rule
1164
+ .split(":")
1165
+ .map((s) => s.trim());
1166
+ if (key && value) {
1167
+ if (key === "font-size")
1168
+ fontAttrs.fontSize = value;
1169
+ else if (key === "color")
1170
+ fontAttrs.color = value;
1171
+ else if (key === "background-color")
1172
+ fontAttrs.backgroundColor = value;
1173
+ }
1174
+ });
1175
+ }
1176
+ return {
1177
+ type: "span",
1178
+ children: children.length > 0 ? children : undefined,
1179
+ attributes: Object.keys(fontAttrs).length > 0
1180
+ ? fontAttrs
1181
+ : undefined,
1182
+ };
1183
+ }
1106
1184
  // Map tag names to semantic types
1107
1185
  const type = tagName === "strong" || tagName === "b"
1108
1186
  ? "bold"
@@ -2121,6 +2199,61 @@ function useEditorEvents({ editorRef, historyRef, isUpdatingRef, mountedRef, not
2121
2199
  }, 0);
2122
2200
  return;
2123
2201
  }
2202
+ // Exit code block: Enter on empty last line escapes <pre>
2203
+ if (e.key === "Enter" &&
2204
+ !e.shiftKey &&
2205
+ !isModifierPressed) {
2206
+ const sel = window.getSelection();
2207
+ if (sel && sel.rangeCount > 0 && sel.isCollapsed) {
2208
+ const range = sel.getRangeAt(0);
2209
+ const node = range.startContainer;
2210
+ const pre = (node instanceof HTMLElement
2211
+ ? node
2212
+ : node.parentElement)?.closest("pre");
2213
+ if (pre && pre.lastChild) {
2214
+ const lastChild = pre.lastChild;
2215
+ const cursorInPre = node === pre &&
2216
+ range.startOffset === pre.childNodes.length;
2217
+ const cursorAtEndOfLastText = node.nodeType === Node.TEXT_NODE &&
2218
+ node === lastChild &&
2219
+ range.startOffset ===
2220
+ (node.textContent?.length ?? 0);
2221
+ const isAtEnd = cursorInPre || cursorAtEndOfLastText;
2222
+ const lastIsBr = lastChild instanceof HTMLElement &&
2223
+ lastChild.tagName === "BR";
2224
+ const endsWithNewline = node.nodeType === Node.TEXT_NODE &&
2225
+ (node.textContent || "").endsWith("\n");
2226
+ if (isAtEnd && (lastIsBr || endsWithNewline)) {
2227
+ e.preventDefault();
2228
+ if (lastIsBr) {
2229
+ pre.removeChild(lastChild);
2230
+ }
2231
+ else if (node.nodeType === Node.TEXT_NODE &&
2232
+ node.textContent) {
2233
+ node.textContent =
2234
+ node.textContent.replace(/\n$/, "");
2235
+ }
2236
+ if (!pre.textContent &&
2237
+ !pre.querySelector("br")) {
2238
+ pre.appendChild(document.createElement("br"));
2239
+ }
2240
+ const p = document.createElement("p");
2241
+ p.appendChild(document.createElement("br"));
2242
+ pre.parentNode?.insertBefore(p, pre.nextSibling);
2243
+ const newRange = document.createRange();
2244
+ newRange.setStart(p, 0);
2245
+ newRange.collapse(true);
2246
+ sel.removeAllRanges();
2247
+ sel.addRange(newRange);
2248
+ const content = domToContent(editor);
2249
+ const serializedSel = serializeSelection(editor);
2250
+ historyRef.current.push(content, serializedSel);
2251
+ notifyChange(content);
2252
+ return;
2253
+ }
2254
+ }
2255
+ }
2256
+ }
2124
2257
  // Auto-link: convert URLs to <a> tags on space/enter
2125
2258
  if (!isModifierPressed && (e.key === " " || e.key === "Enter")) {
2126
2259
  handleAutoLink(editor, e);
@@ -4015,7 +4148,7 @@ function createAdvancedLinkPlugin(options = {}) {
4015
4148
  /** Pre-built link plugin with just target enabled (no custom fields). */
4016
4149
  const advancedLinkPlugin = createAdvancedLinkPlugin();
4017
4150
 
4018
- const InsertTableDialog = ({ onInsert, onClose, }) => {
4151
+ const InsertTableDialog = ({ onInsert, onClose, anchorRect, }) => {
4019
4152
  const [rows, setRows] = useState(3);
4020
4153
  const [cols, setCols] = useState(3);
4021
4154
  const dialogRef = useRef(null);
@@ -4029,7 +4162,25 @@ const InsertTableDialog = ({ onInsert, onClose, }) => {
4029
4162
  document.addEventListener("mousedown", handler);
4030
4163
  return () => document.removeEventListener("mousedown", handler);
4031
4164
  }, [onClose]);
4032
- return (jsxs("div", { className: "rte-table-insert-dialog", ref: dialogRef, children: [jsx("div", { className: "rte-table-insert-title", children: "Insert Table" }), jsxs("div", { className: "rte-table-insert-fields", children: [jsxs("label", { className: "rte-table-insert-label", children: [jsx("span", { children: "Zeilen" }), jsx("input", { type: "number", min: 1, max: 20, value: rows, onChange: (e) => setRows(Math.max(1, Math.min(20, parseInt(e.target.value) || 1))), className: "rte-table-insert-input" })] }), jsxs("label", { className: "rte-table-insert-label", children: [jsx("span", { children: "Spalten" }), jsx("input", { type: "number", min: 1, max: 10, value: cols, onChange: (e) => setCols(Math.max(1, Math.min(10, parseInt(e.target.value) || 1))), className: "rte-table-insert-input" })] })] }), jsx("button", { type: "button", className: "rte-table-insert-btn", onClick: () => onInsert(rows, cols), children: "Insert" })] }));
4165
+ const style = { position: "fixed" };
4166
+ if (anchorRect) {
4167
+ const pad = 8;
4168
+ let top = anchorRect.bottom + 4;
4169
+ let left = anchorRect.left;
4170
+ if (left + 220 > window.innerWidth - pad) {
4171
+ left = window.innerWidth - 220 - pad;
4172
+ }
4173
+ if (left < pad)
4174
+ left = pad;
4175
+ if (top + 200 > window.innerHeight - pad) {
4176
+ top = anchorRect.top - 200 - 4;
4177
+ }
4178
+ if (top < pad)
4179
+ top = pad;
4180
+ style.top = top;
4181
+ style.left = left;
4182
+ }
4183
+ return createPortal(jsxs("div", { className: "rte-table-insert-dialog", ref: dialogRef, style: style, onMouseDown: (e) => e.preventDefault(), children: [jsx("div", { className: "rte-table-insert-title", children: "Insert Table" }), jsxs("div", { className: "rte-table-insert-fields", children: [jsxs("label", { className: "rte-table-insert-label", children: [jsx("span", { children: "Zeilen" }), jsx("input", { type: "number", min: 1, max: 20, value: rows, onChange: (e) => setRows(Math.max(1, Math.min(20, parseInt(e.target.value) || 1))), className: "rte-table-insert-input", onMouseDown: (e) => e.stopPropagation() })] }), jsxs("label", { className: "rte-table-insert-label", children: [jsx("span", { children: "Spalten" }), jsx("input", { type: "number", min: 1, max: 10, value: cols, onChange: (e) => setCols(Math.max(1, Math.min(10, parseInt(e.target.value) || 1))), className: "rte-table-insert-input", onMouseDown: (e) => e.stopPropagation() })] })] }), jsx("button", { type: "button", className: "rte-table-insert-btn", onClick: () => onInsert(rows, cols), children: "Insert" })] }), document.body);
4033
4184
  };
4034
4185
  const TableContextMenu = ({ x, y, onClose, }) => {
4035
4186
  const ref = useRef(null);
@@ -4050,6 +4201,14 @@ const TableContextMenu = ({ x, y, onClose, }) => {
4050
4201
  };
4051
4202
  const TableToolbarButton = (props) => {
4052
4203
  const [showDialog, setShowDialog] = useState(false);
4204
+ const btnRef = useRef(null);
4205
+ const [anchorRect, setAnchorRect] = useState(null);
4206
+ const handleToggle = useCallback(() => {
4207
+ if (!showDialog && btnRef.current) {
4208
+ setAnchorRect(btnRef.current.getBoundingClientRect());
4209
+ }
4210
+ setShowDialog((v) => !v);
4211
+ }, [showDialog]);
4053
4212
  const handleInsert = useCallback((rows, cols) => {
4054
4213
  setShowDialog(false);
4055
4214
  if (!props.editorAPI)
@@ -4058,7 +4217,6 @@ const TableToolbarButton = (props) => {
4058
4217
  if (!sel || sel.rangeCount === 0)
4059
4218
  return;
4060
4219
  const range = sel.getRangeAt(0);
4061
- // Find the editor's contentEditable root
4062
4220
  const container = range.commonAncestorContainer;
4063
4221
  const editorEl = container.nodeType === Node.TEXT_NODE
4064
4222
  ? container.parentElement
@@ -4067,7 +4225,6 @@ const TableToolbarButton = (props) => {
4067
4225
  if (!editorRoot)
4068
4226
  return;
4069
4227
  const table = createTable(rows, cols);
4070
- // Insert after the current block element
4071
4228
  let block = editorEl;
4072
4229
  while (block &&
4073
4230
  block !== editorRoot &&
@@ -4080,11 +4237,9 @@ const TableToolbarButton = (props) => {
4080
4237
  else {
4081
4238
  editorRoot.appendChild(table);
4082
4239
  }
4083
- // Add a paragraph after the table so the user can continue typing
4084
4240
  const p = document.createElement("p");
4085
4241
  p.innerHTML = "<br>";
4086
4242
  table.parentNode?.insertBefore(p, table.nextSibling);
4087
- // Focus the first cell
4088
4243
  const firstCell = table.querySelector("td, th");
4089
4244
  if (firstCell) {
4090
4245
  const newRange = document.createRange();
@@ -4094,7 +4249,7 @@ const TableToolbarButton = (props) => {
4094
4249
  sel.addRange(newRange);
4095
4250
  }
4096
4251
  }, [props.editorAPI]);
4097
- return (jsxs("div", { style: { position: "relative" }, children: [jsx("button", { type: "button", onClick: () => setShowDialog(!showDialog), disabled: props.disabled, className: `rte-toolbar-button ${props.isActive ? "rte-toolbar-button-active" : ""}`, title: "Table", "aria-label": "Table", children: jsx(IconWrapper, { icon: "mdi:table", width: 18, height: 18 }) }), showDialog && (jsx(InsertTableDialog, { onInsert: handleInsert, onClose: () => setShowDialog(false) }))] }));
4252
+ return (jsxs(Fragment, { children: [jsx("button", { ref: btnRef, type: "button", onClick: handleToggle, disabled: props.disabled, className: `rte-toolbar-button ${props.isActive ? "rte-toolbar-button-active" : ""}`, title: "Table", "aria-label": "Table", children: jsx(IconWrapper, { icon: "mdi:table", width: 18, height: 18 }) }), showDialog && (jsx(InsertTableDialog, { onInsert: handleInsert, onClose: () => setShowDialog(false), anchorRect: anchorRect }))] }));
4098
4253
  };
4099
4254
  /* ══════════════════════════════════════════════════════════════════════════
4100
4255
  Table Plugin export
@@ -4304,10 +4459,10 @@ const FloatingToolbar = ({ plugins, editorAPI, editorElement, }) => {
4304
4459
  }
4305
4460
  // Center horizontally on the selection, clamp to viewport
4306
4461
  let left = rect.left + rect.width / 2 - toolbarW / 2;
4307
- if (left < pad)
4308
- left = pad;
4309
4462
  if (left + toolbarW > vw - pad)
4310
4463
  left = vw - toolbarW - pad;
4464
+ if (left < pad)
4465
+ left = pad;
4311
4466
  setPos({ top, left, visible: true });
4312
4467
  }, [editorElement]);
4313
4468
  // Debounce via requestAnimationFrame for smooth repositioning
@@ -4352,16 +4507,14 @@ const FloatingToolbar = ({ plugins, editorAPI, editorElement, }) => {
4352
4507
  requestAnimationFrame(() => scheduleUpdate());
4353
4508
  }, [editorAPI, scheduleUpdate]);
4354
4509
  const isHidden = !pos.visible || leftPlugins.length === 0;
4355
- return (jsx("div", { ref: toolbarRef, className: "rte-floating-toolbar", style: {
4510
+ return createPortal(jsx("div", { ref: toolbarRef, className: "rte-floating-toolbar", style: {
4356
4511
  position: "fixed",
4357
4512
  top: pos.top,
4358
4513
  left: pos.left,
4359
4514
  visibility: isHidden ? "hidden" : "visible",
4360
4515
  opacity: isHidden ? 0 : 1,
4361
4516
  pointerEvents: isHidden ? "none" : "auto",
4362
- },
4363
- // Prevent selection loss when clicking toolbar buttons
4364
- onMouseDown: (e) => e.preventDefault(), children: jsxs("div", { className: "rte-floating-toolbar-content", children: [jsx("div", { className: "rte-toolbar-left", children: leftPlugins.map((plugin) => {
4517
+ }, onMouseDown: (e) => e.preventDefault(), children: jsxs("div", { className: "rte-floating-toolbar-content", children: [jsx("div", { className: "rte-toolbar-left", children: leftPlugins.map((plugin) => {
4365
4518
  if (!plugin.renderButton)
4366
4519
  return null;
4367
4520
  const isActive = plugin.isActive
@@ -4395,7 +4548,7 @@ const FloatingToolbar = ({ plugins, editorAPI, editorElement, }) => {
4395
4548
  disabled: !canExecute,
4396
4549
  editorAPI,
4397
4550
  });
4398
- })()] }))] }) }));
4551
+ })()] }))] }) }), document.body);
4399
4552
  };
4400
4553
 
4401
4554
  /**