@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.
package/dist/index.js CHANGED
@@ -87,10 +87,45 @@ const Icon = ({ icon, width = 18, height = 18, className }) => {
87
87
  const Dropdown = ({ icon, label, options, onSelect, currentValue, disabled, showCustomColorInput, }) => {
88
88
  const [isOpen, setIsOpen] = React.useState(false);
89
89
  const [customColor, setCustomColor] = React.useState("#000000");
90
+ const [menuPos, setMenuPos] = React.useState({ top: 0, left: 0 });
90
91
  const dropdownRef = React.useRef(null);
92
+ const menuRef = React.useRef(null);
93
+ const buttonRef = React.useRef(null);
94
+ const updateMenuPosition = React.useCallback(() => {
95
+ if (!buttonRef.current)
96
+ return;
97
+ const rect = buttonRef.current.getBoundingClientRect();
98
+ const pad = 8;
99
+ let top = rect.bottom + 4;
100
+ let left = rect.left;
101
+ const menuEl = menuRef.current;
102
+ if (menuEl) {
103
+ const menuW = menuEl.offsetWidth;
104
+ const menuH = menuEl.offsetHeight;
105
+ if (left + menuW > window.innerWidth - pad) {
106
+ left = window.innerWidth - menuW - pad;
107
+ }
108
+ if (left < pad)
109
+ left = pad;
110
+ if (top + menuH > window.innerHeight - pad) {
111
+ top = rect.top - menuH - 4;
112
+ }
113
+ if (top < pad)
114
+ top = pad;
115
+ }
116
+ setMenuPos({ top, left });
117
+ }, []);
118
+ React.useEffect(() => {
119
+ if (!isOpen)
120
+ return;
121
+ updateMenuPosition();
122
+ requestAnimationFrame(updateMenuPosition);
123
+ }, [isOpen, updateMenuPosition]);
91
124
  React.useEffect(() => {
92
125
  const handleClickOutside = (event) => {
93
- if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
126
+ const target = event.target;
127
+ if (dropdownRef.current && !dropdownRef.current.contains(target) &&
128
+ (!menuRef.current || !menuRef.current.contains(target))) {
94
129
  setIsOpen(false);
95
130
  }
96
131
  };
@@ -106,7 +141,6 @@ const Dropdown = ({ icon, label, options, onSelect, currentValue, disabled, show
106
141
  setIsOpen(false);
107
142
  };
108
143
  const currentOption = options.find(opt => opt.value === currentValue);
109
- // Close on Escape key
110
144
  React.useEffect(() => {
111
145
  if (!isOpen)
112
146
  return;
@@ -119,28 +153,28 @@ const Dropdown = ({ icon, label, options, onSelect, currentValue, disabled, show
119
153
  document.addEventListener("keydown", handleKeyDown);
120
154
  return () => document.removeEventListener("keydown", handleKeyDown);
121
155
  }, [isOpen]);
122
- return (jsxRuntime.jsxs("div", { className: "rte-dropdown", ref: dropdownRef, onMouseDown: (e) => e.preventDefault(), children: [jsxRuntime.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: [jsxRuntime.jsx(Icon, { icon: icon, width: 18, height: 18 }), currentOption && (jsxRuntime.jsx("span", { className: "rte-dropdown-value", children: currentOption.label }))] }), isOpen && (jsxRuntime.jsxs("div", { className: "rte-dropdown-menu", role: "listbox", "aria-label": label, children: [options.map((option) => (jsxRuntime.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 && (jsxRuntime.jsx("span", { className: `rte-dropdown-color-preview ${currentValue === option.value ? 'active' : ''}`, style: { backgroundColor: option.color } })), option.preview && !option.headingPreview && (jsxRuntime.jsx("span", { className: "rte-dropdown-fontsize-preview", style: { fontSize: `${option.preview}px` }, children: "Aa" })), option.headingPreview && (jsxRuntime.jsx("span", { className: `rte-dropdown-heading-preview ${option.headingPreview}`, children: option.headingPreview === 'p' ? 'Normal' : option.headingPreview.toUpperCase() })), option.icon && jsxRuntime.jsx(Icon, { icon: option.icon, width: 16, height: 16 }), jsxRuntime.jsx("span", { style: { flex: 1, fontWeight: currentValue === option.value ? 600 : 400 }, children: option.label })] }, option.value))), showCustomColorInput && (jsxRuntime.jsxs("div", { className: "rte-color-custom-input", onMouseDown: (e) => e.stopPropagation(), children: [jsxRuntime.jsx("input", { type: "color", value: customColor, onChange: (e) => setCustomColor(e.target.value), title: "Pick a color" }), jsxRuntime.jsx("input", { type: "text", value: customColor, onChange: (e) => {
123
- const v = e.target.value;
124
- setCustomColor(v);
125
- }, placeholder: "#000000", maxLength: 7, onKeyDown: (e) => {
126
- if (e.key === "Enter") {
127
- e.preventDefault();
128
- if (/^#[0-9a-fA-F]{3,6}$/.test(customColor)) {
129
- handleSelect(customColor);
130
- }
131
- }
132
- } }), jsxRuntime.jsx("button", { type: "button", className: "rte-color-custom-apply", onClick: () => {
133
- if (/^#[0-9a-fA-F]{3,6}$/.test(customColor)) {
134
- handleSelect(customColor);
135
- }
136
- }, children: "Apply" })] }))] }))] }));
156
+ const menuContent = isOpen ? (jsxRuntime.jsxs("div", { ref: menuRef, className: "rte-dropdown-menu", role: "listbox", "aria-label": label, style: {
157
+ position: 'fixed',
158
+ top: menuPos.top,
159
+ left: menuPos.left,
160
+ }, onMouseDown: (e) => e.preventDefault(), children: [options.map((option) => (jsxRuntime.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 && (jsxRuntime.jsx("span", { className: `rte-dropdown-color-preview ${currentValue === option.value ? 'active' : ''}`, style: { backgroundColor: option.color } })), option.preview && !option.headingPreview && (jsxRuntime.jsx("span", { className: "rte-dropdown-fontsize-preview", style: { fontSize: `${option.preview}px` }, children: "Aa" })), option.headingPreview && (jsxRuntime.jsx("span", { className: `rte-dropdown-heading-preview ${option.headingPreview}`, children: option.headingPreview === 'p' ? 'Normal' : option.headingPreview.toUpperCase() })), option.icon && jsxRuntime.jsx(Icon, { icon: option.icon, width: 16, height: 16 }), jsxRuntime.jsx("span", { style: { flex: 1, fontWeight: currentValue === option.value ? 600 : 400 }, children: option.label })] }, option.value))), showCustomColorInput && (jsxRuntime.jsxs("div", { className: "rte-color-custom-input", onMouseDown: (e) => e.stopPropagation(), children: [jsxRuntime.jsx("input", { type: "color", value: customColor, onChange: (e) => setCustomColor(e.target.value), title: "Pick a color" }), jsxRuntime.jsx("input", { type: "text", value: customColor, onChange: (e) => {
161
+ const v = e.target.value;
162
+ setCustomColor(v);
163
+ }, placeholder: "#000000", maxLength: 7, onKeyDown: (e) => {
164
+ if (e.key === "Enter") {
165
+ e.preventDefault();
166
+ if (/^#[0-9a-fA-F]{3,6}$/.test(customColor)) {
167
+ handleSelect(customColor);
168
+ }
169
+ }
170
+ } }), jsxRuntime.jsx("button", { type: "button", className: "rte-color-custom-apply", onClick: () => {
171
+ if (/^#[0-9a-fA-F]{3,6}$/.test(customColor)) {
172
+ handleSelect(customColor);
173
+ }
174
+ }, children: "Apply" })] }))] })) : null;
175
+ return (jsxRuntime.jsxs("div", { className: "rte-dropdown", ref: dropdownRef, onMouseDown: (e) => e.preventDefault(), children: [jsxRuntime.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: [jsxRuntime.jsx(Icon, { icon: icon, width: 18, height: 18 }), currentOption && (jsxRuntime.jsx("span", { className: "rte-dropdown-value", children: currentOption.label }))] }), menuContent && reactDom.createPortal(menuContent, document.body)] }));
137
176
  };
138
177
 
139
- /** Debounce time for pushing to history after input (ms) */
140
- const HISTORY_DEBOUNCE_MS = 300;
141
- /** Width of the clickable checkbox area in pixels */
142
- const CHECKBOX_CLICK_ZONE_PX = 40;
143
-
144
178
  /**
145
179
  * Pure DOM utility functions.
146
180
  * No React dependencies - only native browser APIs.
@@ -393,8 +427,8 @@ function useCheckbox({ editorRef, isUpdatingRef, pushToHistory, notifyChange, ge
393
427
  const rect = listItem.getBoundingClientRect();
394
428
  const clientX = event.clientX;
395
429
  const isInCheckboxArea = listItem.dir === "rtl"
396
- ? clientX >= rect.right - CHECKBOX_CLICK_ZONE_PX
397
- : clientX <= rect.left + CHECKBOX_CLICK_ZONE_PX;
430
+ ? clientX >= rect.right
431
+ : clientX <= rect.left;
398
432
  if (isInCheckboxArea) {
399
433
  event.preventDefault();
400
434
  event.stopPropagation();
@@ -421,8 +455,8 @@ function useCheckbox({ editorRef, isUpdatingRef, pushToHistory, notifyChange, ge
421
455
  const rect = listItem.getBoundingClientRect();
422
456
  const clientX = event.clientX;
423
457
  const isInCheckboxArea = listItem.dir === "rtl"
424
- ? clientX >= rect.right - CHECKBOX_CLICK_ZONE_PX
425
- : clientX <= rect.left + CHECKBOX_CLICK_ZONE_PX;
458
+ ? clientX >= rect.right
459
+ : clientX <= rect.left;
426
460
  if (isInCheckboxArea) {
427
461
  event.preventDefault();
428
462
  }
@@ -680,6 +714,9 @@ function useCheckbox({ editorRef, isUpdatingRef, pushToHistory, notifyChange, ge
680
714
  };
681
715
  }
682
716
 
717
+ /** Debounce time for pushing to history after input (ms) */
718
+ const HISTORY_DEBOUNCE_MS = 300;
719
+
683
720
  /**
684
721
  * Lightweight HTML sanitizer (no external dependencies).
685
722
  *
@@ -810,7 +847,7 @@ const ALLOWED_CONTENT_TAGS = new Set([
810
847
  "ul", "ol", "li", "a", "strong", "em", "u", "s", "del",
811
848
  "sub", "sup", "code", "pre", "blockquote", "br", "hr",
812
849
  "img", "table", "thead", "tbody", "tr", "th", "td",
813
- "b", "i", "strike",
850
+ "b", "i", "strike", "font",
814
851
  ]);
815
852
  /** Checks if an attribute key is safe to set on a DOM element. */
816
853
  function isSafeAttribute(key) {
@@ -1018,6 +1055,7 @@ function domToContent(element) {
1018
1055
  "sub",
1019
1056
  "sup",
1020
1057
  "code",
1058
+ "font",
1021
1059
  ].includes(tagName)) {
1022
1060
  const children = [];
1023
1061
  Array.from(el.childNodes).forEach((child) => {
@@ -1107,6 +1145,46 @@ function domToContent(element) {
1107
1145
  });
1108
1146
  }
1109
1147
  }
1148
+ // <font> from execCommand('foreColor') or pasted HTML
1149
+ if (tagName === "font") {
1150
+ const fontAttrs = {};
1151
+ const colorAttr = el.getAttribute("color");
1152
+ if (colorAttr)
1153
+ fontAttrs.color = colorAttr;
1154
+ const sizeAttr = el.getAttribute("size");
1155
+ if (sizeAttr) {
1156
+ const sizeMap = {
1157
+ "1": "10px", "2": "13px", "3": "16px", "4": "18px",
1158
+ "5": "24px", "6": "32px", "7": "48px",
1159
+ };
1160
+ const mapped = sizeMap[sizeAttr];
1161
+ if (mapped)
1162
+ fontAttrs.fontSize = mapped;
1163
+ }
1164
+ const style = el.getAttribute("style") || "";
1165
+ if (style) {
1166
+ style.split(";").forEach((rule) => {
1167
+ const [key, value] = rule
1168
+ .split(":")
1169
+ .map((s) => s.trim());
1170
+ if (key && value) {
1171
+ if (key === "font-size")
1172
+ fontAttrs.fontSize = value;
1173
+ else if (key === "color")
1174
+ fontAttrs.color = value;
1175
+ else if (key === "background-color")
1176
+ fontAttrs.backgroundColor = value;
1177
+ }
1178
+ });
1179
+ }
1180
+ return {
1181
+ type: "span",
1182
+ children: children.length > 0 ? children : undefined,
1183
+ attributes: Object.keys(fontAttrs).length > 0
1184
+ ? fontAttrs
1185
+ : undefined,
1186
+ };
1187
+ }
1110
1188
  // Map tag names to semantic types
1111
1189
  const type = tagName === "strong" || tagName === "b"
1112
1190
  ? "bold"
@@ -2125,6 +2203,61 @@ function useEditorEvents({ editorRef, historyRef, isUpdatingRef, mountedRef, not
2125
2203
  }, 0);
2126
2204
  return;
2127
2205
  }
2206
+ // Exit code block: Enter on empty last line escapes <pre>
2207
+ if (e.key === "Enter" &&
2208
+ !e.shiftKey &&
2209
+ !isModifierPressed) {
2210
+ const sel = window.getSelection();
2211
+ if (sel && sel.rangeCount > 0 && sel.isCollapsed) {
2212
+ const range = sel.getRangeAt(0);
2213
+ const node = range.startContainer;
2214
+ const pre = (node instanceof HTMLElement
2215
+ ? node
2216
+ : node.parentElement)?.closest("pre");
2217
+ if (pre && pre.lastChild) {
2218
+ const lastChild = pre.lastChild;
2219
+ const cursorInPre = node === pre &&
2220
+ range.startOffset === pre.childNodes.length;
2221
+ const cursorAtEndOfLastText = node.nodeType === Node.TEXT_NODE &&
2222
+ node === lastChild &&
2223
+ range.startOffset ===
2224
+ (node.textContent?.length ?? 0);
2225
+ const isAtEnd = cursorInPre || cursorAtEndOfLastText;
2226
+ const lastIsBr = lastChild instanceof HTMLElement &&
2227
+ lastChild.tagName === "BR";
2228
+ const endsWithNewline = node.nodeType === Node.TEXT_NODE &&
2229
+ (node.textContent || "").endsWith("\n");
2230
+ if (isAtEnd && (lastIsBr || endsWithNewline)) {
2231
+ e.preventDefault();
2232
+ if (lastIsBr) {
2233
+ pre.removeChild(lastChild);
2234
+ }
2235
+ else if (node.nodeType === Node.TEXT_NODE &&
2236
+ node.textContent) {
2237
+ node.textContent =
2238
+ node.textContent.replace(/\n$/, "");
2239
+ }
2240
+ if (!pre.textContent &&
2241
+ !pre.querySelector("br")) {
2242
+ pre.appendChild(document.createElement("br"));
2243
+ }
2244
+ const p = document.createElement("p");
2245
+ p.appendChild(document.createElement("br"));
2246
+ pre.parentNode?.insertBefore(p, pre.nextSibling);
2247
+ const newRange = document.createRange();
2248
+ newRange.setStart(p, 0);
2249
+ newRange.collapse(true);
2250
+ sel.removeAllRanges();
2251
+ sel.addRange(newRange);
2252
+ const content = domToContent(editor);
2253
+ const serializedSel = serializeSelection(editor);
2254
+ historyRef.current.push(content, serializedSel);
2255
+ notifyChange(content);
2256
+ return;
2257
+ }
2258
+ }
2259
+ }
2260
+ }
2128
2261
  // Auto-link: convert URLs to <a> tags on space/enter
2129
2262
  if (!isModifierPressed && (e.key === " " || e.key === "Enter")) {
2130
2263
  handleAutoLink(editor, e);
@@ -4019,7 +4152,7 @@ function createAdvancedLinkPlugin(options = {}) {
4019
4152
  /** Pre-built link plugin with just target enabled (no custom fields). */
4020
4153
  const advancedLinkPlugin = createAdvancedLinkPlugin();
4021
4154
 
4022
- const InsertTableDialog = ({ onInsert, onClose, }) => {
4155
+ const InsertTableDialog = ({ onInsert, onClose, anchorRect, }) => {
4023
4156
  const [rows, setRows] = React.useState(3);
4024
4157
  const [cols, setCols] = React.useState(3);
4025
4158
  const dialogRef = React.useRef(null);
@@ -4033,7 +4166,25 @@ const InsertTableDialog = ({ onInsert, onClose, }) => {
4033
4166
  document.addEventListener("mousedown", handler);
4034
4167
  return () => document.removeEventListener("mousedown", handler);
4035
4168
  }, [onClose]);
4036
- return (jsxRuntime.jsxs("div", { className: "rte-table-insert-dialog", ref: dialogRef, children: [jsxRuntime.jsx("div", { className: "rte-table-insert-title", children: "Insert Table" }), jsxRuntime.jsxs("div", { className: "rte-table-insert-fields", children: [jsxRuntime.jsxs("label", { className: "rte-table-insert-label", children: [jsxRuntime.jsx("span", { children: "Zeilen" }), jsxRuntime.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" })] }), jsxRuntime.jsxs("label", { className: "rte-table-insert-label", children: [jsxRuntime.jsx("span", { children: "Spalten" }), jsxRuntime.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" })] })] }), jsxRuntime.jsx("button", { type: "button", className: "rte-table-insert-btn", onClick: () => onInsert(rows, cols), children: "Insert" })] }));
4169
+ const style = { position: "fixed" };
4170
+ if (anchorRect) {
4171
+ const pad = 8;
4172
+ let top = anchorRect.bottom + 4;
4173
+ let left = anchorRect.left;
4174
+ if (left + 220 > window.innerWidth - pad) {
4175
+ left = window.innerWidth - 220 - pad;
4176
+ }
4177
+ if (left < pad)
4178
+ left = pad;
4179
+ if (top + 200 > window.innerHeight - pad) {
4180
+ top = anchorRect.top - 200 - 4;
4181
+ }
4182
+ if (top < pad)
4183
+ top = pad;
4184
+ style.top = top;
4185
+ style.left = left;
4186
+ }
4187
+ return reactDom.createPortal(jsxRuntime.jsxs("div", { className: "rte-table-insert-dialog", ref: dialogRef, style: style, onMouseDown: (e) => e.preventDefault(), children: [jsxRuntime.jsx("div", { className: "rte-table-insert-title", children: "Insert Table" }), jsxRuntime.jsxs("div", { className: "rte-table-insert-fields", children: [jsxRuntime.jsxs("label", { className: "rte-table-insert-label", children: [jsxRuntime.jsx("span", { children: "Zeilen" }), jsxRuntime.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() })] }), jsxRuntime.jsxs("label", { className: "rte-table-insert-label", children: [jsxRuntime.jsx("span", { children: "Spalten" }), jsxRuntime.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() })] })] }), jsxRuntime.jsx("button", { type: "button", className: "rte-table-insert-btn", onClick: () => onInsert(rows, cols), children: "Insert" })] }), document.body);
4037
4188
  };
4038
4189
  const TableContextMenu = ({ x, y, onClose, }) => {
4039
4190
  const ref = React.useRef(null);
@@ -4054,6 +4205,14 @@ const TableContextMenu = ({ x, y, onClose, }) => {
4054
4205
  };
4055
4206
  const TableToolbarButton = (props) => {
4056
4207
  const [showDialog, setShowDialog] = React.useState(false);
4208
+ const btnRef = React.useRef(null);
4209
+ const [anchorRect, setAnchorRect] = React.useState(null);
4210
+ const handleToggle = React.useCallback(() => {
4211
+ if (!showDialog && btnRef.current) {
4212
+ setAnchorRect(btnRef.current.getBoundingClientRect());
4213
+ }
4214
+ setShowDialog((v) => !v);
4215
+ }, [showDialog]);
4057
4216
  const handleInsert = React.useCallback((rows, cols) => {
4058
4217
  setShowDialog(false);
4059
4218
  if (!props.editorAPI)
@@ -4062,7 +4221,6 @@ const TableToolbarButton = (props) => {
4062
4221
  if (!sel || sel.rangeCount === 0)
4063
4222
  return;
4064
4223
  const range = sel.getRangeAt(0);
4065
- // Find the editor's contentEditable root
4066
4224
  const container = range.commonAncestorContainer;
4067
4225
  const editorEl = container.nodeType === Node.TEXT_NODE
4068
4226
  ? container.parentElement
@@ -4071,7 +4229,6 @@ const TableToolbarButton = (props) => {
4071
4229
  if (!editorRoot)
4072
4230
  return;
4073
4231
  const table = createTable(rows, cols);
4074
- // Insert after the current block element
4075
4232
  let block = editorEl;
4076
4233
  while (block &&
4077
4234
  block !== editorRoot &&
@@ -4084,11 +4241,9 @@ const TableToolbarButton = (props) => {
4084
4241
  else {
4085
4242
  editorRoot.appendChild(table);
4086
4243
  }
4087
- // Add a paragraph after the table so the user can continue typing
4088
4244
  const p = document.createElement("p");
4089
4245
  p.innerHTML = "<br>";
4090
4246
  table.parentNode?.insertBefore(p, table.nextSibling);
4091
- // Focus the first cell
4092
4247
  const firstCell = table.querySelector("td, th");
4093
4248
  if (firstCell) {
4094
4249
  const newRange = document.createRange();
@@ -4098,7 +4253,7 @@ const TableToolbarButton = (props) => {
4098
4253
  sel.addRange(newRange);
4099
4254
  }
4100
4255
  }, [props.editorAPI]);
4101
- return (jsxRuntime.jsxs("div", { style: { position: "relative" }, children: [jsxRuntime.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: jsxRuntime.jsx(IconWrapper, { icon: "mdi:table", width: 18, height: 18 }) }), showDialog && (jsxRuntime.jsx(InsertTableDialog, { onInsert: handleInsert, onClose: () => setShowDialog(false) }))] }));
4256
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.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: jsxRuntime.jsx(IconWrapper, { icon: "mdi:table", width: 18, height: 18 }) }), showDialog && (jsxRuntime.jsx(InsertTableDialog, { onInsert: handleInsert, onClose: () => setShowDialog(false), anchorRect: anchorRect }))] }));
4102
4257
  };
4103
4258
  /* ══════════════════════════════════════════════════════════════════════════
4104
4259
  Table Plugin export
@@ -4308,10 +4463,10 @@ const FloatingToolbar = ({ plugins, editorAPI, editorElement, }) => {
4308
4463
  }
4309
4464
  // Center horizontally on the selection, clamp to viewport
4310
4465
  let left = rect.left + rect.width / 2 - toolbarW / 2;
4311
- if (left < pad)
4312
- left = pad;
4313
4466
  if (left + toolbarW > vw - pad)
4314
4467
  left = vw - toolbarW - pad;
4468
+ if (left < pad)
4469
+ left = pad;
4315
4470
  setPos({ top, left, visible: true });
4316
4471
  }, [editorElement]);
4317
4472
  // Debounce via requestAnimationFrame for smooth repositioning
@@ -4356,16 +4511,14 @@ const FloatingToolbar = ({ plugins, editorAPI, editorElement, }) => {
4356
4511
  requestAnimationFrame(() => scheduleUpdate());
4357
4512
  }, [editorAPI, scheduleUpdate]);
4358
4513
  const isHidden = !pos.visible || leftPlugins.length === 0;
4359
- return (jsxRuntime.jsx("div", { ref: toolbarRef, className: "rte-floating-toolbar", style: {
4514
+ return reactDom.createPortal(jsxRuntime.jsx("div", { ref: toolbarRef, className: "rte-floating-toolbar", style: {
4360
4515
  position: "fixed",
4361
4516
  top: pos.top,
4362
4517
  left: pos.left,
4363
4518
  visibility: isHidden ? "hidden" : "visible",
4364
4519
  opacity: isHidden ? 0 : 1,
4365
4520
  pointerEvents: isHidden ? "none" : "auto",
4366
- },
4367
- // Prevent selection loss when clicking toolbar buttons
4368
- onMouseDown: (e) => e.preventDefault(), children: jsxRuntime.jsxs("div", { className: "rte-floating-toolbar-content", children: [jsxRuntime.jsx("div", { className: "rte-toolbar-left", children: leftPlugins.map((plugin) => {
4521
+ }, onMouseDown: (e) => e.preventDefault(), children: jsxRuntime.jsxs("div", { className: "rte-floating-toolbar-content", children: [jsxRuntime.jsx("div", { className: "rte-toolbar-left", children: leftPlugins.map((plugin) => {
4369
4522
  if (!plugin.renderButton)
4370
4523
  return null;
4371
4524
  const isActive = plugin.isActive
@@ -4399,7 +4552,7 @@ const FloatingToolbar = ({ plugins, editorAPI, editorElement, }) => {
4399
4552
  disabled: !canExecute,
4400
4553
  editorAPI,
4401
4554
  });
4402
- })()] }))] }) }));
4555
+ })()] }))] }) }), document.body);
4403
4556
  };
4404
4557
 
4405
4558
  /**