@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.
- package/dist/editor-x/editor.cjs +33121 -0
- package/dist/editor-x/editor.cjs.map +1 -0
- package/dist/editor-x/editor.css +2854 -0
- package/dist/editor-x/editor.css.map +1 -0
- package/dist/editor-x/editor.d.cts +12 -0
- package/dist/editor-x/editor.d.ts +12 -0
- package/dist/editor-x/editor.js +33095 -0
- package/dist/editor-x/editor.js.map +1 -0
- package/dist/index.cjs +33210 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.css +2854 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.cts +15 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +33183 -0
- package/dist/index.js.map +1 -0
- package/package.json +84 -0
- package/src/components/lexical-editor.tsx +123 -0
- package/src/context/editor-container-context.tsx +29 -0
- package/src/context/priority-image-context.tsx +7 -0
- package/src/context/toolbar-context.tsx +60 -0
- package/src/context/uploads-context.tsx +53 -0
- package/src/editor-hooks/use-debounce.ts +80 -0
- package/src/editor-hooks/use-modal.tsx +64 -0
- package/src/editor-hooks/use-report.ts +57 -0
- package/src/editor-hooks/use-update-toolbar.ts +41 -0
- package/src/editor-ui/broken-image.tsx +18 -0
- package/src/editor-ui/caption-composer.tsx +45 -0
- package/src/editor-ui/code-button.tsx +75 -0
- package/src/editor-ui/color-picker.tsx +2010 -0
- package/src/editor-ui/content-editable.tsx +37 -0
- package/src/editor-ui/hooks/use-image-caption-controls.ts +118 -0
- package/src/editor-ui/hooks/use-image-node-interactions.ts +245 -0
- package/src/editor-ui/hooks/use-responsive-image-dimensions.ts +202 -0
- package/src/editor-ui/image-component.tsx +321 -0
- package/src/editor-ui/image-placeholder.tsx +57 -0
- package/src/editor-ui/image-resizer.tsx +499 -0
- package/src/editor-ui/image-sizing.ts +120 -0
- package/src/editor-ui/lazy-image.tsx +136 -0
- package/src/editor-x/editor.tsx +117 -0
- package/src/editor-x/nodes.ts +79 -0
- package/src/editor-x/plugins.tsx +380 -0
- package/src/hooks/use-click-outside.ts +27 -0
- package/src/hooks/use-element-size.ts +54 -0
- package/src/hooks/use-header-height.ts +95 -0
- package/src/hooks/use-isomorphic-layout-effect.ts +4 -0
- package/src/index.ts +4 -0
- package/src/lib/logger.ts +6 -0
- package/src/lib/utils.ts +19 -0
- package/src/nodes/autocomplete-node.tsx +94 -0
- package/src/nodes/embeds/tweet-node.tsx +224 -0
- package/src/nodes/embeds/youtube-node.tsx +519 -0
- package/src/nodes/emoji-node.tsx +83 -0
- package/src/nodes/image-node.tsx +328 -0
- package/src/nodes/keyword-node.tsx +58 -0
- package/src/nodes/layout-container-node.tsx +128 -0
- package/src/nodes/layout-item-node.tsx +118 -0
- package/src/nodes/list-with-color-node.tsx +160 -0
- package/src/nodes/mention-node.ts +122 -0
- package/src/plugins/actions/actions-plugin.tsx +3 -0
- package/src/plugins/actions/character-limit-plugin.tsx +27 -0
- package/src/plugins/actions/clear-editor-plugin.tsx +70 -0
- package/src/plugins/actions/counter-character-plugin.tsx +80 -0
- package/src/plugins/actions/edit-mode-toggle-plugin.tsx +49 -0
- package/src/plugins/actions/import-export-plugin.tsx +61 -0
- package/src/plugins/actions/markdown-toggle-plugin.tsx +78 -0
- package/src/plugins/actions/max-length-plugin.tsx +59 -0
- package/src/plugins/actions/share-content-plugin.tsx +72 -0
- package/src/plugins/actions/speech-to-text-plugin.tsx +159 -0
- package/src/plugins/actions/tree-view-plugin.tsx +63 -0
- package/src/plugins/align-plugin.tsx +86 -0
- package/src/plugins/auto-link-plugin.tsx +34 -0
- package/src/plugins/autocomplete-plugin.tsx +2574 -0
- package/src/plugins/code-action-menu-plugin.tsx +240 -0
- package/src/plugins/code-highlight-plugin.tsx +22 -0
- package/src/plugins/component-picker-menu-plugin.tsx +427 -0
- package/src/plugins/context-menu-plugin.tsx +311 -0
- package/src/plugins/drag-drop-paste-plugin.tsx +52 -0
- package/src/plugins/draggable-block-plugin.tsx +50 -0
- package/src/plugins/embeds/auto-embed-plugin.tsx +324 -0
- package/src/plugins/embeds/twitter-plugin.tsx +45 -0
- package/src/plugins/embeds/youtube-plugin.tsx +84 -0
- package/src/plugins/emoji-picker-plugin.tsx +206 -0
- package/src/plugins/emojis-plugin.tsx +84 -0
- package/src/plugins/floating-link-editor-plugin.tsx +791 -0
- package/src/plugins/floating-text-format-plugin.tsx +710 -0
- package/src/plugins/images-plugin.tsx +671 -0
- package/src/plugins/keywords-plugin.tsx +59 -0
- package/src/plugins/layout-plugin.tsx +658 -0
- package/src/plugins/link-plugin.tsx +18 -0
- package/src/plugins/list-color-plugin.tsx +178 -0
- package/src/plugins/list-max-indent-level-plugin.tsx +85 -0
- package/src/plugins/mentions-plugin.tsx +714 -0
- package/src/plugins/picker/alignment-picker-plugin.tsx +40 -0
- package/src/plugins/picker/bulleted-list-picker-plugin.tsx +14 -0
- package/src/plugins/picker/check-list-picker-plugin.tsx +14 -0
- package/src/plugins/picker/code-picker-plugin.tsx +30 -0
- package/src/plugins/picker/columns-layout-picker-plugin.tsx +16 -0
- package/src/plugins/picker/component-picker-option.tsx +47 -0
- package/src/plugins/picker/divider-picker-plugin.tsx +14 -0
- package/src/plugins/picker/embeds-picker-plugin.tsx +24 -0
- package/src/plugins/picker/heading-picker-plugin.tsx +32 -0
- package/src/plugins/picker/image-picker-plugin.tsx +16 -0
- package/src/plugins/picker/numbered-list-picker-plugin.tsx +14 -0
- package/src/plugins/picker/paragraph-picker-plugin.tsx +20 -0
- package/src/plugins/picker/quote-picker-plugin.tsx +21 -0
- package/src/plugins/picker/table-picker-plugin.tsx +56 -0
- package/src/plugins/tab-focus-plugin.tsx +66 -0
- package/src/plugins/table-column-resizer-plugin.tsx +309 -0
- package/src/plugins/table-plugin.tsx +299 -0
- package/src/plugins/toolbar/block-format/block-format-data.tsx +69 -0
- package/src/plugins/toolbar/block-format/format-bulleted-list.tsx +40 -0
- package/src/plugins/toolbar/block-format/format-check-list.tsx +40 -0
- package/src/plugins/toolbar/block-format/format-code-block.tsx +45 -0
- package/src/plugins/toolbar/block-format/format-heading.tsx +34 -0
- package/src/plugins/toolbar/block-format/format-list-with-marker.tsx +74 -0
- package/src/plugins/toolbar/block-format/format-numbered-list.tsx +40 -0
- package/src/plugins/toolbar/block-format/format-paragraph.tsx +31 -0
- package/src/plugins/toolbar/block-format/format-quote.tsx +32 -0
- package/src/plugins/toolbar/block-format-toolbar-plugin.tsx +117 -0
- package/src/plugins/toolbar/block-insert/insert-columns-layout.tsx +32 -0
- package/src/plugins/toolbar/block-insert/insert-embeds.tsx +31 -0
- package/src/plugins/toolbar/block-insert/insert-horizontal-rule.tsx +30 -0
- package/src/plugins/toolbar/block-insert/insert-image.tsx +32 -0
- package/src/plugins/toolbar/block-insert/insert-table.tsx +32 -0
- package/src/plugins/toolbar/block-insert-plugin.tsx +30 -0
- package/src/plugins/toolbar/clear-formatting-toolbar-plugin.tsx +92 -0
- package/src/plugins/toolbar/code-language-toolbar-plugin.tsx +121 -0
- package/src/plugins/toolbar/element-format-toolbar-plugin.tsx +251 -0
- package/src/plugins/toolbar/font-background-toolbar-plugin.tsx +179 -0
- package/src/plugins/toolbar/font-color-toolbar-plugin.tsx +101 -0
- package/src/plugins/toolbar/font-family-toolbar-plugin.tsx +91 -0
- package/src/plugins/toolbar/font-format-toolbar-plugin.tsx +85 -0
- package/src/plugins/toolbar/font-size-toolbar-plugin.tsx +177 -0
- package/src/plugins/toolbar/history-toolbar-plugin.tsx +87 -0
- package/src/plugins/toolbar/link-toolbar-plugin.tsx +90 -0
- package/src/plugins/toolbar/subsuper-toolbar-plugin.tsx +69 -0
- package/src/plugins/toolbar/toolbar-plugin.tsx +66 -0
- package/src/plugins/typing-pref-plugin.tsx +118 -0
- package/src/shared/can-use-dom.ts +4 -0
- package/src/shared/environment.ts +47 -0
- package/src/shared/invariant.ts +16 -0
- package/src/shared/use-layout-effect.ts +12 -0
- package/src/themes/_mixins.scss +107 -0
- package/src/themes/_variables.scss +33 -0
- package/src/themes/editor-theme.scss +622 -0
- package/src/themes/editor-theme.ts +118 -0
- package/src/themes/plugins.scss +1180 -0
- package/src/themes/ui-components.scss +936 -0
- package/src/transformers/markdown-emoji-transformer.ts +20 -0
- package/src/transformers/markdown-hr-transformer.ts +28 -0
- package/src/transformers/markdown-image-transformer.ts +31 -0
- package/src/transformers/markdown-list-transformer.ts +51 -0
- package/src/transformers/markdown-table-transformer.ts +200 -0
- package/src/transformers/markdown-tweet-transformer.ts +26 -0
- package/src/ui/button-group.tsx +10 -0
- package/src/ui/button.tsx +29 -0
- package/src/ui/collapsible.tsx +67 -0
- package/src/ui/command.tsx +48 -0
- package/src/ui/dialog.tsx +146 -0
- package/src/ui/flex.tsx +38 -0
- package/src/ui/input.tsx +20 -0
- package/src/ui/label.tsx +20 -0
- package/src/ui/popover.tsx +128 -0
- package/src/ui/scroll-area.tsx +17 -0
- package/src/ui/select.tsx +171 -0
- package/src/ui/separator.tsx +20 -0
- package/src/ui/slider.tsx +14 -0
- package/src/ui/slot.tsx +3 -0
- package/src/ui/tabs.tsx +87 -0
- package/src/ui/toggle-group.tsx +109 -0
- package/src/ui/toggle.tsx +28 -0
- package/src/ui/tooltip.tsx +28 -0
- package/src/ui/typography.tsx +44 -0
- package/src/utils/doc-serialization.ts +68 -0
- package/src/utils/emoji-list.ts +16604 -0
- package/src/utils/get-dom-range-rect.ts +20 -0
- package/src/utils/get-selected-node.ts +20 -0
- package/src/utils/is-mobile-width.ts +0 -0
- package/src/utils/set-floating-elem-position-for-link-editor.ts +39 -0
- package/src/utils/set-floating-elem-position.ts +44 -0
- package/src/utils/swipe.ts +119 -0
- package/src/utils/url.ts +32 -0
|
@@ -0,0 +1,791 @@
|
|
|
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 {
|
|
12
|
+
$createLinkNode,
|
|
13
|
+
$isAutoLinkNode,
|
|
14
|
+
$isLinkNode,
|
|
15
|
+
TOGGLE_LINK_COMMAND,
|
|
16
|
+
} from "@lexical/link"
|
|
17
|
+
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"
|
|
18
|
+
import { $findMatchingParent, $wrapNodeInElement, mergeRegister } from "@lexical/utils"
|
|
19
|
+
import {
|
|
20
|
+
$getSelection,
|
|
21
|
+
$isLineBreakNode,
|
|
22
|
+
$isNodeSelection,
|
|
23
|
+
$isRangeSelection,
|
|
24
|
+
$setSelection,
|
|
25
|
+
BaseSelection,
|
|
26
|
+
CLICK_COMMAND,
|
|
27
|
+
COMMAND_PRIORITY_CRITICAL,
|
|
28
|
+
COMMAND_PRIORITY_HIGH,
|
|
29
|
+
COMMAND_PRIORITY_LOW,
|
|
30
|
+
KEY_ESCAPE_COMMAND,
|
|
31
|
+
LexicalEditor,
|
|
32
|
+
SELECTION_CHANGE_COMMAND,
|
|
33
|
+
} from "lexical"
|
|
34
|
+
import { Check, Pencil, Trash, X } from "lucide-react"
|
|
35
|
+
import { createPortal } from "react-dom"
|
|
36
|
+
|
|
37
|
+
import { getSelectedNode } from "../utils/get-selected-node"
|
|
38
|
+
import { setFloatingElemPositionForLinkEditor } from "../utils/set-floating-elem-position-for-link-editor"
|
|
39
|
+
import { sanitizeUrl, validateUrl } from "../utils/url"
|
|
40
|
+
import { Button } from "../ui/button"
|
|
41
|
+
import { Input } from "../ui/input"
|
|
42
|
+
import { Flex } from "../ui/flex"
|
|
43
|
+
import { TypographyPSmall } from "../ui/typography"
|
|
44
|
+
import { $isImageNode } from "../nodes/image-node"
|
|
45
|
+
|
|
46
|
+
function FloatingLinkEditor({
|
|
47
|
+
editor,
|
|
48
|
+
isLink,
|
|
49
|
+
setIsLink,
|
|
50
|
+
anchorElem,
|
|
51
|
+
isLinkEditMode,
|
|
52
|
+
setIsLinkEditMode,
|
|
53
|
+
}: {
|
|
54
|
+
editor: LexicalEditor
|
|
55
|
+
isLink: boolean
|
|
56
|
+
setIsLink: Dispatch<boolean>
|
|
57
|
+
anchorElem: HTMLElement
|
|
58
|
+
isLinkEditMode: boolean
|
|
59
|
+
setIsLinkEditMode: Dispatch<boolean>
|
|
60
|
+
}): JSX.Element {
|
|
61
|
+
const editorRef = useRef<HTMLDivElement | null>(null)
|
|
62
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
63
|
+
const [linkUrl, setLinkUrl] = useState("")
|
|
64
|
+
const [editedLinkUrl, setEditedLinkUrl] = useState("https://")
|
|
65
|
+
const [lastSelection, setLastSelection] = useState<BaseSelection | null>(null)
|
|
66
|
+
|
|
67
|
+
const $updateLinkEditor = useCallback(() => {
|
|
68
|
+
const selection = $getSelection()
|
|
69
|
+
let linkNode = null
|
|
70
|
+
let selectedNode = null
|
|
71
|
+
|
|
72
|
+
if ($isRangeSelection(selection)) {
|
|
73
|
+
const node = getSelectedNode(selection)
|
|
74
|
+
if (node !== null) {
|
|
75
|
+
selectedNode = node
|
|
76
|
+
linkNode = $findMatchingParent(node, $isLinkNode)
|
|
77
|
+
if (!linkNode && $isLinkNode(node)) {
|
|
78
|
+
linkNode = node
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
} else if ($isNodeSelection(selection)) {
|
|
82
|
+
const nodes = selection.getNodes()
|
|
83
|
+
const node = nodes[0]
|
|
84
|
+
if (node) {
|
|
85
|
+
selectedNode = node
|
|
86
|
+
// Check if the node itself is a link node
|
|
87
|
+
if ($isLinkNode(node)) {
|
|
88
|
+
linkNode = node
|
|
89
|
+
} else {
|
|
90
|
+
// Check if the node is wrapped in a link (e.g., image node in link)
|
|
91
|
+
linkNode = $findMatchingParent(node, $isLinkNode)
|
|
92
|
+
// For image nodes, also check if parent is a link
|
|
93
|
+
if (!linkNode && $isImageNode(node)) {
|
|
94
|
+
const parent = node.getParent()
|
|
95
|
+
if ($isLinkNode(parent)) {
|
|
96
|
+
linkNode = parent
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (linkNode) {
|
|
104
|
+
setLinkUrl(linkNode.getURL())
|
|
105
|
+
} else {
|
|
106
|
+
setLinkUrl("")
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (isLinkEditMode && linkNode) {
|
|
110
|
+
setEditedLinkUrl(linkUrl || linkNode.getURL())
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const editorElem = editorRef.current
|
|
114
|
+
const nativeSelection = window.getSelection()
|
|
115
|
+
const activeElement = document.activeElement
|
|
116
|
+
|
|
117
|
+
if (editorElem === null) {
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const rootElement = editor.getRootElement()
|
|
122
|
+
|
|
123
|
+
// Check if we have a valid selection (with or without link)
|
|
124
|
+
const hasValidSelection = selection !== null
|
|
125
|
+
const hasValidNativeSelection =
|
|
126
|
+
nativeSelection !== null &&
|
|
127
|
+
rootElement !== null &&
|
|
128
|
+
(nativeSelection.anchorNode && rootElement.contains(nativeSelection.anchorNode))
|
|
129
|
+
|
|
130
|
+
// Show floating editor if:
|
|
131
|
+
// 1. We have a link node (existing link) - show to view/edit
|
|
132
|
+
// 2. We're in edit mode (creating new link) - show input to create link
|
|
133
|
+
const hasImageNode = $isNodeSelection(selection) && selectedNode && $isImageNode(selectedNode)
|
|
134
|
+
const shouldShowEditor =
|
|
135
|
+
(linkNode !== null || isLinkEditMode) &&
|
|
136
|
+
hasValidSelection &&
|
|
137
|
+
editor.isEditable() &&
|
|
138
|
+
(
|
|
139
|
+
// If in edit mode, always show (even if nativeSelection is not valid)
|
|
140
|
+
isLinkEditMode ||
|
|
141
|
+
hasValidNativeSelection ||
|
|
142
|
+
($isNodeSelection(selection) && selectedNode) ||
|
|
143
|
+
($isRangeSelection(selection) && selection.getTextContent().length > 0)
|
|
144
|
+
)
|
|
145
|
+
void hasImageNode // reserved for future: hide editor when selection is image-only
|
|
146
|
+
|
|
147
|
+
if (shouldShowEditor) {
|
|
148
|
+
// For node selection (e.g., image), try to get the DOM element
|
|
149
|
+
let domRect: DOMRect | undefined
|
|
150
|
+
|
|
151
|
+
if ($isNodeSelection(selection) && selectedNode) {
|
|
152
|
+
// Try to get DOM element using node key
|
|
153
|
+
const nodeKey = selectedNode.getKey()
|
|
154
|
+
const nodeElement = editor.getElementByKey(nodeKey)
|
|
155
|
+
|
|
156
|
+
if (nodeElement) {
|
|
157
|
+
// For image nodes wrapped in links, find the link element
|
|
158
|
+
if ($isImageNode(selectedNode) && linkNode) {
|
|
159
|
+
// Find the link element that wraps the image
|
|
160
|
+
const linkElement = nodeElement.closest("a") || nodeElement.parentElement?.closest("a")
|
|
161
|
+
if (linkElement) {
|
|
162
|
+
domRect = linkElement.getBoundingClientRect()
|
|
163
|
+
} else {
|
|
164
|
+
domRect = nodeElement.getBoundingClientRect()
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
domRect = nodeElement.getBoundingClientRect()
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Fallback to native selection if we don't have a specific DOM rect
|
|
173
|
+
if (!domRect && nativeSelection) {
|
|
174
|
+
// Try to find link element in DOM if we have a link node
|
|
175
|
+
if (linkNode && nativeSelection.focusNode) {
|
|
176
|
+
// focusNode can be a Text node, so we need to get the parent element
|
|
177
|
+
let focusElement: HTMLElement | null = null
|
|
178
|
+
if (nativeSelection.focusNode instanceof HTMLElement) {
|
|
179
|
+
focusElement = nativeSelection.focusNode
|
|
180
|
+
} else if (nativeSelection.focusNode.parentElement) {
|
|
181
|
+
focusElement = nativeSelection.focusNode.parentElement
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (focusElement) {
|
|
185
|
+
const linkElement = focusElement.closest("a") || focusElement.parentElement?.closest("a")
|
|
186
|
+
if (linkElement) {
|
|
187
|
+
domRect = linkElement.getBoundingClientRect()
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (!domRect && nativeSelection.focusNode) {
|
|
192
|
+
// Get parent element if focusNode is not an HTMLElement
|
|
193
|
+
const parentElement = nativeSelection.focusNode instanceof HTMLElement
|
|
194
|
+
? nativeSelection.focusNode
|
|
195
|
+
: nativeSelection.focusNode.parentElement
|
|
196
|
+
if (parentElement) {
|
|
197
|
+
domRect = parentElement.getBoundingClientRect()
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// If in edit mode but no domRect, try to get from range selection
|
|
203
|
+
if (!domRect && isLinkEditMode && $isRangeSelection(selection)) {
|
|
204
|
+
const range = nativeSelection?.getRangeAt(0)
|
|
205
|
+
if (range) {
|
|
206
|
+
domRect = range.getBoundingClientRect()
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (domRect) {
|
|
211
|
+
domRect.y += 40
|
|
212
|
+
setFloatingElemPositionForLinkEditor(domRect, editorElem, anchorElem)
|
|
213
|
+
} else if (isLinkEditMode) {
|
|
214
|
+
// If in edit mode but no domRect, show editor at a default position
|
|
215
|
+
// Use a fallback position based on editor root
|
|
216
|
+
if (rootElement) {
|
|
217
|
+
const rootRect = rootElement.getBoundingClientRect()
|
|
218
|
+
const fallbackRect = new DOMRect(
|
|
219
|
+
rootRect.left + 20,
|
|
220
|
+
rootRect.top + 100,
|
|
221
|
+
300,
|
|
222
|
+
50
|
|
223
|
+
)
|
|
224
|
+
setFloatingElemPositionForLinkEditor(fallbackRect, editorElem, anchorElem)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
setLastSelection(selection)
|
|
228
|
+
} else if (!activeElement || !activeElement.classList.contains("editor-link-input")) {
|
|
229
|
+
if (rootElement !== null) {
|
|
230
|
+
setFloatingElemPositionForLinkEditor(null, editorElem, anchorElem)
|
|
231
|
+
}
|
|
232
|
+
setLastSelection(null)
|
|
233
|
+
setIsLinkEditMode(false)
|
|
234
|
+
if (!linkNode) {
|
|
235
|
+
setLinkUrl("")
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return true
|
|
240
|
+
}, [anchorElem, editor, setIsLinkEditMode, isLinkEditMode, linkUrl])
|
|
241
|
+
|
|
242
|
+
useEffect(() => {
|
|
243
|
+
const scrollerElem = anchorElem.parentElement
|
|
244
|
+
|
|
245
|
+
const update = () => {
|
|
246
|
+
editor.getEditorState().read(() => {
|
|
247
|
+
$updateLinkEditor()
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
window.addEventListener("resize", update)
|
|
252
|
+
|
|
253
|
+
if (scrollerElem) {
|
|
254
|
+
scrollerElem.addEventListener("scroll", update, { passive: true })
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return () => {
|
|
258
|
+
window.removeEventListener("resize", update)
|
|
259
|
+
|
|
260
|
+
if (scrollerElem) {
|
|
261
|
+
scrollerElem.removeEventListener("scroll", update)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}, [anchorElem.parentElement, editor, $updateLinkEditor])
|
|
265
|
+
|
|
266
|
+
useEffect(() => {
|
|
267
|
+
return mergeRegister(
|
|
268
|
+
editor.registerUpdateListener(({ editorState }) => {
|
|
269
|
+
editorState.read(() => {
|
|
270
|
+
$updateLinkEditor()
|
|
271
|
+
})
|
|
272
|
+
}),
|
|
273
|
+
|
|
274
|
+
editor.registerCommand(
|
|
275
|
+
SELECTION_CHANGE_COMMAND,
|
|
276
|
+
() => {
|
|
277
|
+
$updateLinkEditor()
|
|
278
|
+
return true
|
|
279
|
+
},
|
|
280
|
+
COMMAND_PRIORITY_LOW
|
|
281
|
+
),
|
|
282
|
+
editor.registerCommand(
|
|
283
|
+
KEY_ESCAPE_COMMAND,
|
|
284
|
+
() => {
|
|
285
|
+
if (isLink) {
|
|
286
|
+
setIsLink(false)
|
|
287
|
+
return true
|
|
288
|
+
}
|
|
289
|
+
return false
|
|
290
|
+
},
|
|
291
|
+
COMMAND_PRIORITY_HIGH
|
|
292
|
+
)
|
|
293
|
+
)
|
|
294
|
+
}, [editor, $updateLinkEditor, setIsLink, isLink])
|
|
295
|
+
|
|
296
|
+
useEffect(() => {
|
|
297
|
+
editor.getEditorState().read(() => {
|
|
298
|
+
$updateLinkEditor()
|
|
299
|
+
})
|
|
300
|
+
}, [editor, $updateLinkEditor])
|
|
301
|
+
|
|
302
|
+
useEffect(() => {
|
|
303
|
+
if (isLinkEditMode && inputRef.current) {
|
|
304
|
+
inputRef.current.focus()
|
|
305
|
+
// Use setTimeout to avoid calling setState synchronously within effect
|
|
306
|
+
setTimeout(() => {
|
|
307
|
+
setIsLink(true)
|
|
308
|
+
}, 0)
|
|
309
|
+
}
|
|
310
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
311
|
+
}, [isLinkEditMode, isLink])
|
|
312
|
+
|
|
313
|
+
const monitorInputInteraction = (
|
|
314
|
+
event: React.KeyboardEvent<HTMLInputElement>
|
|
315
|
+
) => {
|
|
316
|
+
if (event.key === "Enter") {
|
|
317
|
+
event.preventDefault()
|
|
318
|
+
handleLinkSubmission()
|
|
319
|
+
} else if (event.key === "Escape") {
|
|
320
|
+
event.preventDefault()
|
|
321
|
+
setIsLinkEditMode(false)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const handleLinkSubmission = () => {
|
|
326
|
+
const url = sanitizeUrl(editedLinkUrl)
|
|
327
|
+
if (url && url !== "https://" && url !== "http://") {
|
|
328
|
+
editor.update(() => {
|
|
329
|
+
// Try to get current selection first
|
|
330
|
+
let selection = $getSelection()
|
|
331
|
+
|
|
332
|
+
// If no current selection, try to restore from lastSelection
|
|
333
|
+
if (!selection && lastSelection !== null) {
|
|
334
|
+
// Clone the selection to avoid frozen object error
|
|
335
|
+
if ($isRangeSelection(lastSelection)) {
|
|
336
|
+
const clonedSelection = lastSelection.clone()
|
|
337
|
+
$setSelection(clonedSelection)
|
|
338
|
+
selection = $getSelection()
|
|
339
|
+
} else if ($isNodeSelection(lastSelection)) {
|
|
340
|
+
const clonedSelection = lastSelection.clone()
|
|
341
|
+
$setSelection(clonedSelection)
|
|
342
|
+
selection = $getSelection()
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (!selection) {
|
|
347
|
+
return
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Handle node selection (e.g., image nodes)
|
|
351
|
+
if ($isNodeSelection(selection)) {
|
|
352
|
+
const nodes = selection.getNodes()
|
|
353
|
+
if (nodes.length > 0) {
|
|
354
|
+
const node = nodes[0]
|
|
355
|
+
|
|
356
|
+
// If it's an image node
|
|
357
|
+
if ($isImageNode(node)) {
|
|
358
|
+
// Check if already wrapped in a link
|
|
359
|
+
const existingLinkNode = $findMatchingParent(node, $isLinkNode) ||
|
|
360
|
+
($isLinkNode(node.getParent()) ? node.getParent() : null)
|
|
361
|
+
|
|
362
|
+
if (existingLinkNode) {
|
|
363
|
+
// Update existing link
|
|
364
|
+
existingLinkNode.setURL(url)
|
|
365
|
+
} else {
|
|
366
|
+
// Wrap image in link using wrapNodeInElement (safe for all cases including root)
|
|
367
|
+
const linkNode = $createLinkNode(url)
|
|
368
|
+
$wrapNodeInElement(node, () => linkNode)
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
// Handle range selection
|
|
374
|
+
else if ($isRangeSelection(selection)) {
|
|
375
|
+
// Use default TOGGLE_LINK_COMMAND for range selection
|
|
376
|
+
editor.dispatchCommand(TOGGLE_LINK_COMMAND, url)
|
|
377
|
+
const parent = getSelectedNode(selection).getParent()
|
|
378
|
+
if ($isAutoLinkNode(parent)) {
|
|
379
|
+
const linkNode = $createLinkNode(parent.getURL(), {
|
|
380
|
+
rel: parent.__rel,
|
|
381
|
+
target: parent.__target,
|
|
382
|
+
title: parent.__title,
|
|
383
|
+
})
|
|
384
|
+
parent.replace(linkNode, true)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
})
|
|
388
|
+
setEditedLinkUrl("https://")
|
|
389
|
+
setIsLinkEditMode(false)
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return (
|
|
393
|
+
<div
|
|
394
|
+
ref={editorRef}
|
|
395
|
+
className="editor-floating-link-editor"
|
|
396
|
+
>
|
|
397
|
+
{isLinkEditMode || isLink ? (
|
|
398
|
+
isLinkEditMode ? (
|
|
399
|
+
<div className="editor-floating-link-editor__input-container">
|
|
400
|
+
<Input
|
|
401
|
+
ref={inputRef}
|
|
402
|
+
value={editedLinkUrl}
|
|
403
|
+
onChange={(event) => setEditedLinkUrl(event.target.value)}
|
|
404
|
+
onKeyDown={monitorInputInteraction}
|
|
405
|
+
className="editor-flex-grow"
|
|
406
|
+
/>
|
|
407
|
+
<Button
|
|
408
|
+
size="icon"
|
|
409
|
+
variant="ghost"
|
|
410
|
+
onClick={() => {
|
|
411
|
+
setIsLinkEditMode(false)
|
|
412
|
+
setIsLink(false)
|
|
413
|
+
}}
|
|
414
|
+
className="editor-shrink-0"
|
|
415
|
+
>
|
|
416
|
+
<X className="editor-icon-sm" />
|
|
417
|
+
</Button>
|
|
418
|
+
<Button
|
|
419
|
+
size="icon"
|
|
420
|
+
onClick={handleLinkSubmission}
|
|
421
|
+
className="editor-shrink-0"
|
|
422
|
+
>
|
|
423
|
+
<Check className="editor-icon-sm" />
|
|
424
|
+
</Button>
|
|
425
|
+
</div>
|
|
426
|
+
) : (
|
|
427
|
+
<div className="editor-floating-link-editor__view-container">
|
|
428
|
+
<a
|
|
429
|
+
href={sanitizeUrl(linkUrl)}
|
|
430
|
+
target="_blank"
|
|
431
|
+
rel="noopener noreferrer"
|
|
432
|
+
className="editor-floating-link-editor__link"
|
|
433
|
+
>
|
|
434
|
+
<TypographyPSmall className="editor-truncate">{linkUrl}</TypographyPSmall>
|
|
435
|
+
</a>
|
|
436
|
+
<Flex gap={0} className="editor-shrink-0">
|
|
437
|
+
<Button
|
|
438
|
+
size="icon"
|
|
439
|
+
variant="ghost"
|
|
440
|
+
onClick={() => {
|
|
441
|
+
setEditedLinkUrl(linkUrl)
|
|
442
|
+
setIsLinkEditMode(true)
|
|
443
|
+
}}
|
|
444
|
+
>
|
|
445
|
+
<Pencil className="editor-icon-sm" />
|
|
446
|
+
</Button>
|
|
447
|
+
<Button
|
|
448
|
+
size="icon"
|
|
449
|
+
variant="destructive"
|
|
450
|
+
onClick={() => {
|
|
451
|
+
editor.update(() => {
|
|
452
|
+
const selection = $getSelection()
|
|
453
|
+
// Handle node selection (e.g., image nodes)
|
|
454
|
+
if ($isNodeSelection(selection)) {
|
|
455
|
+
const nodes = selection.getNodes()
|
|
456
|
+
if (nodes.length > 0) {
|
|
457
|
+
const node = nodes[0]
|
|
458
|
+
if ($isImageNode(node)) {
|
|
459
|
+
const linkNode = $findMatchingParent(node, $isLinkNode) ||
|
|
460
|
+
($isLinkNode(node.getParent()) ? node.getParent() : null)
|
|
461
|
+
if (linkNode) {
|
|
462
|
+
// Remove link by unwrapping - insert children into parent and remove link
|
|
463
|
+
const parent = linkNode.getParent()
|
|
464
|
+
if (parent) {
|
|
465
|
+
const children = linkNode.getChildren()
|
|
466
|
+
children.forEach((child) => {
|
|
467
|
+
linkNode.insertBefore(child)
|
|
468
|
+
})
|
|
469
|
+
linkNode.remove()
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
} else {
|
|
475
|
+
// Use default TOGGLE_LINK_COMMAND for range selection
|
|
476
|
+
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null)
|
|
477
|
+
}
|
|
478
|
+
})
|
|
479
|
+
}}
|
|
480
|
+
>
|
|
481
|
+
<Trash className="editor-icon-sm" />
|
|
482
|
+
</Button>
|
|
483
|
+
</Flex>
|
|
484
|
+
</div>
|
|
485
|
+
)
|
|
486
|
+
) : null}
|
|
487
|
+
</div>
|
|
488
|
+
)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function useFloatingLinkEditorToolbar(
|
|
492
|
+
editor: LexicalEditor,
|
|
493
|
+
anchorElem: HTMLDivElement | null,
|
|
494
|
+
isLinkEditMode: boolean,
|
|
495
|
+
setIsLinkEditMode: Dispatch<boolean>
|
|
496
|
+
): JSX.Element | null {
|
|
497
|
+
const [activeEditor, setActiveEditor] = useState(editor)
|
|
498
|
+
const [isLink, setIsLink] = useState(false)
|
|
499
|
+
|
|
500
|
+
useEffect(() => {
|
|
501
|
+
function $updateToolbar() {
|
|
502
|
+
const selection = $getSelection()
|
|
503
|
+
if ($isRangeSelection(selection)) {
|
|
504
|
+
const focusNode = getSelectedNode(selection)
|
|
505
|
+
const focusLinkNode = $findMatchingParent(focusNode, $isLinkNode)
|
|
506
|
+
const focusAutoLinkNode = $findMatchingParent(
|
|
507
|
+
focusNode,
|
|
508
|
+
$isAutoLinkNode
|
|
509
|
+
)
|
|
510
|
+
if (!(focusLinkNode || focusAutoLinkNode)) {
|
|
511
|
+
setIsLink(false)
|
|
512
|
+
return
|
|
513
|
+
}
|
|
514
|
+
const badNode = selection
|
|
515
|
+
.getNodes()
|
|
516
|
+
.filter((node) => !$isLineBreakNode(node))
|
|
517
|
+
.find((node) => {
|
|
518
|
+
const linkNode = $findMatchingParent(node, $isLinkNode)
|
|
519
|
+
const autoLinkNode = $findMatchingParent(node, $isAutoLinkNode)
|
|
520
|
+
return (
|
|
521
|
+
(focusLinkNode && !focusLinkNode.is(linkNode)) ||
|
|
522
|
+
(linkNode && !linkNode.is(focusLinkNode)) ||
|
|
523
|
+
(focusAutoLinkNode && !focusAutoLinkNode.is(autoLinkNode)) ||
|
|
524
|
+
(autoLinkNode &&
|
|
525
|
+
(!autoLinkNode.is(focusAutoLinkNode) ||
|
|
526
|
+
autoLinkNode.getIsUnlinked()))
|
|
527
|
+
)
|
|
528
|
+
})
|
|
529
|
+
if (!badNode) {
|
|
530
|
+
setIsLink(true)
|
|
531
|
+
} else {
|
|
532
|
+
setIsLink(false)
|
|
533
|
+
}
|
|
534
|
+
} else if ($isNodeSelection(selection)) {
|
|
535
|
+
const nodes = selection.getNodes()
|
|
536
|
+
if (nodes.length === 0) {
|
|
537
|
+
setIsLink(false)
|
|
538
|
+
return
|
|
539
|
+
}
|
|
540
|
+
const node = nodes[0]
|
|
541
|
+
if (node) {
|
|
542
|
+
// Check if node itself is a link
|
|
543
|
+
if ($isLinkNode(node)) {
|
|
544
|
+
setIsLink(true)
|
|
545
|
+
return
|
|
546
|
+
}
|
|
547
|
+
// Check if node is wrapped in a link (using $findMatchingParent for better traversal)
|
|
548
|
+
const linkParent = $findMatchingParent(node, $isLinkNode)
|
|
549
|
+
if (linkParent) {
|
|
550
|
+
setIsLink(true)
|
|
551
|
+
return
|
|
552
|
+
}
|
|
553
|
+
// For image nodes, also check direct parent
|
|
554
|
+
if ($isImageNode(node)) {
|
|
555
|
+
const parent = node.getParent()
|
|
556
|
+
if ($isLinkNode(parent)) {
|
|
557
|
+
setIsLink(true)
|
|
558
|
+
return
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
setIsLink(false)
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return mergeRegister(
|
|
566
|
+
editor.registerUpdateListener(({ editorState }) => {
|
|
567
|
+
editorState.read(() => {
|
|
568
|
+
$updateToolbar()
|
|
569
|
+
})
|
|
570
|
+
}),
|
|
571
|
+
// Register TOGGLE_LINK_COMMAND handler for node selection (image nodes)
|
|
572
|
+
editor.registerCommand(
|
|
573
|
+
TOGGLE_LINK_COMMAND,
|
|
574
|
+
(url: string | null) => {
|
|
575
|
+
const selection = $getSelection()
|
|
576
|
+
|
|
577
|
+
// Handle node selection (e.g., image nodes)
|
|
578
|
+
if ($isNodeSelection(selection)) {
|
|
579
|
+
const nodes = selection.getNodes()
|
|
580
|
+
if (nodes.length > 0) {
|
|
581
|
+
const node = nodes[0]
|
|
582
|
+
|
|
583
|
+
if ($isImageNode(node)) {
|
|
584
|
+
if (url) {
|
|
585
|
+
// Create or update link
|
|
586
|
+
const existingLinkNode = $findMatchingParent(node, $isLinkNode) ||
|
|
587
|
+
($isLinkNode(node.getParent()) ? node.getParent() : null)
|
|
588
|
+
|
|
589
|
+
if (existingLinkNode) {
|
|
590
|
+
// Update existing link
|
|
591
|
+
existingLinkNode.setURL(url)
|
|
592
|
+
} else {
|
|
593
|
+
// Wrap image in link using wrapNodeInElement (safe for all cases including root)
|
|
594
|
+
const linkNode = $createLinkNode(url)
|
|
595
|
+
$wrapNodeInElement(node, () => linkNode)
|
|
596
|
+
}
|
|
597
|
+
} else {
|
|
598
|
+
// Remove link
|
|
599
|
+
const linkNode = $findMatchingParent(node, $isLinkNode) ||
|
|
600
|
+
($isLinkNode(node.getParent()) ? node.getParent() : null)
|
|
601
|
+
if (linkNode) {
|
|
602
|
+
// Remove link by unwrapping - insert children into parent and remove link
|
|
603
|
+
const parent = linkNode.getParent()
|
|
604
|
+
if (parent) {
|
|
605
|
+
const children = linkNode.getChildren()
|
|
606
|
+
children.forEach((child) => {
|
|
607
|
+
linkNode.insertBefore(child)
|
|
608
|
+
})
|
|
609
|
+
linkNode.remove()
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return true
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Let default handler process range selection
|
|
619
|
+
return false
|
|
620
|
+
},
|
|
621
|
+
COMMAND_PRIORITY_HIGH
|
|
622
|
+
),
|
|
623
|
+
editor.registerCommand(
|
|
624
|
+
SELECTION_CHANGE_COMMAND,
|
|
625
|
+
(_payload, newEditor) => {
|
|
626
|
+
editor.getEditorState().read(() => {
|
|
627
|
+
$updateToolbar()
|
|
628
|
+
})
|
|
629
|
+
setActiveEditor(newEditor)
|
|
630
|
+
return false
|
|
631
|
+
},
|
|
632
|
+
COMMAND_PRIORITY_CRITICAL
|
|
633
|
+
),
|
|
634
|
+
// Register a listener for when node selection changes to image with link
|
|
635
|
+
editor.registerCommand(
|
|
636
|
+
SELECTION_CHANGE_COMMAND,
|
|
637
|
+
() => {
|
|
638
|
+
editor.getEditorState().read(() => {
|
|
639
|
+
const selection = $getSelection()
|
|
640
|
+
if ($isNodeSelection(selection)) {
|
|
641
|
+
const nodes = selection.getNodes()
|
|
642
|
+
if (nodes.length > 0) {
|
|
643
|
+
const node = nodes[0]
|
|
644
|
+
if ($isImageNode(node)) {
|
|
645
|
+
const linkNode = $findMatchingParent(node, $isLinkNode) ||
|
|
646
|
+
($isLinkNode(node.getParent()) ? node.getParent() : null)
|
|
647
|
+
if (linkNode) {
|
|
648
|
+
// Delay to ensure DOM is updated
|
|
649
|
+
setTimeout(() => {
|
|
650
|
+
editor.getEditorState().read(() => {
|
|
651
|
+
$updateToolbar()
|
|
652
|
+
})
|
|
653
|
+
}, 10)
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
})
|
|
659
|
+
return false
|
|
660
|
+
},
|
|
661
|
+
COMMAND_PRIORITY_LOW
|
|
662
|
+
),
|
|
663
|
+
editor.registerCommand(
|
|
664
|
+
CLICK_COMMAND,
|
|
665
|
+
(payload) => {
|
|
666
|
+
let shouldReturnTrue = false
|
|
667
|
+
|
|
668
|
+
editor.getEditorState().read(() => {
|
|
669
|
+
const selection = $getSelection()
|
|
670
|
+
|
|
671
|
+
// Check if we clicked on an image node (with or without link)
|
|
672
|
+
let hasImageNode = false
|
|
673
|
+
if ($isNodeSelection(selection)) {
|
|
674
|
+
const nodes = selection.getNodes()
|
|
675
|
+
if (nodes.length > 0) {
|
|
676
|
+
const node = nodes[0]
|
|
677
|
+
if ($isImageNode(node)) {
|
|
678
|
+
hasImageNode = true
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Handle Ctrl/Cmd + click to open link
|
|
684
|
+
if (payload.metaKey || payload.ctrlKey) {
|
|
685
|
+
if ($isRangeSelection(selection)) {
|
|
686
|
+
const node = getSelectedNode(selection)
|
|
687
|
+
if (node) {
|
|
688
|
+
const linkNode = $findMatchingParent(node, $isLinkNode)
|
|
689
|
+
if ($isLinkNode(linkNode)) {
|
|
690
|
+
const url = linkNode.getURL()
|
|
691
|
+
// Validate URL before opening to prevent errors with invalid URLs
|
|
692
|
+
if (url && validateUrl(url)) {
|
|
693
|
+
window.open(url, "_blank")
|
|
694
|
+
}
|
|
695
|
+
shouldReturnTrue = true
|
|
696
|
+
return
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
} else if ($isNodeSelection(selection)) {
|
|
700
|
+
const nodes = selection.getNodes()
|
|
701
|
+
if (nodes.length > 0) {
|
|
702
|
+
const node = nodes[0]
|
|
703
|
+
let linkNode = null
|
|
704
|
+
if ($isLinkNode(node)) {
|
|
705
|
+
linkNode = node
|
|
706
|
+
} else {
|
|
707
|
+
if (node) {
|
|
708
|
+
linkNode = $findMatchingParent(node, $isLinkNode)
|
|
709
|
+
if (!linkNode && $isImageNode(node)) {
|
|
710
|
+
const parent = node.getParent()
|
|
711
|
+
if ($isLinkNode(parent)) {
|
|
712
|
+
linkNode = parent
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
if (linkNode) {
|
|
718
|
+
const url = linkNode.getURL()
|
|
719
|
+
if (url && validateUrl(url)) {
|
|
720
|
+
window.open(url, "_blank")
|
|
721
|
+
}
|
|
722
|
+
shouldReturnTrue = true
|
|
723
|
+
return
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// If we clicked on an image (with or without link), trigger toolbar update
|
|
730
|
+
if (hasImageNode) {
|
|
731
|
+
// Use requestAnimationFrame to ensure selection is updated
|
|
732
|
+
requestAnimationFrame(() => {
|
|
733
|
+
editor.getEditorState().read(() => {
|
|
734
|
+
$updateToolbar()
|
|
735
|
+
})
|
|
736
|
+
})
|
|
737
|
+
}
|
|
738
|
+
})
|
|
739
|
+
|
|
740
|
+
if (shouldReturnTrue) {
|
|
741
|
+
return true
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Trigger toolbar update on click to ensure floating editor shows
|
|
745
|
+
setTimeout(() => {
|
|
746
|
+
editor.getEditorState().read(() => {
|
|
747
|
+
$updateToolbar()
|
|
748
|
+
})
|
|
749
|
+
}, 0)
|
|
750
|
+
return false
|
|
751
|
+
},
|
|
752
|
+
COMMAND_PRIORITY_HIGH
|
|
753
|
+
)
|
|
754
|
+
)
|
|
755
|
+
}, [editor])
|
|
756
|
+
|
|
757
|
+
if (!anchorElem) {
|
|
758
|
+
return null
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
return createPortal(
|
|
762
|
+
<FloatingLinkEditor
|
|
763
|
+
editor={activeEditor}
|
|
764
|
+
isLink={isLink}
|
|
765
|
+
anchorElem={anchorElem}
|
|
766
|
+
setIsLink={setIsLink}
|
|
767
|
+
isLinkEditMode={isLinkEditMode}
|
|
768
|
+
setIsLinkEditMode={setIsLinkEditMode}
|
|
769
|
+
/>,
|
|
770
|
+
anchorElem
|
|
771
|
+
)
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
export function FloatingLinkEditorPlugin({
|
|
775
|
+
anchorElem,
|
|
776
|
+
isLinkEditMode,
|
|
777
|
+
setIsLinkEditMode,
|
|
778
|
+
}: {
|
|
779
|
+
anchorElem: HTMLDivElement | null
|
|
780
|
+
isLinkEditMode: boolean
|
|
781
|
+
setIsLinkEditMode: Dispatch<boolean>
|
|
782
|
+
}): JSX.Element | null {
|
|
783
|
+
const [editor] = useLexicalComposerContext()
|
|
784
|
+
|
|
785
|
+
return useFloatingLinkEditorToolbar(
|
|
786
|
+
editor,
|
|
787
|
+
anchorElem,
|
|
788
|
+
isLinkEditMode,
|
|
789
|
+
setIsLinkEditMode
|
|
790
|
+
)
|
|
791
|
+
}
|