@thangph2146/lexical-editor 0.0.1

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.
Files changed (183) hide show
  1. package/dist/editor-x/editor.cjs +33121 -0
  2. package/dist/editor-x/editor.cjs.map +1 -0
  3. package/dist/editor-x/editor.css +2854 -0
  4. package/dist/editor-x/editor.css.map +1 -0
  5. package/dist/editor-x/editor.d.cts +12 -0
  6. package/dist/editor-x/editor.d.ts +12 -0
  7. package/dist/editor-x/editor.js +33095 -0
  8. package/dist/editor-x/editor.js.map +1 -0
  9. package/dist/index.cjs +33210 -0
  10. package/dist/index.cjs.map +1 -0
  11. package/dist/index.css +2854 -0
  12. package/dist/index.css.map +1 -0
  13. package/dist/index.d.cts +15 -0
  14. package/dist/index.d.ts +15 -0
  15. package/dist/index.js +33183 -0
  16. package/dist/index.js.map +1 -0
  17. package/package.json +84 -0
  18. package/src/components/lexical-editor.tsx +123 -0
  19. package/src/context/editor-container-context.tsx +29 -0
  20. package/src/context/priority-image-context.tsx +7 -0
  21. package/src/context/toolbar-context.tsx +60 -0
  22. package/src/context/uploads-context.tsx +53 -0
  23. package/src/editor-hooks/use-debounce.ts +80 -0
  24. package/src/editor-hooks/use-modal.tsx +64 -0
  25. package/src/editor-hooks/use-report.ts +57 -0
  26. package/src/editor-hooks/use-update-toolbar.ts +41 -0
  27. package/src/editor-ui/broken-image.tsx +18 -0
  28. package/src/editor-ui/caption-composer.tsx +45 -0
  29. package/src/editor-ui/code-button.tsx +75 -0
  30. package/src/editor-ui/color-picker.tsx +2010 -0
  31. package/src/editor-ui/content-editable.tsx +37 -0
  32. package/src/editor-ui/hooks/use-image-caption-controls.ts +118 -0
  33. package/src/editor-ui/hooks/use-image-node-interactions.ts +245 -0
  34. package/src/editor-ui/hooks/use-responsive-image-dimensions.ts +202 -0
  35. package/src/editor-ui/image-component.tsx +321 -0
  36. package/src/editor-ui/image-placeholder.tsx +57 -0
  37. package/src/editor-ui/image-resizer.tsx +499 -0
  38. package/src/editor-ui/image-sizing.ts +120 -0
  39. package/src/editor-ui/lazy-image.tsx +136 -0
  40. package/src/editor-x/editor.tsx +117 -0
  41. package/src/editor-x/nodes.ts +79 -0
  42. package/src/editor-x/plugins.tsx +380 -0
  43. package/src/hooks/use-click-outside.ts +27 -0
  44. package/src/hooks/use-element-size.ts +54 -0
  45. package/src/hooks/use-header-height.ts +95 -0
  46. package/src/hooks/use-isomorphic-layout-effect.ts +4 -0
  47. package/src/index.ts +4 -0
  48. package/src/lib/logger.ts +6 -0
  49. package/src/lib/utils.ts +19 -0
  50. package/src/nodes/autocomplete-node.tsx +94 -0
  51. package/src/nodes/embeds/tweet-node.tsx +224 -0
  52. package/src/nodes/embeds/youtube-node.tsx +519 -0
  53. package/src/nodes/emoji-node.tsx +83 -0
  54. package/src/nodes/image-node.tsx +328 -0
  55. package/src/nodes/keyword-node.tsx +58 -0
  56. package/src/nodes/layout-container-node.tsx +128 -0
  57. package/src/nodes/layout-item-node.tsx +118 -0
  58. package/src/nodes/list-with-color-node.tsx +160 -0
  59. package/src/nodes/mention-node.ts +122 -0
  60. package/src/plugins/actions/actions-plugin.tsx +3 -0
  61. package/src/plugins/actions/character-limit-plugin.tsx +27 -0
  62. package/src/plugins/actions/clear-editor-plugin.tsx +70 -0
  63. package/src/plugins/actions/counter-character-plugin.tsx +80 -0
  64. package/src/plugins/actions/edit-mode-toggle-plugin.tsx +49 -0
  65. package/src/plugins/actions/import-export-plugin.tsx +61 -0
  66. package/src/plugins/actions/markdown-toggle-plugin.tsx +78 -0
  67. package/src/plugins/actions/max-length-plugin.tsx +59 -0
  68. package/src/plugins/actions/share-content-plugin.tsx +72 -0
  69. package/src/plugins/actions/speech-to-text-plugin.tsx +159 -0
  70. package/src/plugins/actions/tree-view-plugin.tsx +63 -0
  71. package/src/plugins/align-plugin.tsx +86 -0
  72. package/src/plugins/auto-link-plugin.tsx +34 -0
  73. package/src/plugins/autocomplete-plugin.tsx +2574 -0
  74. package/src/plugins/code-action-menu-plugin.tsx +240 -0
  75. package/src/plugins/code-highlight-plugin.tsx +22 -0
  76. package/src/plugins/component-picker-menu-plugin.tsx +427 -0
  77. package/src/plugins/context-menu-plugin.tsx +311 -0
  78. package/src/plugins/drag-drop-paste-plugin.tsx +52 -0
  79. package/src/plugins/draggable-block-plugin.tsx +50 -0
  80. package/src/plugins/embeds/auto-embed-plugin.tsx +324 -0
  81. package/src/plugins/embeds/twitter-plugin.tsx +45 -0
  82. package/src/plugins/embeds/youtube-plugin.tsx +84 -0
  83. package/src/plugins/emoji-picker-plugin.tsx +206 -0
  84. package/src/plugins/emojis-plugin.tsx +84 -0
  85. package/src/plugins/floating-link-editor-plugin.tsx +791 -0
  86. package/src/plugins/floating-text-format-plugin.tsx +710 -0
  87. package/src/plugins/images-plugin.tsx +671 -0
  88. package/src/plugins/keywords-plugin.tsx +59 -0
  89. package/src/plugins/layout-plugin.tsx +658 -0
  90. package/src/plugins/link-plugin.tsx +18 -0
  91. package/src/plugins/list-color-plugin.tsx +178 -0
  92. package/src/plugins/list-max-indent-level-plugin.tsx +85 -0
  93. package/src/plugins/mentions-plugin.tsx +714 -0
  94. package/src/plugins/picker/alignment-picker-plugin.tsx +40 -0
  95. package/src/plugins/picker/bulleted-list-picker-plugin.tsx +14 -0
  96. package/src/plugins/picker/check-list-picker-plugin.tsx +14 -0
  97. package/src/plugins/picker/code-picker-plugin.tsx +30 -0
  98. package/src/plugins/picker/columns-layout-picker-plugin.tsx +16 -0
  99. package/src/plugins/picker/component-picker-option.tsx +47 -0
  100. package/src/plugins/picker/divider-picker-plugin.tsx +14 -0
  101. package/src/plugins/picker/embeds-picker-plugin.tsx +24 -0
  102. package/src/plugins/picker/heading-picker-plugin.tsx +32 -0
  103. package/src/plugins/picker/image-picker-plugin.tsx +16 -0
  104. package/src/plugins/picker/numbered-list-picker-plugin.tsx +14 -0
  105. package/src/plugins/picker/paragraph-picker-plugin.tsx +20 -0
  106. package/src/plugins/picker/quote-picker-plugin.tsx +21 -0
  107. package/src/plugins/picker/table-picker-plugin.tsx +56 -0
  108. package/src/plugins/tab-focus-plugin.tsx +66 -0
  109. package/src/plugins/table-column-resizer-plugin.tsx +309 -0
  110. package/src/plugins/table-plugin.tsx +299 -0
  111. package/src/plugins/toolbar/block-format/block-format-data.tsx +69 -0
  112. package/src/plugins/toolbar/block-format/format-bulleted-list.tsx +40 -0
  113. package/src/plugins/toolbar/block-format/format-check-list.tsx +40 -0
  114. package/src/plugins/toolbar/block-format/format-code-block.tsx +45 -0
  115. package/src/plugins/toolbar/block-format/format-heading.tsx +34 -0
  116. package/src/plugins/toolbar/block-format/format-list-with-marker.tsx +74 -0
  117. package/src/plugins/toolbar/block-format/format-numbered-list.tsx +40 -0
  118. package/src/plugins/toolbar/block-format/format-paragraph.tsx +31 -0
  119. package/src/plugins/toolbar/block-format/format-quote.tsx +32 -0
  120. package/src/plugins/toolbar/block-format-toolbar-plugin.tsx +117 -0
  121. package/src/plugins/toolbar/block-insert/insert-columns-layout.tsx +32 -0
  122. package/src/plugins/toolbar/block-insert/insert-embeds.tsx +31 -0
  123. package/src/plugins/toolbar/block-insert/insert-horizontal-rule.tsx +30 -0
  124. package/src/plugins/toolbar/block-insert/insert-image.tsx +32 -0
  125. package/src/plugins/toolbar/block-insert/insert-table.tsx +32 -0
  126. package/src/plugins/toolbar/block-insert-plugin.tsx +30 -0
  127. package/src/plugins/toolbar/clear-formatting-toolbar-plugin.tsx +92 -0
  128. package/src/plugins/toolbar/code-language-toolbar-plugin.tsx +121 -0
  129. package/src/plugins/toolbar/element-format-toolbar-plugin.tsx +251 -0
  130. package/src/plugins/toolbar/font-background-toolbar-plugin.tsx +179 -0
  131. package/src/plugins/toolbar/font-color-toolbar-plugin.tsx +101 -0
  132. package/src/plugins/toolbar/font-family-toolbar-plugin.tsx +91 -0
  133. package/src/plugins/toolbar/font-format-toolbar-plugin.tsx +85 -0
  134. package/src/plugins/toolbar/font-size-toolbar-plugin.tsx +177 -0
  135. package/src/plugins/toolbar/history-toolbar-plugin.tsx +87 -0
  136. package/src/plugins/toolbar/link-toolbar-plugin.tsx +90 -0
  137. package/src/plugins/toolbar/subsuper-toolbar-plugin.tsx +69 -0
  138. package/src/plugins/toolbar/toolbar-plugin.tsx +66 -0
  139. package/src/plugins/typing-pref-plugin.tsx +118 -0
  140. package/src/shared/can-use-dom.ts +4 -0
  141. package/src/shared/environment.ts +47 -0
  142. package/src/shared/invariant.ts +16 -0
  143. package/src/shared/use-layout-effect.ts +12 -0
  144. package/src/themes/_mixins.scss +107 -0
  145. package/src/themes/_variables.scss +33 -0
  146. package/src/themes/editor-theme.scss +622 -0
  147. package/src/themes/editor-theme.ts +118 -0
  148. package/src/themes/plugins.scss +1180 -0
  149. package/src/themes/ui-components.scss +936 -0
  150. package/src/transformers/markdown-emoji-transformer.ts +20 -0
  151. package/src/transformers/markdown-hr-transformer.ts +28 -0
  152. package/src/transformers/markdown-image-transformer.ts +31 -0
  153. package/src/transformers/markdown-list-transformer.ts +51 -0
  154. package/src/transformers/markdown-table-transformer.ts +200 -0
  155. package/src/transformers/markdown-tweet-transformer.ts +26 -0
  156. package/src/ui/button-group.tsx +10 -0
  157. package/src/ui/button.tsx +29 -0
  158. package/src/ui/collapsible.tsx +67 -0
  159. package/src/ui/command.tsx +48 -0
  160. package/src/ui/dialog.tsx +146 -0
  161. package/src/ui/flex.tsx +38 -0
  162. package/src/ui/input.tsx +20 -0
  163. package/src/ui/label.tsx +20 -0
  164. package/src/ui/popover.tsx +128 -0
  165. package/src/ui/scroll-area.tsx +17 -0
  166. package/src/ui/select.tsx +171 -0
  167. package/src/ui/separator.tsx +20 -0
  168. package/src/ui/slider.tsx +14 -0
  169. package/src/ui/slot.tsx +3 -0
  170. package/src/ui/tabs.tsx +87 -0
  171. package/src/ui/toggle-group.tsx +109 -0
  172. package/src/ui/toggle.tsx +28 -0
  173. package/src/ui/tooltip.tsx +28 -0
  174. package/src/ui/typography.tsx +44 -0
  175. package/src/utils/doc-serialization.ts +68 -0
  176. package/src/utils/emoji-list.ts +16604 -0
  177. package/src/utils/get-dom-range-rect.ts +20 -0
  178. package/src/utils/get-selected-node.ts +20 -0
  179. package/src/utils/is-mobile-width.ts +0 -0
  180. package/src/utils/set-floating-elem-position-for-link-editor.ts +39 -0
  181. package/src/utils/set-floating-elem-position.ts +44 -0
  182. package/src/utils/swipe.ts +119 -0
  183. package/src/utils/url.ts +32 -0
@@ -0,0 +1,710 @@
1
+ "use client"
2
+
3
+ /**
4
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
5
+ *
6
+ * This source code is licensed under the MIT license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ *
9
+ */
10
+ import { Dispatch, JSX, useCallback, useEffect, useRef, useState } from "react"
11
+ import * as React from "react"
12
+ import { $isCodeHighlightNode } from "@lexical/code"
13
+ import { $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link"
14
+ import {
15
+ $getSelectionStyleValueForProperty,
16
+ $patchStyleText,
17
+ } from "@lexical/selection"
18
+ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"
19
+ import { mergeRegister } from "@lexical/utils"
20
+ import {
21
+ $getSelection,
22
+ $isParagraphNode,
23
+ $isRangeSelection,
24
+ $isTextNode,
25
+ COMMAND_PRIORITY_LOW,
26
+ FORMAT_TEXT_COMMAND,
27
+ LexicalEditor,
28
+ SELECTION_CHANGE_COMMAND,
29
+ } from "lexical"
30
+ import {
31
+ BoldIcon,
32
+ CodeIcon,
33
+ ItalicIcon,
34
+ LinkIcon,
35
+ PaintBucketIcon,
36
+ StrikethroughIcon,
37
+ SubscriptIcon,
38
+ SuperscriptIcon,
39
+ UnderlineIcon,
40
+ BaselineIcon,
41
+ } from "lucide-react"
42
+ import { createPortal } from "react-dom"
43
+
44
+ import {
45
+ ColorPicker,
46
+ ColorPickerAlphaSlider,
47
+ ColorPickerArea,
48
+ ColorPickerContent,
49
+ ColorPickerHueSlider,
50
+ ColorPickerInput,
51
+ ColorPickerPresets,
52
+ } from "../editor-ui/color-picker"
53
+ import { useEditorModal } from "../editor-hooks/use-modal"
54
+ import { getDOMRangeRect } from "../utils/get-dom-range-rect"
55
+ import { getSelectedNode } from "../utils/get-selected-node"
56
+ import { setFloatingElemPosition } from "../utils/set-floating-elem-position"
57
+ import { Button } from "../ui/button"
58
+ import { DialogFooter } from "../ui/dialog"
59
+ import { Flex } from "../ui/flex"
60
+ import { Separator } from "../ui/separator"
61
+ import {
62
+ ToggleGroup,
63
+ ToggleGroupItem,
64
+ } from "../ui/toggle-group"
65
+ import { IconSize } from "../ui/typography"
66
+
67
+ function FontColorModalContent({
68
+ initialColor,
69
+ onApply,
70
+ onClose,
71
+ }: {
72
+ initialColor: string
73
+ onApply: (color: string) => void
74
+ onClose: () => void
75
+ }) {
76
+ const [color, setColor] = useState(initialColor)
77
+
78
+ return (
79
+ <div className="editor-list-color-dialog">
80
+ <Flex direction="column" gap={4}>
81
+ <div className="editor-text-xs-muted">Chọn màu cho văn bản đang chọn.</div>
82
+
83
+ <ColorPicker inline value={color} onValueChange={setColor}>
84
+ <ColorPickerContent className="editor-w-full editor-border-0 editor-shadow-none editor-p-0">
85
+ <ColorPickerArea className="editor-h-40 editor-w-full editor-rounded-md" />
86
+ <Flex direction="column" gap={3} className="editor-mt-3">
87
+ <Flex direction="column" gap={2}>
88
+ <ColorPickerHueSlider className="editor-w-full" />
89
+ <ColorPickerAlphaSlider className="editor-w-full" />
90
+ <ColorPickerInput className="editor-w-full" />
91
+ </Flex>
92
+ <ColorPickerPresets />
93
+ </Flex>
94
+ </ColorPickerContent>
95
+ </ColorPicker>
96
+
97
+ <DialogFooter className="editor-px-0">
98
+ <Button
99
+ variant="outline"
100
+ size="sm"
101
+ onClick={() => {
102
+ onApply(color)
103
+ onClose()
104
+ }}
105
+ className="editor-w-full"
106
+ >
107
+ Hoàn tất
108
+ </Button>
109
+ </DialogFooter>
110
+ </Flex>
111
+ </div>
112
+ )
113
+ }
114
+
115
+ function BgColorModalContent({
116
+ initialColor,
117
+ onApply,
118
+ onClose,
119
+ }: {
120
+ initialColor: string
121
+ onApply: (color: string) => void
122
+ onClose: () => void
123
+ }) {
124
+ const [color, setColor] = useState(initialColor)
125
+
126
+ return (
127
+ <div className="editor-list-color-dialog">
128
+ <Flex direction="column" gap={4}>
129
+ <div className="editor-text-xs-muted">
130
+ Chọn màu nền cho văn bản đang chọn.
131
+ </div>
132
+
133
+ <ColorPicker inline value={color} onValueChange={setColor}>
134
+ <ColorPickerContent className="editor-w-full editor-border-0 editor-shadow-none editor-p-0">
135
+ <ColorPickerArea className="editor-h-40 editor-w-full editor-rounded-md" />
136
+ <Flex direction="column" gap={3} className="editor-mt-3">
137
+ <Flex direction="column" gap={2}>
138
+ <ColorPickerHueSlider className="editor-w-full" />
139
+ <ColorPickerAlphaSlider className="editor-w-full" />
140
+ <ColorPickerInput className="editor-w-full" />
141
+ </Flex>
142
+ <ColorPickerPresets />
143
+ </Flex>
144
+ </ColorPickerContent>
145
+ </ColorPicker>
146
+
147
+ <DialogFooter className="editor-px-0">
148
+ <Button
149
+ variant="outline"
150
+ size="sm"
151
+ onClick={() => {
152
+ onApply(color)
153
+ onClose()
154
+ }}
155
+ className="editor-w-full"
156
+ >
157
+ Hoàn tất
158
+ </Button>
159
+ </DialogFooter>
160
+ </Flex>
161
+ </div>
162
+ )
163
+ }
164
+
165
+ function FloatingTextFormat({
166
+ editor,
167
+ anchorElem,
168
+ isLink,
169
+ isBold,
170
+ isItalic,
171
+ isUnderline,
172
+ isCode,
173
+ isStrikethrough,
174
+ isSubscript,
175
+ isSuperscript,
176
+ fontColor,
177
+ bgColor,
178
+ setIsLinkEditMode,
179
+ showModal,
180
+ isModalOpen,
181
+ }: {
182
+ editor: LexicalEditor
183
+ anchorElem: HTMLElement
184
+ isBold: boolean
185
+ isCode: boolean
186
+ isItalic: boolean
187
+ isLink: boolean
188
+ isStrikethrough: boolean
189
+ isSubscript: boolean
190
+ isSuperscript: boolean
191
+ isUnderline: boolean
192
+ fontColor: string
193
+ bgColor: string
194
+ setIsLinkEditMode: Dispatch<boolean>
195
+ showModal: (
196
+ title: string,
197
+ content: (onClose: () => void) => JSX.Element
198
+ ) => void
199
+ isModalOpen: boolean
200
+ }): JSX.Element {
201
+ const popupCharStylesEditorRef = useRef<HTMLDivElement | null>(null)
202
+
203
+ const insertLink = useCallback(() => {
204
+ if (!isLink) {
205
+ setIsLinkEditMode(true)
206
+ editor.dispatchCommand(TOGGLE_LINK_COMMAND, "https://")
207
+ } else {
208
+ setIsLinkEditMode(false)
209
+ editor.dispatchCommand(TOGGLE_LINK_COMMAND, null)
210
+ }
211
+ }, [editor, isLink, setIsLinkEditMode])
212
+
213
+ const applyStyleText = useCallback(
214
+ (styles: Record<string, string>) => {
215
+ editor.update(() => {
216
+ const selection = $getSelection()
217
+ if ($isRangeSelection(selection)) {
218
+ $patchStyleText(selection, styles)
219
+ }
220
+ })
221
+ },
222
+ [editor]
223
+ )
224
+
225
+ const onFontColorSelect = useCallback(
226
+ (value: string) => {
227
+ if (value !== "inherit") {
228
+ applyStyleText({ color: value })
229
+ }
230
+ },
231
+ [applyStyleText]
232
+ )
233
+
234
+ const onBgColorSelect = useCallback(
235
+ (value: string) => {
236
+ if (value !== "inherit") {
237
+ applyStyleText({ "background-color": value })
238
+ }
239
+ },
240
+ [applyStyleText]
241
+ )
242
+
243
+ const openFontColorModal = () => {
244
+ showModal("Đổi màu chữ", (onClose) => (
245
+ <FontColorModalContent
246
+ initialColor={fontColor}
247
+ onApply={onFontColorSelect}
248
+ onClose={onClose}
249
+ />
250
+ ))
251
+ }
252
+
253
+ const openBgColorModal = () => {
254
+ showModal("Đổi màu nền", (onClose) => (
255
+ <BgColorModalContent
256
+ initialColor={bgColor}
257
+ onApply={onBgColorSelect}
258
+ onClose={onClose}
259
+ />
260
+ ))
261
+ }
262
+
263
+ useEffect(() => {
264
+ function mouseMoveListener(e: MouseEvent) {
265
+ if (
266
+ popupCharStylesEditorRef?.current &&
267
+ (e.buttons === 1 || e.buttons === 3)
268
+ ) {
269
+ if (popupCharStylesEditorRef.current.style.pointerEvents !== "none") {
270
+ const x = e.clientX
271
+ const y = e.clientY
272
+ const elementUnderMouse = document.elementFromPoint(x, y)
273
+
274
+ if (
275
+ !popupCharStylesEditorRef.current.contains(elementUnderMouse) &&
276
+ !isModalOpen
277
+ ) {
278
+ // Mouse is not over the target element => not a normal click, but probably a drag
279
+ popupCharStylesEditorRef.current.style.pointerEvents = "none"
280
+ }
281
+ }
282
+ }
283
+ }
284
+ function mouseUpListener(_e: MouseEvent) {
285
+ void _e
286
+ if (popupCharStylesEditorRef?.current) {
287
+ if (popupCharStylesEditorRef.current.style.pointerEvents !== "auto") {
288
+ popupCharStylesEditorRef.current.style.pointerEvents = "auto"
289
+ }
290
+ }
291
+ }
292
+
293
+ if (popupCharStylesEditorRef?.current) {
294
+ document.addEventListener("mousemove", mouseMoveListener)
295
+ document.addEventListener("mouseup", mouseUpListener)
296
+
297
+ return () => {
298
+ document.removeEventListener("mousemove", mouseMoveListener)
299
+ document.removeEventListener("mouseup", mouseUpListener)
300
+ }
301
+ }
302
+ }, [popupCharStylesEditorRef, isModalOpen])
303
+
304
+ const $updateTextFormatFloatingToolbar = useCallback(() => {
305
+ const selection = $getSelection()
306
+
307
+ const popupCharStylesEditorElem = popupCharStylesEditorRef.current
308
+ const nativeSelection = window.getSelection()
309
+
310
+ if (popupCharStylesEditorElem === null) {
311
+ return
312
+ }
313
+
314
+ const rootElement = editor.getRootElement()
315
+ if (
316
+ selection !== null &&
317
+ nativeSelection !== null &&
318
+ !nativeSelection.isCollapsed &&
319
+ rootElement !== null &&
320
+ rootElement.contains(nativeSelection.anchorNode)
321
+ ) {
322
+ const rangeRect = getDOMRangeRect(nativeSelection, rootElement)
323
+
324
+ setFloatingElemPosition(
325
+ rangeRect,
326
+ popupCharStylesEditorElem,
327
+ anchorElem,
328
+ isLink
329
+ )
330
+ popupCharStylesEditorElem.classList.add(
331
+ "editor-floating-text-format--visible"
332
+ )
333
+ } else {
334
+ setFloatingElemPosition(null, popupCharStylesEditorElem, anchorElem, isLink)
335
+ popupCharStylesEditorElem.classList.remove(
336
+ "editor-floating-text-format--visible"
337
+ )
338
+ }
339
+ }, [editor, anchorElem, isLink])
340
+
341
+ useEffect(() => {
342
+ const scrollerElem = anchorElem.parentElement
343
+
344
+ const update = () => {
345
+ editor.getEditorState().read(() => {
346
+ $updateTextFormatFloatingToolbar()
347
+ })
348
+ }
349
+
350
+ window.addEventListener("resize", update)
351
+ if (scrollerElem) {
352
+ scrollerElem.addEventListener("scroll", update, { passive: true })
353
+ }
354
+
355
+ return () => {
356
+ window.removeEventListener("resize", update)
357
+ if (scrollerElem) {
358
+ scrollerElem.removeEventListener("scroll", update)
359
+ }
360
+ }
361
+ }, [editor, $updateTextFormatFloatingToolbar, anchorElem])
362
+
363
+ useEffect(() => {
364
+ editor.getEditorState().read(() => {
365
+ $updateTextFormatFloatingToolbar()
366
+ })
367
+ return mergeRegister(
368
+ editor.registerUpdateListener(({ editorState }) => {
369
+ editorState.read(() => {
370
+ $updateTextFormatFloatingToolbar()
371
+ })
372
+ }),
373
+
374
+ editor.registerCommand(
375
+ SELECTION_CHANGE_COMMAND,
376
+ () => {
377
+ $updateTextFormatFloatingToolbar()
378
+ return false
379
+ },
380
+ COMMAND_PRIORITY_LOW
381
+ )
382
+ )
383
+ }, [editor, $updateTextFormatFloatingToolbar])
384
+
385
+ return (
386
+ <div
387
+ ref={popupCharStylesEditorRef}
388
+ className="editor-floating-text-format"
389
+ >
390
+ {editor.isEditable() && (
391
+ <Flex align="center" gap={1} className="editor-flex-nowrap">
392
+ <div className="editor-floating-group editor-flex editor-items-center">
393
+ <ToggleGroup
394
+ type="multiple"
395
+ className="editor-flex editor-items-center"
396
+ value={[
397
+ isBold ? "bold" : "",
398
+ isItalic ? "italic" : "",
399
+ isUnderline ? "underline" : "",
400
+ isStrikethrough ? "strikethrough" : "",
401
+ isCode ? "code" : "",
402
+ isLink ? "link" : "",
403
+ ].filter(Boolean)}
404
+ >
405
+ <ToggleGroupItem
406
+ value="bold"
407
+ aria-label="Toggle bold"
408
+ className="editor-toolbar-item"
409
+ onClick={() => {
410
+ editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold")
411
+ }}
412
+ size="sm"
413
+ >
414
+ <IconSize size="sm">
415
+ <BoldIcon />
416
+ </IconSize>
417
+ </ToggleGroupItem>
418
+ <ToggleGroupItem
419
+ value="italic"
420
+ aria-label="Toggle italic"
421
+ className="editor-toolbar-item"
422
+ onClick={() => {
423
+ editor.dispatchCommand(FORMAT_TEXT_COMMAND, "italic")
424
+ }}
425
+ size="sm"
426
+ >
427
+ <IconSize size="sm">
428
+ <ItalicIcon />
429
+ </IconSize>
430
+ </ToggleGroupItem>
431
+ <ToggleGroupItem
432
+ value="underline"
433
+ aria-label="Toggle underline"
434
+ className="editor-toolbar-item"
435
+ onClick={() => {
436
+ editor.dispatchCommand(FORMAT_TEXT_COMMAND, "underline")
437
+ }}
438
+ size="sm"
439
+ >
440
+ <IconSize size="sm">
441
+ <UnderlineIcon />
442
+ </IconSize>
443
+ </ToggleGroupItem>
444
+ <ToggleGroupItem
445
+ value="strikethrough"
446
+ aria-label="Toggle strikethrough"
447
+ className="editor-toolbar-item"
448
+ onClick={() => {
449
+ editor.dispatchCommand(FORMAT_TEXT_COMMAND, "strikethrough")
450
+ }}
451
+ size="sm"
452
+ >
453
+ <IconSize size="sm">
454
+ <StrikethroughIcon />
455
+ </IconSize>
456
+ </ToggleGroupItem>
457
+ <ToggleGroupItem
458
+ value="code"
459
+ aria-label="Toggle code"
460
+ className="editor-toolbar-item"
461
+ onClick={() => {
462
+ editor.dispatchCommand(FORMAT_TEXT_COMMAND, "code")
463
+ }}
464
+ size="sm"
465
+ >
466
+ <IconSize size="sm">
467
+ <CodeIcon />
468
+ </IconSize>
469
+ </ToggleGroupItem>
470
+ <ToggleGroupItem
471
+ value="link"
472
+ aria-label="Toggle link"
473
+ className="editor-toolbar-item"
474
+ onClick={insertLink}
475
+ size="sm"
476
+ >
477
+ <IconSize size="sm">
478
+ <LinkIcon />
479
+ </IconSize>
480
+ </ToggleGroupItem>
481
+ </ToggleGroup>
482
+ </div>
483
+
484
+ <Separator orientation="vertical" className="editor-separator--vertical" />
485
+
486
+ <div className="editor-floating-group--lg editor-flex editor-items-center">
487
+ <Button
488
+ variant="ghost"
489
+ size="sm"
490
+ className="editor-toolbar-item"
491
+ onClick={openFontColorModal}
492
+ >
493
+ <IconSize size="sm">
494
+ <BaselineIcon />
495
+ </IconSize>
496
+ </Button>
497
+
498
+ <Button
499
+ variant="ghost"
500
+ size="sm"
501
+ className="editor-toolbar-item"
502
+ onClick={openBgColorModal}
503
+ >
504
+ <IconSize size="sm">
505
+ <PaintBucketIcon />
506
+ </IconSize>
507
+ </Button>
508
+ </div>
509
+
510
+ <Separator orientation="vertical" className="editor-separator--vertical" />
511
+
512
+ <div className="editor-floating-group editor-flex editor-items-center">
513
+ <ToggleGroup
514
+ type="single"
515
+ className="editor-flex editor-items-center"
516
+ value={
517
+ isSubscript ? "subscript" : isSuperscript ? "superscript" : ""
518
+ }
519
+ >
520
+ <ToggleGroupItem
521
+ value="subscript"
522
+ aria-label="Toggle subscript"
523
+ className="editor-toolbar-item"
524
+ onClick={() => {
525
+ editor.dispatchCommand(FORMAT_TEXT_COMMAND, "subscript")
526
+ }}
527
+ size="sm"
528
+ >
529
+ <IconSize size="sm">
530
+ <SubscriptIcon />
531
+ </IconSize>
532
+ </ToggleGroupItem>
533
+ <ToggleGroupItem
534
+ value="superscript"
535
+ aria-label="Toggle superscript"
536
+ className="editor-toolbar-item"
537
+ onClick={() => {
538
+ editor.dispatchCommand(FORMAT_TEXT_COMMAND, "superscript")
539
+ }}
540
+ size="sm"
541
+ >
542
+ <IconSize size="sm">
543
+ <SuperscriptIcon />
544
+ </IconSize>
545
+ </ToggleGroupItem>
546
+ </ToggleGroup>
547
+ </div>
548
+ </Flex>
549
+ )}
550
+ </div>
551
+ )
552
+ }
553
+
554
+ function useFloatingTextFormatToolbar(
555
+ editor: LexicalEditor,
556
+ anchorElem: HTMLDivElement | null,
557
+ setIsLinkEditMode: Dispatch<boolean>,
558
+ showModal: (
559
+ title: string,
560
+ content: (onClose: () => void) => JSX.Element
561
+ ) => void,
562
+ isModalOpen: boolean
563
+ ): JSX.Element | null {
564
+ const [isText, setIsText] = useState(false)
565
+ const [isLink, setIsLink] = useState(false)
566
+ const [isBold, setIsBold] = useState(false)
567
+ const [isItalic, setIsItalic] = useState(false)
568
+ const [isUnderline, setIsUnderline] = useState(false)
569
+ const [isStrikethrough, setIsStrikethrough] = useState(false)
570
+ const [isSubscript, setIsSubscript] = useState(false)
571
+ const [isSuperscript, setIsSuperscript] = useState(false)
572
+ const [isCode, setIsCode] = useState(false)
573
+ const [fontColor, setFontColor] = useState("inherit")
574
+ const [bgColor, setBgColor] = useState("inherit")
575
+
576
+ const updatePopup = useCallback(() => {
577
+ editor.getEditorState().read(() => {
578
+ // Should not to pop up the floating toolbar when using IME input
579
+ if (editor.isComposing()) {
580
+ return
581
+ }
582
+ const selection = $getSelection()
583
+ const nativeSelection = window.getSelection()
584
+ const rootElement = editor.getRootElement()
585
+
586
+ if (
587
+ nativeSelection !== null &&
588
+ (!$isRangeSelection(selection) ||
589
+ rootElement === null ||
590
+ !rootElement.contains(nativeSelection.anchorNode))
591
+ ) {
592
+ setIsText(false)
593
+ return
594
+ }
595
+
596
+ if (!$isRangeSelection(selection)) {
597
+ return
598
+ }
599
+
600
+ const node = getSelectedNode(selection)
601
+
602
+ // Update text format
603
+ setIsBold(selection.hasFormat("bold"))
604
+ setIsItalic(selection.hasFormat("italic"))
605
+ setIsUnderline(selection.hasFormat("underline"))
606
+ setIsStrikethrough(selection.hasFormat("strikethrough"))
607
+ setIsSubscript(selection.hasFormat("subscript"))
608
+ setIsSuperscript(selection.hasFormat("superscript"))
609
+ setIsCode(selection.hasFormat("code"))
610
+ setFontColor($getSelectionStyleValueForProperty(selection, "color", "inherit"))
611
+ setBgColor(
612
+ $getSelectionStyleValueForProperty(selection, "background-color", "inherit")
613
+ )
614
+
615
+ // Update links
616
+ const parent = node.getParent()
617
+ if ($isLinkNode(parent) || $isLinkNode(node)) {
618
+ setIsLink(true)
619
+ } else {
620
+ setIsLink(false)
621
+ }
622
+
623
+ if (
624
+ !$isCodeHighlightNode(selection.anchor.getNode()) &&
625
+ selection.getTextContent() !== ""
626
+ ) {
627
+ setIsText($isTextNode(node) || $isParagraphNode(node))
628
+ } else {
629
+ setIsText(false)
630
+ }
631
+
632
+ const rawTextContent = selection.getTextContent().replace(/\n/g, "")
633
+ if (!selection.isCollapsed() && rawTextContent === "") {
634
+ setIsText(false)
635
+ return
636
+ }
637
+ })
638
+ }, [editor])
639
+
640
+ useEffect(() => {
641
+ document.addEventListener("selectionchange", updatePopup)
642
+ return () => {
643
+ document.removeEventListener("selectionchange", updatePopup)
644
+ }
645
+ }, [updatePopup])
646
+
647
+ useEffect(() => {
648
+ return mergeRegister(
649
+ editor.registerUpdateListener(() => {
650
+ updatePopup()
651
+ }),
652
+ editor.registerRootListener(() => {
653
+ if (editor.getRootElement() === null) {
654
+ setIsText(false)
655
+ }
656
+ })
657
+ )
658
+ }, [editor, updatePopup])
659
+
660
+ if (!isText || !anchorElem) {
661
+ return null
662
+ }
663
+
664
+ return createPortal(
665
+ <FloatingTextFormat
666
+ editor={editor}
667
+ anchorElem={anchorElem}
668
+ isLink={isLink}
669
+ isBold={isBold}
670
+ isItalic={isItalic}
671
+ isStrikethrough={isStrikethrough}
672
+ isSubscript={isSubscript}
673
+ isSuperscript={isSuperscript}
674
+ isUnderline={isUnderline}
675
+ isCode={isCode}
676
+ fontColor={fontColor}
677
+ bgColor={bgColor}
678
+ setIsLinkEditMode={setIsLinkEditMode}
679
+ showModal={showModal}
680
+ isModalOpen={isModalOpen}
681
+ />,
682
+ anchorElem
683
+ )
684
+ }
685
+
686
+ export function FloatingTextFormatToolbarPlugin({
687
+ anchorElem,
688
+ setIsLinkEditMode,
689
+ }: {
690
+ anchorElem: HTMLDivElement | null
691
+ setIsLinkEditMode: Dispatch<boolean>
692
+ }): JSX.Element | null {
693
+ const [editor] = useLexicalComposerContext()
694
+ const [modal, showModal] = useEditorModal()
695
+
696
+ const toolbar = useFloatingTextFormatToolbar(
697
+ editor,
698
+ anchorElem,
699
+ setIsLinkEditMode,
700
+ showModal,
701
+ modal !== null
702
+ )
703
+
704
+ return (
705
+ <>
706
+ {toolbar}
707
+ {modal}
708
+ </>
709
+ )
710
+ }