@haklex/rich-plugin-floating-toolbar 0.1.0 → 0.2.0

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.mjs CHANGED
@@ -1,649 +1,604 @@
1
- import { jsxs, Fragment, jsx } from "react/jsx-runtime";
2
- import { useTextSelectionSnapshot, getDOMRectFromTextSelection } from "@haklex/rich-editor";
3
- import { $toggleSpoilerSelection, $selectionTouchesSpoiler } from "@haklex/rich-editor/commands";
4
- import { $isKaTeXInlineNode, $createRubyNode, $isRubyNode } from "@haklex/rich-editor/nodes";
1
+ import { getDOMRectFromTextSelection, useTextSelectionSnapshot } from "@haklex/rich-editor";
2
+ import { $selectionTouchesSpoiler, $toggleSpoilerSelection } from "@haklex/rich-editor/commands";
3
+ import { $createRubyNode, $isKaTeXInlineNode, $isRubyNode } from "@haklex/rich-editor/nodes";
5
4
  import { ColorPicker } from "@haklex/rich-editor-ui";
6
- import { usePortalTheme, usePortalContainer, vars } from "@haklex/rich-style-token";
7
- import { TOGGLE_LINK_COMMAND, $isLinkNode, $isAutoLinkNode } from "@lexical/link";
5
+ import { usePortalContainer, usePortalTheme, vars } from "@haklex/rich-style-token";
6
+ import { $isAutoLinkNode, $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link";
8
7
  import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
9
- import { $patchStyleText, $getSelectionStyleValueForProperty } from "@lexical/selection";
10
- import { $getSelection, $isRangeSelection, SELECTION_CHANGE_COMMAND, COMMAND_PRIORITY_LOW, FORMAT_TEXT_COMMAND, $createTextNode, $getNodeByKey } from "lexical";
11
- import { Bold, Italic, Underline, Strikethrough, Superscript, Subscript, Code, Highlighter, EyeOff, Link, Languages, Check, X, Trash2 } from "lucide-react";
12
- import { useRef, useState, useCallback, useEffect } from "react";
8
+ import { $getSelectionStyleValueForProperty, $patchStyleText } from "@lexical/selection";
9
+ import { $createTextNode, $getNodeByKey, $getSelection, $isRangeSelection, COMMAND_PRIORITY_LOW, FORMAT_TEXT_COMMAND, SELECTION_CHANGE_COMMAND } from "lexical";
10
+ import { Bold, Check, Code, EyeOff, Highlighter, Italic, Languages, Link, Strikethrough, Subscript, Superscript, Trash2, Underline, X } from "lucide-react";
11
+ import { useCallback, useEffect, useRef, useState } from "react";
13
12
  import { createPortal } from "react-dom";
13
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
14
+ //#region src/styles.css.ts
14
15
  var toolbar = "_1m6axz71";
15
16
  var btn = "_1m6axz72";
16
17
  var btnActive = "_1m6axz73";
17
- var btnIndicator = "_1m6axz74";
18
18
  var rubyEditor = "_1m6axz75";
19
- var rubyPreview = "_1m6axz76";
20
- var rubyPreviewReading = "_1m6axz77";
21
- var rubyPreviewBase = "_1m6axz78";
22
- var rubyInputRow = "_1m6axz79";
23
- var rubyInput = "_1m6axz7a";
24
- var rubyActionBtn = "_1m6axz7b";
25
- var rubyHint = "_1m6axz7c";
26
- var separator = "_1m6axz7d";
19
+ //#endregion
20
+ //#region src/FloatingToolbarPlugin.tsx
27
21
  function isEffectiveLinkNode(node) {
28
- if (!$isLinkNode(node)) return false;
29
- return !$isAutoLinkNode(node) || !node.getIsUnlinked();
22
+ if (!$isLinkNode(node)) return false;
23
+ return !$isAutoLinkNode(node) || !node.getIsUnlinked();
30
24
  }
31
25
  function isRegularLinkNode(node) {
32
- return $isLinkNode(node) && !$isAutoLinkNode(node);
26
+ return $isLinkNode(node) && !$isAutoLinkNode(node);
33
27
  }
34
28
  function collectSelectedActiveAutoLinkNodes(selection) {
35
- const autoLinkNodes = /* @__PURE__ */ new Map();
36
- for (const node of selection.getNodes()) {
37
- if ($isAutoLinkNode(node) && !node.getIsUnlinked()) {
38
- autoLinkNodes.set(node.getKey(), node);
39
- }
40
- const parent = node.getParent();
41
- if ($isAutoLinkNode(parent) && !parent.getIsUnlinked()) {
42
- autoLinkNodes.set(parent.getKey(), parent);
43
- }
44
- }
45
- return Array.from(autoLinkNodes.values());
29
+ const autoLinkNodes = /* @__PURE__ */ new Map();
30
+ for (const node of selection.getNodes()) {
31
+ if ($isAutoLinkNode(node) && !node.getIsUnlinked()) autoLinkNodes.set(node.getKey(), node);
32
+ const parent = node.getParent();
33
+ if ($isAutoLinkNode(parent) && !parent.getIsUnlinked()) autoLinkNodes.set(parent.getKey(), parent);
34
+ }
35
+ return Array.from(autoLinkNodes.values());
46
36
  }
47
- const INITIAL_STATE = {
48
- isBold: false,
49
- isItalic: false,
50
- isUnderline: false,
51
- isStrikethrough: false,
52
- isSuperscript: false,
53
- isSubscript: false,
54
- isCode: false,
55
- isHighlight: false,
56
- isLink: false,
57
- isRuby: false,
58
- isSpoiler: false,
59
- fontColor: ""
37
+ var INITIAL_STATE = {
38
+ isBold: false,
39
+ isItalic: false,
40
+ isUnderline: false,
41
+ isStrikethrough: false,
42
+ isSuperscript: false,
43
+ isSubscript: false,
44
+ isCode: false,
45
+ isHighlight: false,
46
+ isLink: false,
47
+ isRuby: false,
48
+ isSpoiler: false,
49
+ fontColor: ""
60
50
  };
61
51
  function findRubyAncestor(node) {
62
- let current = node;
63
- while (current) {
64
- if ($isRubyNode(current)) {
65
- return current;
66
- }
67
- current = current.getParent();
68
- }
69
- return null;
52
+ let current = node;
53
+ while (current) {
54
+ if ($isRubyNode(current)) return current;
55
+ current = current.getParent();
56
+ }
57
+ return null;
70
58
  }
71
59
  function getSelectedRubyNodes(nodes) {
72
- const rubyNodes = /* @__PURE__ */ new Map();
73
- for (const node of nodes) {
74
- const rubyNode = findRubyAncestor(node);
75
- if (rubyNode) {
76
- rubyNodes.set(rubyNode.getKey(), rubyNode);
77
- }
78
- }
79
- return Array.from(rubyNodes.values());
60
+ const rubyNodes = /* @__PURE__ */ new Map();
61
+ for (const node of nodes) {
62
+ const rubyNode = findRubyAncestor(node);
63
+ if (rubyNode) rubyNodes.set(rubyNode.getKey(), rubyNode);
64
+ }
65
+ return Array.from(rubyNodes.values());
80
66
  }
81
67
  function getSelectionState(selection) {
82
- const nodes = selection.getNodes();
83
- const hasLink = nodes.some((node) => {
84
- const parent = node.getParent();
85
- return isEffectiveLinkNode(parent) || isEffectiveLinkNode(node);
86
- });
87
- const hasRuby = getSelectedRubyNodes(nodes).length > 0;
88
- const textColor = $getSelectionStyleValueForProperty(selection, "color", "");
89
- let fontColor = textColor;
90
- if (!fontColor) {
91
- const katex = nodes.find((n) => $isKaTeXInlineNode(n));
92
- if (katex) fontColor = katex.getColor() ?? "";
93
- }
94
- return {
95
- isBold: selection.hasFormat("bold"),
96
- isItalic: selection.hasFormat("italic"),
97
- isUnderline: selection.hasFormat("underline"),
98
- isStrikethrough: selection.hasFormat("strikethrough"),
99
- isSuperscript: selection.hasFormat("superscript"),
100
- isSubscript: selection.hasFormat("subscript"),
101
- isCode: selection.hasFormat("code"),
102
- isHighlight: selection.hasFormat("highlight"),
103
- isLink: hasLink,
104
- isRuby: hasRuby,
105
- isSpoiler: $selectionTouchesSpoiler(selection),
106
- fontColor
107
- };
68
+ const nodes = selection.getNodes();
69
+ const hasLink = nodes.some((node) => {
70
+ return isEffectiveLinkNode(node.getParent()) || isEffectiveLinkNode(node);
71
+ });
72
+ const hasRuby = getSelectedRubyNodes(nodes).length > 0;
73
+ let fontColor = $getSelectionStyleValueForProperty(selection, "color", "");
74
+ if (!fontColor) {
75
+ const katex = nodes.find((n) => $isKaTeXInlineNode(n));
76
+ if (katex) fontColor = katex.getColor() ?? "";
77
+ }
78
+ return {
79
+ isBold: selection.hasFormat("bold"),
80
+ isItalic: selection.hasFormat("italic"),
81
+ isUnderline: selection.hasFormat("underline"),
82
+ isStrikethrough: selection.hasFormat("strikethrough"),
83
+ isSuperscript: selection.hasFormat("superscript"),
84
+ isSubscript: selection.hasFormat("subscript"),
85
+ isCode: selection.hasFormat("code"),
86
+ isHighlight: selection.hasFormat("highlight"),
87
+ isLink: hasLink,
88
+ isRuby: hasRuby,
89
+ isSpoiler: $selectionTouchesSpoiler(selection),
90
+ fontColor
91
+ };
108
92
  }
109
- function computePosition(rect, toolbar2, container) {
110
- if (rect.width === 0 && rect.height === 0) return null;
111
- const toolbarWidth = toolbar2.offsetWidth;
112
- const toolbarHeight = toolbar2.offsetHeight;
113
- const isBody = container === document.body;
114
- const containerRect = isBody ? void 0 : container.getBoundingClientRect();
115
- const offsetX = containerRect?.left ?? 0;
116
- const offsetY = containerRect?.top ?? 0;
117
- const availableWidth = containerRect?.width ?? window.innerWidth;
118
- const rawLeft = rect.left - offsetX + rect.width / 2 - toolbarWidth / 2;
119
- const clampedLeft = Math.max(8, Math.min(rawLeft, availableWidth - toolbarWidth - 8));
120
- return {
121
- top: rect.top - offsetY - toolbarHeight - 10,
122
- left: clampedLeft
123
- };
93
+ function computePosition(rect, toolbar, container) {
94
+ if (rect.width === 0 && rect.height === 0) return null;
95
+ const toolbarWidth = toolbar.offsetWidth;
96
+ const toolbarHeight = toolbar.offsetHeight;
97
+ const containerRect = container === document.body ? void 0 : container.getBoundingClientRect();
98
+ const offsetX = containerRect?.left ?? 0;
99
+ const offsetY = containerRect?.top ?? 0;
100
+ const availableWidth = containerRect?.width ?? window.innerWidth;
101
+ const rawLeft = rect.left - offsetX + rect.width / 2 - toolbarWidth / 2;
102
+ const clampedLeft = Math.max(8, Math.min(rawLeft, availableWidth - toolbarWidth - 8));
103
+ return {
104
+ top: rect.top - offsetY - toolbarHeight - 10,
105
+ left: clampedLeft
106
+ };
124
107
  }
125
108
  function ToolbarButton({ active, onClick, ariaLabel, children }) {
126
- return /* @__PURE__ */ jsxs(
127
- "button",
128
- {
129
- "aria-label": ariaLabel,
130
- "aria-pressed": active,
131
- className: `${btn}${active ? ` ${btnActive}` : ""}`,
132
- type: "button",
133
- onMouseDown: (e) => {
134
- e.preventDefault();
135
- onClick();
136
- },
137
- children: [
138
- children,
139
- active && /* @__PURE__ */ jsx("span", { className: btnIndicator })
140
- ]
141
- }
142
- );
109
+ return /* @__PURE__ */ jsxs("button", {
110
+ "aria-label": ariaLabel,
111
+ "aria-pressed": active,
112
+ className: `${btn}${active ? ` ${btnActive}` : ""}`,
113
+ type: "button",
114
+ onMouseDown: (e) => {
115
+ e.preventDefault();
116
+ onClick();
117
+ },
118
+ children: [children, active && /* @__PURE__ */ jsx("span", { className: "_1m6axz74" })]
119
+ });
143
120
  }
144
- const ICON_SIZE = 15;
145
- const ICON_STROKE = 2;
121
+ var ICON_SIZE = 15;
122
+ var ICON_STROKE = 2;
146
123
  function extractCssVarName(value) {
147
- const match = value.match(/^var\((--[^\s),]+)(?:,[^)]+)?\)$/);
148
- return match?.[1] ?? null;
124
+ return value.match(/^var\((--[^\s),]+)(?:,[^)]+)?\)$/)?.[1] ?? null;
149
125
  }
150
126
  function collectThemeVarNames(contract, output) {
151
- if (typeof contract === "string") {
152
- const cssVarName = extractCssVarName(contract);
153
- if (cssVarName) output.add(cssVarName);
154
- return output;
155
- }
156
- if (contract && typeof contract === "object") {
157
- for (const value of Object.values(contract)) {
158
- collectThemeVarNames(value, output);
159
- }
160
- }
161
- return output;
127
+ if (typeof contract === "string") {
128
+ const cssVarName = extractCssVarName(contract);
129
+ if (cssVarName) output.add(cssVarName);
130
+ return output;
131
+ }
132
+ if (contract && typeof contract === "object") for (const value of Object.values(contract)) collectThemeVarNames(value, output);
133
+ return output;
162
134
  }
163
- const THEME_VAR_NAMES = Array.from(collectThemeVarNames(vars, /* @__PURE__ */ new Set()));
164
- function FloatingToolbarPlugin({
165
- actions
166
- } = {}) {
167
- const [editor] = useLexicalComposerContext();
168
- const { className: portalClassName } = usePortalTheme();
169
- const portalContainer = usePortalContainer();
170
- const selectionSnapshot = useTextSelectionSnapshot();
171
- const toolbarRef = useRef(null);
172
- const [visible, setVisible] = useState(false);
173
- const [state, setState] = useState(INITIAL_STATE);
174
- const [rubyEdit, setRubyEdit] = useState(null);
175
- const rubyEditorRef = useRef(null);
176
- const rubyInputRef = useRef(null);
177
- const rubyEditRef = useRef(rubyEdit);
178
- rubyEditRef.current = rubyEdit;
179
- const updateToolbar = useCallback(() => {
180
- const selection = $getSelection();
181
- if (!$isRangeSelection(selection) || selection.isCollapsed()) {
182
- setVisible(false);
183
- return;
184
- }
185
- setState(getSelectionState(selection));
186
- setVisible(true);
187
- }, []);
188
- const applyThemeVars = useCallback(
189
- (toolbar2) => {
190
- const rootElement = editor.getRootElement();
191
- if (!rootElement) return;
192
- const computed = window.getComputedStyle(rootElement);
193
- for (const name of THEME_VAR_NAMES) {
194
- const value = computed.getPropertyValue(name).trim();
195
- if (value) {
196
- toolbar2.style.setProperty(name, value);
197
- }
198
- }
199
- },
200
- [editor]
201
- );
202
- useEffect(() => {
203
- const unregisterCommand = editor.registerCommand(
204
- SELECTION_CHANGE_COMMAND,
205
- () => {
206
- updateToolbar();
207
- return false;
208
- },
209
- COMMAND_PRIORITY_LOW
210
- );
211
- const unregisterUpdate = editor.registerUpdateListener(({ editorState }) => {
212
- editorState.read(() => {
213
- updateToolbar();
214
- });
215
- });
216
- return () => {
217
- unregisterCommand();
218
- unregisterUpdate();
219
- };
220
- }, [editor, updateToolbar]);
221
- useEffect(() => {
222
- if (!selectionSnapshot) {
223
- setVisible(false);
224
- }
225
- }, [selectionSnapshot]);
226
- useEffect(() => {
227
- if (!visible || !toolbarRef.current || !selectionSnapshot) return;
228
- const positionToolbar = () => {
229
- const toolbar2 = toolbarRef.current;
230
- if (!toolbar2) return;
231
- applyThemeVars(toolbar2);
232
- const rootElement2 = editor.getRootElement();
233
- if (!rootElement2) {
234
- setVisible(false);
235
- return;
236
- }
237
- const rect = getDOMRectFromTextSelection(rootElement2, selectionSnapshot);
238
- if (!rect) {
239
- setVisible(false);
240
- return;
241
- }
242
- const pos = computePosition(rect, toolbar2, portalContainer);
243
- if (!pos) {
244
- setVisible(false);
245
- return;
246
- }
247
- toolbar2.style.top = `${pos.top}px`;
248
- toolbar2.style.left = `${pos.left}px`;
249
- };
250
- requestAnimationFrame(positionToolbar);
251
- const rootElement = editor.getRootElement();
252
- if (!rootElement) return;
253
- let scrollParent = window;
254
- let el = rootElement.parentElement;
255
- while (el) {
256
- const { overflowY } = window.getComputedStyle(el);
257
- if (overflowY === "auto" || overflowY === "scroll") {
258
- scrollParent = el;
259
- break;
260
- }
261
- el = el.parentElement;
262
- }
263
- const onScroll = () => requestAnimationFrame(positionToolbar);
264
- scrollParent.addEventListener("scroll", onScroll, { passive: true });
265
- return () => scrollParent.removeEventListener("scroll", onScroll);
266
- }, [applyThemeVars, editor, portalContainer, selectionSnapshot, state, visible]);
267
- const handleFormat = useCallback(
268
- (format) => {
269
- editor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
270
- },
271
- [editor]
272
- );
273
- const handleLink = useCallback(() => {
274
- editor.update(() => {
275
- const selection = $getSelection();
276
- if (!$isRangeSelection(selection)) return;
277
- const nodes = selection.getNodes();
278
- const hasLink = nodes.some((node) => {
279
- const parent = node.getParent();
280
- return isEffectiveLinkNode(parent) || isEffectiveLinkNode(node);
281
- });
282
- if (hasLink) {
283
- for (const autoLinkNode of collectSelectedActiveAutoLinkNodes(selection)) {
284
- autoLinkNode.setIsUnlinked(true);
285
- autoLinkNode.markDirty();
286
- }
287
- const hasRegularLink = nodes.some((node) => {
288
- const parent = node.getParent();
289
- return isRegularLinkNode(parent) || isRegularLinkNode(node);
290
- });
291
- if (hasRegularLink) {
292
- editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
293
- }
294
- } else {
295
- const text = selection.getTextContent();
296
- const url = /^https?:\/\//.test(text) ? text : "";
297
- editor.dispatchCommand(TOGGLE_LINK_COMMAND, url || "https://");
298
- }
299
- });
300
- }, [editor]);
301
- const handleColor = useCallback(
302
- (value) => {
303
- editor.update(() => {
304
- const sel = $getSelection();
305
- if (!$isRangeSelection(sel)) return;
306
- const color = value === "inherit" ? null : value;
307
- $patchStyleText(sel, { color });
308
- for (const node of sel.getNodes()) {
309
- if ($isKaTeXInlineNode(node)) node.setColor(color);
310
- }
311
- });
312
- },
313
- [editor]
314
- );
315
- const handleRuby = useCallback(() => {
316
- editor.update(() => {
317
- const selection = $getSelection();
318
- if (!$isRangeSelection(selection)) return;
319
- const nodes = selection.getNodes();
320
- const rubyNodes = getSelectedRubyNodes(nodes);
321
- if (rubyNodes.length > 0) {
322
- const rubyNode = rubyNodes[0];
323
- if (!rubyNode) return;
324
- setRubyEdit({
325
- nodeKey: rubyNode.getKey(),
326
- reading: rubyNode.getReading(),
327
- baseText: rubyNode.getTextContent(),
328
- isNew: false
329
- });
330
- } else {
331
- const text = selection.getTextContent();
332
- if (!text.trim()) return;
333
- selection.removeText();
334
- const rubyNode = $createRubyNode("");
335
- rubyNode.append($createTextNode(text));
336
- const freshSelection = $getSelection();
337
- if ($isRangeSelection(freshSelection)) {
338
- freshSelection.insertNodes([rubyNode]);
339
- }
340
- setRubyEdit({
341
- nodeKey: rubyNode.getKey(),
342
- reading: "",
343
- baseText: text,
344
- isNew: true
345
- });
346
- }
347
- });
348
- }, [editor]);
349
- const handleRubyConfirm = useCallback(() => {
350
- const edit = rubyEditRef.current;
351
- if (!edit) return;
352
- editor.update(() => {
353
- const node = $getNodeByKey(edit.nodeKey);
354
- if ($isRubyNode(node)) {
355
- node.setReading(edit.reading);
356
- }
357
- });
358
- setRubyEdit(null);
359
- }, [editor]);
360
- const handleRubyCancel = useCallback(() => {
361
- const edit = rubyEditRef.current;
362
- if (!edit) return;
363
- if (edit.isNew) {
364
- editor.update(() => {
365
- const node = $getNodeByKey(edit.nodeKey);
366
- if ($isRubyNode(node)) {
367
- const children = node.getChildren();
368
- for (const child of children) {
369
- node.insertBefore(child);
370
- }
371
- node.remove();
372
- }
373
- });
374
- }
375
- setRubyEdit(null);
376
- }, [editor]);
377
- const handleRubyDelete = useCallback(() => {
378
- const edit = rubyEditRef.current;
379
- if (!edit) return;
380
- editor.update(() => {
381
- const node = $getNodeByKey(edit.nodeKey);
382
- if ($isRubyNode(node)) {
383
- const children = node.getChildren();
384
- for (const child of children) {
385
- node.insertBefore(child);
386
- }
387
- node.remove();
388
- }
389
- });
390
- setRubyEdit(null);
391
- }, [editor]);
392
- const isRubyEditing = rubyEdit !== null;
393
- useEffect(() => {
394
- if (!isRubyEditing) return;
395
- const handleClickOutside = (e) => {
396
- const currentEdit = rubyEditRef.current;
397
- if (!currentEdit) return;
398
- if (rubyEditorRef.current && !rubyEditorRef.current.contains(e.target)) {
399
- if (currentEdit.reading.trim()) {
400
- editor.update(() => {
401
- const node = $getNodeByKey(currentEdit.nodeKey);
402
- if ($isRubyNode(node)) {
403
- node.setReading(currentEdit.reading);
404
- }
405
- });
406
- } else if (currentEdit.isNew) {
407
- editor.update(() => {
408
- const node = $getNodeByKey(currentEdit.nodeKey);
409
- if ($isRubyNode(node)) {
410
- const children = node.getChildren();
411
- for (const child of children) {
412
- node.insertBefore(child);
413
- }
414
- node.remove();
415
- }
416
- });
417
- }
418
- setRubyEdit(null);
419
- }
420
- };
421
- const timer = setTimeout(() => {
422
- document.addEventListener("mousedown", handleClickOutside);
423
- }, 0);
424
- return () => {
425
- clearTimeout(timer);
426
- document.removeEventListener("mousedown", handleClickOutside);
427
- };
428
- }, [editor, isRubyEditing]);
429
- useEffect(() => {
430
- if (!rubyEdit || !rubyEditorRef.current) return;
431
- const positionEditor = () => {
432
- const editorEl = rubyEditorRef.current;
433
- if (!editorEl) return;
434
- applyThemeVars(editorEl);
435
- const rubyDom = editor.getElementByKey(rubyEdit.nodeKey);
436
- if (!rubyDom) return;
437
- const rect = rubyDom.getBoundingClientRect();
438
- const isBody = portalContainer === document.body;
439
- const containerRect = isBody ? void 0 : portalContainer.getBoundingClientRect();
440
- const oX = containerRect?.left ?? 0;
441
- const oY = containerRect?.top ?? 0;
442
- const availW = containerRect?.width ?? window.innerWidth;
443
- const editorWidth = editorEl.offsetWidth;
444
- const rawLeft = rect.left - oX + rect.width / 2 - editorWidth / 2;
445
- const clampedLeft = Math.max(8, Math.min(rawLeft, availW - editorWidth - 8));
446
- editorEl.style.top = `${rect.bottom - oY + 8}px`;
447
- editorEl.style.left = `${clampedLeft}px`;
448
- };
449
- requestAnimationFrame(positionEditor);
450
- requestAnimationFrame(() => {
451
- rubyInputRef.current?.focus();
452
- });
453
- }, [applyThemeVars, editor, rubyEdit]);
454
- if (!visible && !rubyEdit) return null;
455
- const toolbarClassName = portalClassName ? `${toolbar} ${portalClassName}` : toolbar;
456
- const rubyEditorClassName = portalClassName ? `${rubyEditor} ${portalClassName}` : rubyEditor;
457
- return /* @__PURE__ */ jsxs(Fragment, { children: [
458
- visible && !rubyEdit && createPortal(
459
- /* @__PURE__ */ jsxs(
460
- "div",
461
- {
462
- "aria-label": "Text formatting",
463
- className: toolbarClassName,
464
- ref: toolbarRef,
465
- role: "toolbar",
466
- style: { position: "fixed", zIndex: 50 },
467
- children: [
468
- /* @__PURE__ */ jsx(
469
- ToolbarButton,
470
- {
471
- active: state.isBold,
472
- ariaLabel: "Bold",
473
- onClick: () => handleFormat("bold"),
474
- children: /* @__PURE__ */ jsx(Bold, { size: ICON_SIZE, strokeWidth: ICON_STROKE })
475
- }
476
- ),
477
- /* @__PURE__ */ jsx(
478
- ToolbarButton,
479
- {
480
- active: state.isItalic,
481
- ariaLabel: "Italic",
482
- onClick: () => handleFormat("italic"),
483
- children: /* @__PURE__ */ jsx(Italic, { size: ICON_SIZE, strokeWidth: ICON_STROKE })
484
- }
485
- ),
486
- /* @__PURE__ */ jsx(
487
- ToolbarButton,
488
- {
489
- active: state.isUnderline,
490
- ariaLabel: "Underline",
491
- onClick: () => handleFormat("underline"),
492
- children: /* @__PURE__ */ jsx(Underline, { size: ICON_SIZE, strokeWidth: ICON_STROKE })
493
- }
494
- ),
495
- /* @__PURE__ */ jsx(
496
- ToolbarButton,
497
- {
498
- active: state.isStrikethrough,
499
- ariaLabel: "Strikethrough",
500
- onClick: () => handleFormat("strikethrough"),
501
- children: /* @__PURE__ */ jsx(Strikethrough, { size: ICON_SIZE, strokeWidth: ICON_STROKE })
502
- }
503
- ),
504
- /* @__PURE__ */ jsx(
505
- ToolbarButton,
506
- {
507
- active: state.isSuperscript,
508
- ariaLabel: "Superscript",
509
- onClick: () => handleFormat("superscript"),
510
- children: /* @__PURE__ */ jsx(Superscript, { size: ICON_SIZE, strokeWidth: ICON_STROKE })
511
- }
512
- ),
513
- /* @__PURE__ */ jsx(
514
- ToolbarButton,
515
- {
516
- active: state.isSubscript,
517
- ariaLabel: "Subscript",
518
- onClick: () => handleFormat("subscript"),
519
- children: /* @__PURE__ */ jsx(Subscript, { size: ICON_SIZE, strokeWidth: ICON_STROKE })
520
- }
521
- ),
522
- /* @__PURE__ */ jsx("span", { className: separator }),
523
- /* @__PURE__ */ jsx(
524
- ToolbarButton,
525
- {
526
- active: state.isCode,
527
- ariaLabel: "Code",
528
- onClick: () => handleFormat("code"),
529
- children: /* @__PURE__ */ jsx(Code, { size: ICON_SIZE, strokeWidth: ICON_STROKE })
530
- }
531
- ),
532
- /* @__PURE__ */ jsx(
533
- ToolbarButton,
534
- {
535
- active: state.isHighlight,
536
- ariaLabel: "Highlight",
537
- onClick: () => handleFormat("highlight"),
538
- children: /* @__PURE__ */ jsx(Highlighter, { size: ICON_SIZE, strokeWidth: ICON_STROKE })
539
- }
540
- ),
541
- /* @__PURE__ */ jsx(
542
- ToolbarButton,
543
- {
544
- active: state.isSpoiler,
545
- ariaLabel: "Spoiler",
546
- onClick: () => editor.update($toggleSpoilerSelection),
547
- children: /* @__PURE__ */ jsx(EyeOff, { size: ICON_SIZE, strokeWidth: ICON_STROKE })
548
- }
549
- ),
550
- /* @__PURE__ */ jsx(ToolbarButton, { active: state.isLink, ariaLabel: "Link", onClick: handleLink, children: /* @__PURE__ */ jsx(Link, { size: ICON_SIZE, strokeWidth: ICON_STROKE }) }),
551
- /* @__PURE__ */ jsx(ToolbarButton, { active: state.isRuby, ariaLabel: "Ruby annotation", onClick: handleRuby, children: /* @__PURE__ */ jsx(Languages, { size: ICON_SIZE, strokeWidth: ICON_STROKE }) }),
552
- /* @__PURE__ */ jsx("span", { className: separator }),
553
- /* @__PURE__ */ jsx(ColorPicker, { currentColor: state.fontColor || "inherit", onSelect: handleColor }),
554
- actions && /* @__PURE__ */ jsxs(Fragment, { children: [
555
- /* @__PURE__ */ jsx("span", { className: separator }),
556
- actions
557
- ] })
558
- ]
559
- }
560
- ),
561
- portalContainer
562
- ),
563
- rubyEdit && createPortal(
564
- /* @__PURE__ */ jsxs(
565
- "div",
566
- {
567
- className: rubyEditorClassName,
568
- ref: rubyEditorRef,
569
- style: { position: "fixed", zIndex: 51 },
570
- children: [
571
- /* @__PURE__ */ jsxs("div", { className: rubyPreview, children: [
572
- /* @__PURE__ */ jsx("span", { className: rubyPreviewReading, children: rubyEdit.reading || " " }),
573
- /* @__PURE__ */ jsx("span", { className: rubyPreviewBase, children: rubyEdit.baseText })
574
- ] }),
575
- /* @__PURE__ */ jsxs("div", { className: rubyInputRow, children: [
576
- /* @__PURE__ */ jsx(
577
- "input",
578
- {
579
- className: rubyInput,
580
- placeholder: "读音",
581
- ref: rubyInputRef,
582
- value: rubyEdit.reading,
583
- onChange: (e) => setRubyEdit((prev) => prev ? { ...prev, reading: e.target.value } : null),
584
- onKeyDown: (e) => {
585
- if (e.key === "Enter") {
586
- e.preventDefault();
587
- handleRubyConfirm();
588
- }
589
- if (e.key === "Escape") {
590
- e.preventDefault();
591
- handleRubyCancel();
592
- }
593
- }
594
- }
595
- ),
596
- /* @__PURE__ */ jsx(
597
- "button",
598
- {
599
- "aria-label": "Confirm",
600
- className: rubyActionBtn,
601
- style: { color: "#22c55e" },
602
- type: "button",
603
- onMouseDown: (e) => {
604
- e.preventDefault();
605
- handleRubyConfirm();
606
- },
607
- children: /* @__PURE__ */ jsx(Check, { size: 14, strokeWidth: ICON_STROKE })
608
- }
609
- ),
610
- /* @__PURE__ */ jsx(
611
- "button",
612
- {
613
- "aria-label": "Cancel",
614
- className: rubyActionBtn,
615
- type: "button",
616
- onMouseDown: (e) => {
617
- e.preventDefault();
618
- handleRubyCancel();
619
- },
620
- children: /* @__PURE__ */ jsx(X, { size: 14, strokeWidth: ICON_STROKE })
621
- }
622
- ),
623
- /* @__PURE__ */ jsx("span", { className: separator }),
624
- /* @__PURE__ */ jsx(
625
- "button",
626
- {
627
- "aria-label": "Delete ruby",
628
- className: rubyActionBtn,
629
- style: { color: "#ef4444" },
630
- type: "button",
631
- onMouseDown: (e) => {
632
- e.preventDefault();
633
- handleRubyDelete();
634
- },
635
- children: /* @__PURE__ */ jsx(Trash2, { size: 14, strokeWidth: ICON_STROKE })
636
- }
637
- )
638
- ] }),
639
- /* @__PURE__ */ jsx("span", { className: rubyHint, children: "Enter 保存 / Esc 取消" })
640
- ]
641
- }
642
- ),
643
- portalContainer
644
- )
645
- ] });
135
+ var THEME_VAR_NAMES = Array.from(collectThemeVarNames(vars, /* @__PURE__ */ new Set()));
136
+ function FloatingToolbarPlugin({ actions } = {}) {
137
+ const [editor] = useLexicalComposerContext();
138
+ const { className: portalClassName } = usePortalTheme();
139
+ const portalContainer = usePortalContainer();
140
+ const selectionSnapshot = useTextSelectionSnapshot();
141
+ const toolbarRef = useRef(null);
142
+ const [visible, setVisible] = useState(false);
143
+ const [state, setState] = useState(INITIAL_STATE);
144
+ const [rubyEdit, setRubyEdit] = useState(null);
145
+ const rubyEditorRef = useRef(null);
146
+ const rubyInputRef = useRef(null);
147
+ const rubyEditRef = useRef(rubyEdit);
148
+ rubyEditRef.current = rubyEdit;
149
+ const updateToolbar = useCallback(() => {
150
+ const selection = $getSelection();
151
+ if (!$isRangeSelection(selection) || selection.isCollapsed()) {
152
+ setVisible(false);
153
+ return;
154
+ }
155
+ setState(getSelectionState(selection));
156
+ setVisible(true);
157
+ }, []);
158
+ const applyThemeVars = useCallback((toolbar) => {
159
+ const rootElement = editor.getRootElement();
160
+ if (!rootElement) return;
161
+ const computed = window.getComputedStyle(rootElement);
162
+ for (const name of THEME_VAR_NAMES) {
163
+ const value = computed.getPropertyValue(name).trim();
164
+ if (value) toolbar.style.setProperty(name, value);
165
+ }
166
+ }, [editor]);
167
+ useEffect(() => {
168
+ const unregisterCommand = editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
169
+ updateToolbar();
170
+ return false;
171
+ }, COMMAND_PRIORITY_LOW);
172
+ const unregisterUpdate = editor.registerUpdateListener(({ editorState }) => {
173
+ editorState.read(() => {
174
+ updateToolbar();
175
+ });
176
+ });
177
+ return () => {
178
+ unregisterCommand();
179
+ unregisterUpdate();
180
+ };
181
+ }, [editor, updateToolbar]);
182
+ useEffect(() => {
183
+ if (!selectionSnapshot) setVisible(false);
184
+ }, [selectionSnapshot]);
185
+ useEffect(() => {
186
+ if (!visible || !toolbarRef.current || !selectionSnapshot) return;
187
+ const positionToolbar = () => {
188
+ const toolbar = toolbarRef.current;
189
+ if (!toolbar) return;
190
+ applyThemeVars(toolbar);
191
+ const rootElement = editor.getRootElement();
192
+ if (!rootElement) {
193
+ setVisible(false);
194
+ return;
195
+ }
196
+ const rect = getDOMRectFromTextSelection(rootElement, selectionSnapshot);
197
+ if (!rect) {
198
+ setVisible(false);
199
+ return;
200
+ }
201
+ const pos = computePosition(rect, toolbar, portalContainer);
202
+ if (!pos) {
203
+ setVisible(false);
204
+ return;
205
+ }
206
+ toolbar.style.top = `${pos.top}px`;
207
+ toolbar.style.left = `${pos.left}px`;
208
+ };
209
+ requestAnimationFrame(positionToolbar);
210
+ const rootElement = editor.getRootElement();
211
+ if (!rootElement) return;
212
+ let scrollParent = window;
213
+ let el = rootElement.parentElement;
214
+ while (el) {
215
+ const { overflowY } = window.getComputedStyle(el);
216
+ if (overflowY === "auto" || overflowY === "scroll") {
217
+ scrollParent = el;
218
+ break;
219
+ }
220
+ el = el.parentElement;
221
+ }
222
+ const onScroll = () => requestAnimationFrame(positionToolbar);
223
+ scrollParent.addEventListener("scroll", onScroll, { passive: true });
224
+ return () => scrollParent.removeEventListener("scroll", onScroll);
225
+ }, [
226
+ applyThemeVars,
227
+ editor,
228
+ portalContainer,
229
+ selectionSnapshot,
230
+ state,
231
+ visible
232
+ ]);
233
+ const handleFormat = useCallback((format) => {
234
+ editor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
235
+ }, [editor]);
236
+ const handleLink = useCallback(() => {
237
+ editor.update(() => {
238
+ const selection = $getSelection();
239
+ if (!$isRangeSelection(selection)) return;
240
+ const nodes = selection.getNodes();
241
+ if (nodes.some((node) => {
242
+ return isEffectiveLinkNode(node.getParent()) || isEffectiveLinkNode(node);
243
+ })) {
244
+ for (const autoLinkNode of collectSelectedActiveAutoLinkNodes(selection)) {
245
+ autoLinkNode.setIsUnlinked(true);
246
+ autoLinkNode.markDirty();
247
+ }
248
+ if (nodes.some((node) => {
249
+ return isRegularLinkNode(node.getParent()) || isRegularLinkNode(node);
250
+ })) editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
251
+ } else {
252
+ const text = selection.getTextContent();
253
+ const url = /^https?:\/\//.test(text) ? text : "";
254
+ editor.dispatchCommand(TOGGLE_LINK_COMMAND, url || "https://");
255
+ }
256
+ });
257
+ }, [editor]);
258
+ const handleColor = useCallback((value) => {
259
+ editor.update(() => {
260
+ const sel = $getSelection();
261
+ if (!$isRangeSelection(sel)) return;
262
+ const color = value === "inherit" ? null : value;
263
+ $patchStyleText(sel, { color });
264
+ for (const node of sel.getNodes()) if ($isKaTeXInlineNode(node)) node.setColor(color);
265
+ });
266
+ }, [editor]);
267
+ const handleRuby = useCallback(() => {
268
+ editor.update(() => {
269
+ const selection = $getSelection();
270
+ if (!$isRangeSelection(selection)) return;
271
+ const rubyNodes = getSelectedRubyNodes(selection.getNodes());
272
+ if (rubyNodes.length > 0) {
273
+ const rubyNode = rubyNodes[0];
274
+ if (!rubyNode) return;
275
+ setRubyEdit({
276
+ nodeKey: rubyNode.getKey(),
277
+ reading: rubyNode.getReading(),
278
+ baseText: rubyNode.getTextContent(),
279
+ isNew: false
280
+ });
281
+ } else {
282
+ const text = selection.getTextContent();
283
+ if (!text.trim()) return;
284
+ selection.removeText();
285
+ const rubyNode = $createRubyNode("");
286
+ rubyNode.append($createTextNode(text));
287
+ const freshSelection = $getSelection();
288
+ if ($isRangeSelection(freshSelection)) freshSelection.insertNodes([rubyNode]);
289
+ setRubyEdit({
290
+ nodeKey: rubyNode.getKey(),
291
+ reading: "",
292
+ baseText: text,
293
+ isNew: true
294
+ });
295
+ }
296
+ });
297
+ }, [editor]);
298
+ const handleRubyConfirm = useCallback(() => {
299
+ const edit = rubyEditRef.current;
300
+ if (!edit) return;
301
+ editor.update(() => {
302
+ const node = $getNodeByKey(edit.nodeKey);
303
+ if ($isRubyNode(node)) node.setReading(edit.reading);
304
+ });
305
+ setRubyEdit(null);
306
+ }, [editor]);
307
+ const handleRubyCancel = useCallback(() => {
308
+ const edit = rubyEditRef.current;
309
+ if (!edit) return;
310
+ if (edit.isNew) editor.update(() => {
311
+ const node = $getNodeByKey(edit.nodeKey);
312
+ if ($isRubyNode(node)) {
313
+ const children = node.getChildren();
314
+ for (const child of children) node.insertBefore(child);
315
+ node.remove();
316
+ }
317
+ });
318
+ setRubyEdit(null);
319
+ }, [editor]);
320
+ const handleRubyDelete = useCallback(() => {
321
+ const edit = rubyEditRef.current;
322
+ if (!edit) return;
323
+ editor.update(() => {
324
+ const node = $getNodeByKey(edit.nodeKey);
325
+ if ($isRubyNode(node)) {
326
+ const children = node.getChildren();
327
+ for (const child of children) node.insertBefore(child);
328
+ node.remove();
329
+ }
330
+ });
331
+ setRubyEdit(null);
332
+ }, [editor]);
333
+ const isRubyEditing = rubyEdit !== null;
334
+ useEffect(() => {
335
+ if (!isRubyEditing) return;
336
+ const handleClickOutside = (e) => {
337
+ const currentEdit = rubyEditRef.current;
338
+ if (!currentEdit) return;
339
+ if (rubyEditorRef.current && !rubyEditorRef.current.contains(e.target)) {
340
+ if (currentEdit.reading.trim()) editor.update(() => {
341
+ const node = $getNodeByKey(currentEdit.nodeKey);
342
+ if ($isRubyNode(node)) node.setReading(currentEdit.reading);
343
+ });
344
+ else if (currentEdit.isNew) editor.update(() => {
345
+ const node = $getNodeByKey(currentEdit.nodeKey);
346
+ if ($isRubyNode(node)) {
347
+ const children = node.getChildren();
348
+ for (const child of children) node.insertBefore(child);
349
+ node.remove();
350
+ }
351
+ });
352
+ setRubyEdit(null);
353
+ }
354
+ };
355
+ const timer = setTimeout(() => {
356
+ document.addEventListener("mousedown", handleClickOutside);
357
+ }, 0);
358
+ return () => {
359
+ clearTimeout(timer);
360
+ document.removeEventListener("mousedown", handleClickOutside);
361
+ };
362
+ }, [editor, isRubyEditing]);
363
+ useEffect(() => {
364
+ if (!rubyEdit || !rubyEditorRef.current) return;
365
+ const positionEditor = () => {
366
+ const editorEl = rubyEditorRef.current;
367
+ if (!editorEl) return;
368
+ applyThemeVars(editorEl);
369
+ const rubyDom = editor.getElementByKey(rubyEdit.nodeKey);
370
+ if (!rubyDom) return;
371
+ const rect = rubyDom.getBoundingClientRect();
372
+ const containerRect = portalContainer === document.body ? void 0 : portalContainer.getBoundingClientRect();
373
+ const oX = containerRect?.left ?? 0;
374
+ const oY = containerRect?.top ?? 0;
375
+ const availW = containerRect?.width ?? window.innerWidth;
376
+ const editorWidth = editorEl.offsetWidth;
377
+ const rawLeft = rect.left - oX + rect.width / 2 - editorWidth / 2;
378
+ const clampedLeft = Math.max(8, Math.min(rawLeft, availW - editorWidth - 8));
379
+ editorEl.style.top = `${rect.bottom - oY + 8}px`;
380
+ editorEl.style.left = `${clampedLeft}px`;
381
+ };
382
+ requestAnimationFrame(positionEditor);
383
+ requestAnimationFrame(() => {
384
+ rubyInputRef.current?.focus();
385
+ });
386
+ }, [
387
+ applyThemeVars,
388
+ editor,
389
+ rubyEdit
390
+ ]);
391
+ if (!visible && !rubyEdit) return null;
392
+ const toolbarClassName = portalClassName ? `${toolbar} ${portalClassName}` : toolbar;
393
+ const rubyEditorClassName = portalClassName ? `${rubyEditor} ${portalClassName}` : rubyEditor;
394
+ return /* @__PURE__ */ jsxs(Fragment, { children: [visible && !rubyEdit && createPortal(/* @__PURE__ */ jsxs("div", {
395
+ "aria-label": "Text formatting",
396
+ className: toolbarClassName,
397
+ ref: toolbarRef,
398
+ role: "toolbar",
399
+ style: {
400
+ position: "fixed",
401
+ zIndex: 50
402
+ },
403
+ children: [
404
+ /* @__PURE__ */ jsx(ToolbarButton, {
405
+ active: state.isBold,
406
+ ariaLabel: "Bold",
407
+ onClick: () => handleFormat("bold"),
408
+ children: /* @__PURE__ */ jsx(Bold, {
409
+ size: ICON_SIZE,
410
+ strokeWidth: ICON_STROKE
411
+ })
412
+ }),
413
+ /* @__PURE__ */ jsx(ToolbarButton, {
414
+ active: state.isItalic,
415
+ ariaLabel: "Italic",
416
+ onClick: () => handleFormat("italic"),
417
+ children: /* @__PURE__ */ jsx(Italic, {
418
+ size: ICON_SIZE,
419
+ strokeWidth: ICON_STROKE
420
+ })
421
+ }),
422
+ /* @__PURE__ */ jsx(ToolbarButton, {
423
+ active: state.isUnderline,
424
+ ariaLabel: "Underline",
425
+ onClick: () => handleFormat("underline"),
426
+ children: /* @__PURE__ */ jsx(Underline, {
427
+ size: ICON_SIZE,
428
+ strokeWidth: ICON_STROKE
429
+ })
430
+ }),
431
+ /* @__PURE__ */ jsx(ToolbarButton, {
432
+ active: state.isStrikethrough,
433
+ ariaLabel: "Strikethrough",
434
+ onClick: () => handleFormat("strikethrough"),
435
+ children: /* @__PURE__ */ jsx(Strikethrough, {
436
+ size: ICON_SIZE,
437
+ strokeWidth: ICON_STROKE
438
+ })
439
+ }),
440
+ /* @__PURE__ */ jsx(ToolbarButton, {
441
+ active: state.isSuperscript,
442
+ ariaLabel: "Superscript",
443
+ onClick: () => handleFormat("superscript"),
444
+ children: /* @__PURE__ */ jsx(Superscript, {
445
+ size: ICON_SIZE,
446
+ strokeWidth: ICON_STROKE
447
+ })
448
+ }),
449
+ /* @__PURE__ */ jsx(ToolbarButton, {
450
+ active: state.isSubscript,
451
+ ariaLabel: "Subscript",
452
+ onClick: () => handleFormat("subscript"),
453
+ children: /* @__PURE__ */ jsx(Subscript, {
454
+ size: ICON_SIZE,
455
+ strokeWidth: ICON_STROKE
456
+ })
457
+ }),
458
+ /* @__PURE__ */ jsx("span", { className: "_1m6axz7d" }),
459
+ /* @__PURE__ */ jsx(ToolbarButton, {
460
+ active: state.isCode,
461
+ ariaLabel: "Code",
462
+ onClick: () => handleFormat("code"),
463
+ children: /* @__PURE__ */ jsx(Code, {
464
+ size: ICON_SIZE,
465
+ strokeWidth: ICON_STROKE
466
+ })
467
+ }),
468
+ /* @__PURE__ */ jsx(ToolbarButton, {
469
+ active: state.isHighlight,
470
+ ariaLabel: "Highlight",
471
+ onClick: () => handleFormat("highlight"),
472
+ children: /* @__PURE__ */ jsx(Highlighter, {
473
+ size: ICON_SIZE,
474
+ strokeWidth: ICON_STROKE
475
+ })
476
+ }),
477
+ /* @__PURE__ */ jsx(ToolbarButton, {
478
+ active: state.isSpoiler,
479
+ ariaLabel: "Spoiler",
480
+ onClick: () => editor.update($toggleSpoilerSelection),
481
+ children: /* @__PURE__ */ jsx(EyeOff, {
482
+ size: ICON_SIZE,
483
+ strokeWidth: ICON_STROKE
484
+ })
485
+ }),
486
+ /* @__PURE__ */ jsx(ToolbarButton, {
487
+ active: state.isLink,
488
+ ariaLabel: "Link",
489
+ onClick: handleLink,
490
+ children: /* @__PURE__ */ jsx(Link, {
491
+ size: ICON_SIZE,
492
+ strokeWidth: ICON_STROKE
493
+ })
494
+ }),
495
+ /* @__PURE__ */ jsx(ToolbarButton, {
496
+ active: state.isRuby,
497
+ ariaLabel: "Ruby annotation",
498
+ onClick: handleRuby,
499
+ children: /* @__PURE__ */ jsx(Languages, {
500
+ size: ICON_SIZE,
501
+ strokeWidth: ICON_STROKE
502
+ })
503
+ }),
504
+ /* @__PURE__ */ jsx("span", { className: "_1m6axz7d" }),
505
+ /* @__PURE__ */ jsx(ColorPicker, {
506
+ currentColor: state.fontColor || "inherit",
507
+ onSelect: handleColor
508
+ }),
509
+ actions && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", { className: "_1m6axz7d" }), actions] })
510
+ ]
511
+ }), portalContainer), rubyEdit && createPortal(/* @__PURE__ */ jsxs("div", {
512
+ className: rubyEditorClassName,
513
+ ref: rubyEditorRef,
514
+ style: {
515
+ position: "fixed",
516
+ zIndex: 51
517
+ },
518
+ children: [
519
+ /* @__PURE__ */ jsxs("div", {
520
+ className: "_1m6axz76",
521
+ children: [/* @__PURE__ */ jsx("span", {
522
+ className: "_1m6axz77",
523
+ children: rubyEdit.reading || "\xA0"
524
+ }), /* @__PURE__ */ jsx("span", {
525
+ className: "_1m6axz78",
526
+ children: rubyEdit.baseText
527
+ })]
528
+ }),
529
+ /* @__PURE__ */ jsxs("div", {
530
+ className: "_1m6axz79",
531
+ children: [
532
+ /* @__PURE__ */ jsx("input", {
533
+ className: "_1m6axz7a",
534
+ placeholder: "读音",
535
+ ref: rubyInputRef,
536
+ value: rubyEdit.reading,
537
+ onChange: (e) => setRubyEdit((prev) => prev ? {
538
+ ...prev,
539
+ reading: e.target.value
540
+ } : null),
541
+ onKeyDown: (e) => {
542
+ if (e.key === "Enter") {
543
+ e.preventDefault();
544
+ handleRubyConfirm();
545
+ }
546
+ if (e.key === "Escape") {
547
+ e.preventDefault();
548
+ handleRubyCancel();
549
+ }
550
+ }
551
+ }),
552
+ /* @__PURE__ */ jsx("button", {
553
+ "aria-label": "Confirm",
554
+ className: "_1m6axz7b",
555
+ style: { color: "#22c55e" },
556
+ type: "button",
557
+ onMouseDown: (e) => {
558
+ e.preventDefault();
559
+ handleRubyConfirm();
560
+ },
561
+ children: /* @__PURE__ */ jsx(Check, {
562
+ size: 14,
563
+ strokeWidth: ICON_STROKE
564
+ })
565
+ }),
566
+ /* @__PURE__ */ jsx("button", {
567
+ "aria-label": "Cancel",
568
+ className: "_1m6axz7b",
569
+ type: "button",
570
+ onMouseDown: (e) => {
571
+ e.preventDefault();
572
+ handleRubyCancel();
573
+ },
574
+ children: /* @__PURE__ */ jsx(X, {
575
+ size: 14,
576
+ strokeWidth: ICON_STROKE
577
+ })
578
+ }),
579
+ /* @__PURE__ */ jsx("span", { className: "_1m6axz7d" }),
580
+ /* @__PURE__ */ jsx("button", {
581
+ "aria-label": "Delete ruby",
582
+ className: "_1m6axz7b",
583
+ style: { color: "#ef4444" },
584
+ type: "button",
585
+ onMouseDown: (e) => {
586
+ e.preventDefault();
587
+ handleRubyDelete();
588
+ },
589
+ children: /* @__PURE__ */ jsx(Trash2, {
590
+ size: 14,
591
+ strokeWidth: ICON_STROKE
592
+ })
593
+ })
594
+ ]
595
+ }),
596
+ /* @__PURE__ */ jsx("span", {
597
+ className: "_1m6axz7c",
598
+ children: "Enter 保存 / Esc 取消"
599
+ })
600
+ ]
601
+ }), portalContainer)] });
646
602
  }
647
- export {
648
- FloatingToolbarPlugin
649
- };
603
+ //#endregion
604
+ export { FloatingToolbarPlugin };