@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,311 @@
|
|
|
1
|
+
import type { JSX } from "react"
|
|
2
|
+
import { useMemo } from "react"
|
|
3
|
+
import { $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link"
|
|
4
|
+
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"
|
|
5
|
+
import {
|
|
6
|
+
NodeContextMenuOption,
|
|
7
|
+
NodeContextMenuPlugin,
|
|
8
|
+
NodeContextMenuSeparator,
|
|
9
|
+
} from "@lexical/react/LexicalNodeContextMenuPlugin"
|
|
10
|
+
import { $mergeCells, $isTableCellNode, $isTableSelection, $unmergeCell, $insertTableColumnAtSelection, $insertTableRowAtSelection } from "@lexical/table"
|
|
11
|
+
import type { TableCellNode } from "@lexical/table"
|
|
12
|
+
import { $isListNode } from "@lexical/list"
|
|
13
|
+
import { $findMatchingParent } from "@lexical/utils"
|
|
14
|
+
import {
|
|
15
|
+
$getSelection,
|
|
16
|
+
$isDecoratorNode,
|
|
17
|
+
$isNodeSelection,
|
|
18
|
+
$isRangeSelection,
|
|
19
|
+
COPY_COMMAND,
|
|
20
|
+
CUT_COMMAND,
|
|
21
|
+
PASTE_COMMAND,
|
|
22
|
+
type LexicalNode,
|
|
23
|
+
} from "lexical"
|
|
24
|
+
import { $isLayoutItemNode } from "../nodes/layout-item-node"
|
|
25
|
+
import { OPEN_UPDATE_LAYOUT_MODAL_COMMAND } from "../plugins/layout-plugin"
|
|
26
|
+
import { OPEN_LIST_COLOR_PICKER_COMMAND } from "../plugins/list-color-plugin"
|
|
27
|
+
import {
|
|
28
|
+
Clipboard,
|
|
29
|
+
ClipboardType,
|
|
30
|
+
Copy,
|
|
31
|
+
Link2Off,
|
|
32
|
+
Scissors,
|
|
33
|
+
Trash2,
|
|
34
|
+
Combine,
|
|
35
|
+
SplitSquareVertical,
|
|
36
|
+
Columns3,
|
|
37
|
+
Rows3,
|
|
38
|
+
LayoutGrid,
|
|
39
|
+
Palette,
|
|
40
|
+
} from "lucide-react"
|
|
41
|
+
import { IconSize } from "../ui/typography"
|
|
42
|
+
|
|
43
|
+
export function ContextMenuPlugin(): JSX.Element {
|
|
44
|
+
const [editor] = useLexicalComposerContext()
|
|
45
|
+
|
|
46
|
+
const isInTable = (node: LexicalNode) =>
|
|
47
|
+
$findMatchingParent(node, $isTableCellNode) !== null
|
|
48
|
+
|
|
49
|
+
const isMergedCell = (node: LexicalNode) => {
|
|
50
|
+
const cell = $findMatchingParent(node, $isTableCellNode)
|
|
51
|
+
if (!cell || !$isTableCellNode(cell)) return false
|
|
52
|
+
const c = cell as TableCellNode
|
|
53
|
+
const colSpan =
|
|
54
|
+
typeof (c as { getColSpan?: () => number }).getColSpan === "function"
|
|
55
|
+
? (c as { getColSpan: () => number }).getColSpan()
|
|
56
|
+
: (c as { __colSpan?: number }).__colSpan ?? 1
|
|
57
|
+
const rowSpan =
|
|
58
|
+
typeof (c as { getRowSpan?: () => number }).getRowSpan === "function"
|
|
59
|
+
? (c as { getRowSpan: () => number }).getRowSpan()
|
|
60
|
+
: (c as { __rowSpan?: number }).__rowSpan ?? 1
|
|
61
|
+
return colSpan > 1 || rowSpan > 1
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const isInLayoutItem = (node: LexicalNode) =>
|
|
65
|
+
$findMatchingParent(node, $isLayoutItemNode) !== null
|
|
66
|
+
|
|
67
|
+
const isInList = (node: LexicalNode) =>
|
|
68
|
+
$findMatchingParent(node, $isListNode) !== null
|
|
69
|
+
|
|
70
|
+
const items = useMemo(() => {
|
|
71
|
+
return [
|
|
72
|
+
new NodeContextMenuOption(`Gộp ô`, {
|
|
73
|
+
$onSelect: () => {
|
|
74
|
+
editor.update(() => {
|
|
75
|
+
const selection = $getSelection()
|
|
76
|
+
if (!$isTableSelection(selection)) return
|
|
77
|
+
const nodes = selection.getNodes()
|
|
78
|
+
const seen = new Set<string>()
|
|
79
|
+
const cellNodes: import("@lexical/table").TableCellNode[] = []
|
|
80
|
+
for (const n of nodes) {
|
|
81
|
+
const cell = $findMatchingParent(n, $isTableCellNode)
|
|
82
|
+
if (cell && !seen.has(cell.getKey())) {
|
|
83
|
+
seen.add(cell.getKey())
|
|
84
|
+
cellNodes.push(cell)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (cellNodes.length >= 2) $mergeCells(cellNodes)
|
|
88
|
+
})
|
|
89
|
+
},
|
|
90
|
+
$showOn: isInTable,
|
|
91
|
+
disabled: false,
|
|
92
|
+
icon: <IconSize size="sm"><Combine /></IconSize>,
|
|
93
|
+
}),
|
|
94
|
+
new NodeContextMenuOption(`Tách ô`, {
|
|
95
|
+
$onSelect: () => {
|
|
96
|
+
editor.update(() => {
|
|
97
|
+
$unmergeCell()
|
|
98
|
+
})
|
|
99
|
+
},
|
|
100
|
+
$showOn: isMergedCell,
|
|
101
|
+
disabled: false,
|
|
102
|
+
icon: <IconSize size="sm"><SplitSquareVertical /></IconSize>,
|
|
103
|
+
}),
|
|
104
|
+
new NodeContextMenuOption(`Chèn cột bên phải`, {
|
|
105
|
+
$onSelect: () => {
|
|
106
|
+
editor.update(() => {
|
|
107
|
+
$insertTableColumnAtSelection(true)
|
|
108
|
+
})
|
|
109
|
+
},
|
|
110
|
+
$showOn: isInTable,
|
|
111
|
+
disabled: false,
|
|
112
|
+
icon: <IconSize size="sm"><Columns3 /></IconSize>,
|
|
113
|
+
}),
|
|
114
|
+
new NodeContextMenuOption(`Chèn cột bên trái`, {
|
|
115
|
+
$onSelect: () => {
|
|
116
|
+
editor.update(() => {
|
|
117
|
+
$insertTableColumnAtSelection(false)
|
|
118
|
+
})
|
|
119
|
+
},
|
|
120
|
+
$showOn: isInTable,
|
|
121
|
+
disabled: false,
|
|
122
|
+
icon: <IconSize size="sm"><Columns3 /></IconSize>,
|
|
123
|
+
}),
|
|
124
|
+
new NodeContextMenuOption(`Chèn dòng bên dưới`, {
|
|
125
|
+
$onSelect: () => {
|
|
126
|
+
editor.update(() => {
|
|
127
|
+
$insertTableRowAtSelection(true)
|
|
128
|
+
})
|
|
129
|
+
},
|
|
130
|
+
$showOn: isInTable,
|
|
131
|
+
disabled: false,
|
|
132
|
+
icon: <IconSize size="sm"><Rows3 /></IconSize>,
|
|
133
|
+
}),
|
|
134
|
+
new NodeContextMenuOption(`Chèn dòng bên trên`, {
|
|
135
|
+
$onSelect: () => {
|
|
136
|
+
editor.update(() => {
|
|
137
|
+
$insertTableRowAtSelection(false)
|
|
138
|
+
})
|
|
139
|
+
},
|
|
140
|
+
$showOn: isInTable,
|
|
141
|
+
disabled: false,
|
|
142
|
+
icon: <IconSize size="sm"><Rows3 /></IconSize>,
|
|
143
|
+
}),
|
|
144
|
+
new NodeContextMenuSeparator({
|
|
145
|
+
$showOn: isInTable,
|
|
146
|
+
}),
|
|
147
|
+
new NodeContextMenuOption(`Cập nhật column layout`, {
|
|
148
|
+
$onSelect: () => {
|
|
149
|
+
let layoutItemKey: string | null = null
|
|
150
|
+
editor.getEditorState().read(() => {
|
|
151
|
+
const selection = $getSelection()
|
|
152
|
+
if (!$isRangeSelection(selection)) return
|
|
153
|
+
const layoutItem = $findMatchingParent(
|
|
154
|
+
selection.anchor.getNode(),
|
|
155
|
+
$isLayoutItemNode
|
|
156
|
+
)
|
|
157
|
+
if ($isLayoutItemNode(layoutItem)) layoutItemKey = layoutItem.getKey()
|
|
158
|
+
})
|
|
159
|
+
if (layoutItemKey)
|
|
160
|
+
editor.dispatchCommand(OPEN_UPDATE_LAYOUT_MODAL_COMMAND, {
|
|
161
|
+
layoutItemKey,
|
|
162
|
+
})
|
|
163
|
+
},
|
|
164
|
+
$showOn: isInLayoutItem,
|
|
165
|
+
disabled: false,
|
|
166
|
+
icon: <IconSize size="sm"><LayoutGrid /></IconSize>,
|
|
167
|
+
}),
|
|
168
|
+
new NodeContextMenuSeparator({
|
|
169
|
+
$showOn: isInLayoutItem,
|
|
170
|
+
}),
|
|
171
|
+
new NodeContextMenuOption(`Đổi màu list`, {
|
|
172
|
+
$onSelect: () => {
|
|
173
|
+
let listKey: string | null = null
|
|
174
|
+
editor.getEditorState().read(() => {
|
|
175
|
+
const selection = $getSelection()
|
|
176
|
+
if (!$isRangeSelection(selection)) return
|
|
177
|
+
const listNode = $findMatchingParent(
|
|
178
|
+
selection.anchor.getNode(),
|
|
179
|
+
$isListNode
|
|
180
|
+
)
|
|
181
|
+
if ($isListNode(listNode)) listKey = listNode.getKey()
|
|
182
|
+
})
|
|
183
|
+
if (listKey)
|
|
184
|
+
editor.dispatchCommand(OPEN_LIST_COLOR_PICKER_COMMAND, {
|
|
185
|
+
listKey,
|
|
186
|
+
})
|
|
187
|
+
},
|
|
188
|
+
$showOn: isInList,
|
|
189
|
+
disabled: false,
|
|
190
|
+
icon: <IconSize size="sm"><Palette /></IconSize>,
|
|
191
|
+
}),
|
|
192
|
+
new NodeContextMenuSeparator({
|
|
193
|
+
$showOn: isInList,
|
|
194
|
+
}),
|
|
195
|
+
new NodeContextMenuOption(`Remove Link`, {
|
|
196
|
+
$onSelect: () => {
|
|
197
|
+
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null)
|
|
198
|
+
},
|
|
199
|
+
$showOn: (node: LexicalNode) => $isLinkNode(node.getParent()),
|
|
200
|
+
disabled: false,
|
|
201
|
+
icon: <IconSize size="sm"><Link2Off /></IconSize>,
|
|
202
|
+
}),
|
|
203
|
+
new NodeContextMenuSeparator({
|
|
204
|
+
$showOn: (node: LexicalNode) => $isLinkNode(node.getParent()),
|
|
205
|
+
}),
|
|
206
|
+
new NodeContextMenuOption(`Cut`, {
|
|
207
|
+
$onSelect: () => {
|
|
208
|
+
editor.dispatchCommand(CUT_COMMAND, null)
|
|
209
|
+
},
|
|
210
|
+
disabled: false,
|
|
211
|
+
icon: <IconSize size="sm"><Scissors /></IconSize>,
|
|
212
|
+
}),
|
|
213
|
+
new NodeContextMenuOption(`Copy`, {
|
|
214
|
+
$onSelect: () => {
|
|
215
|
+
editor.dispatchCommand(COPY_COMMAND, null)
|
|
216
|
+
},
|
|
217
|
+
disabled: false,
|
|
218
|
+
icon: <IconSize size="sm"><Copy /></IconSize>,
|
|
219
|
+
}),
|
|
220
|
+
new NodeContextMenuOption(`Paste`, {
|
|
221
|
+
$onSelect: () => {
|
|
222
|
+
navigator.clipboard.read().then(async function (...args) {
|
|
223
|
+
void args
|
|
224
|
+
const data = new DataTransfer()
|
|
225
|
+
|
|
226
|
+
const readClipboardItems = await navigator.clipboard.read()
|
|
227
|
+
const item = readClipboardItems[0]
|
|
228
|
+
if (!item) return
|
|
229
|
+
|
|
230
|
+
const permission = await navigator.permissions.query({
|
|
231
|
+
name: "clipboard-read" as PermissionName,
|
|
232
|
+
})
|
|
233
|
+
if (permission.state === "denied") {
|
|
234
|
+
alert("Not allowed to paste from clipboard.")
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
for (const type of item.types) {
|
|
239
|
+
const dataString = await (await item.getType(type)).text()
|
|
240
|
+
data.setData(type, dataString)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const event = new ClipboardEvent("paste", {
|
|
244
|
+
clipboardData: data,
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
editor.dispatchCommand(PASTE_COMMAND, event)
|
|
248
|
+
})
|
|
249
|
+
},
|
|
250
|
+
disabled: false,
|
|
251
|
+
icon: <IconSize size="sm"><Clipboard /></IconSize>,
|
|
252
|
+
}),
|
|
253
|
+
new NodeContextMenuOption(`Paste as Plain Text`, {
|
|
254
|
+
$onSelect: () => {
|
|
255
|
+
navigator.clipboard.read().then(async function (...args) {
|
|
256
|
+
void args
|
|
257
|
+
const permission = await navigator.permissions.query({
|
|
258
|
+
name: "clipboard-read" as PermissionName,
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
if (permission.state === "denied") {
|
|
262
|
+
alert("Not allowed to paste from clipboard.")
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const data = new DataTransfer()
|
|
267
|
+
const clipboardText = await navigator.clipboard.readText()
|
|
268
|
+
data.setData("text/plain", clipboardText)
|
|
269
|
+
|
|
270
|
+
const event = new ClipboardEvent("paste", {
|
|
271
|
+
clipboardData: data,
|
|
272
|
+
})
|
|
273
|
+
editor.dispatchCommand(PASTE_COMMAND, event)
|
|
274
|
+
})
|
|
275
|
+
},
|
|
276
|
+
disabled: false,
|
|
277
|
+
icon: <IconSize size="sm"><ClipboardType /></IconSize>,
|
|
278
|
+
}),
|
|
279
|
+
new NodeContextMenuSeparator(),
|
|
280
|
+
new NodeContextMenuOption(`Delete Node`, {
|
|
281
|
+
$onSelect: () => {
|
|
282
|
+
const selection = $getSelection()
|
|
283
|
+
if ($isRangeSelection(selection)) {
|
|
284
|
+
const currentNode = selection.anchor.getNode()
|
|
285
|
+
const ancestorNodeWithRootAsParent = currentNode.getParents().at(-2)
|
|
286
|
+
|
|
287
|
+
ancestorNodeWithRootAsParent?.remove()
|
|
288
|
+
} else if ($isNodeSelection(selection)) {
|
|
289
|
+
const selectedNodes = selection.getNodes()
|
|
290
|
+
selectedNodes.forEach((node) => {
|
|
291
|
+
if ($isDecoratorNode(node)) {
|
|
292
|
+
node.remove()
|
|
293
|
+
}
|
|
294
|
+
})
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
disabled: false,
|
|
298
|
+
icon: <IconSize size="sm"><Trash2 /></IconSize>,
|
|
299
|
+
}),
|
|
300
|
+
]
|
|
301
|
+
}, [editor])
|
|
302
|
+
|
|
303
|
+
return (
|
|
304
|
+
<NodeContextMenuPlugin
|
|
305
|
+
className="editor-context-menu"
|
|
306
|
+
itemClassName="editor-context-menu-item"
|
|
307
|
+
separatorClassName="editor-context-menu-separator"
|
|
308
|
+
items={items}
|
|
309
|
+
/>
|
|
310
|
+
)
|
|
311
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
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 { useEffect } from "react"
|
|
11
|
+
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"
|
|
12
|
+
import { DRAG_DROP_PASTE } from "@lexical/rich-text"
|
|
13
|
+
import { isMimeType, mediaFileReader } from "@lexical/utils"
|
|
14
|
+
import { COMMAND_PRIORITY_LOW } from "lexical"
|
|
15
|
+
|
|
16
|
+
import { INSERT_IMAGE_COMMAND } from "./images-plugin"
|
|
17
|
+
|
|
18
|
+
const ACCEPTABLE_IMAGE_TYPES = [
|
|
19
|
+
"image/",
|
|
20
|
+
"image/heic",
|
|
21
|
+
"image/heif",
|
|
22
|
+
"image/gif",
|
|
23
|
+
"image/webp",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
export function DragDropPastePlugin(): null {
|
|
27
|
+
const [editor] = useLexicalComposerContext()
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
return editor.registerCommand(
|
|
30
|
+
DRAG_DROP_PASTE,
|
|
31
|
+
(files) => {
|
|
32
|
+
;(async () => {
|
|
33
|
+
const filesResult = await mediaFileReader(
|
|
34
|
+
files,
|
|
35
|
+
[ACCEPTABLE_IMAGE_TYPES].flatMap((x) => x)
|
|
36
|
+
)
|
|
37
|
+
for (const { file, result } of filesResult) {
|
|
38
|
+
if (isMimeType(file, ACCEPTABLE_IMAGE_TYPES)) {
|
|
39
|
+
editor.dispatchCommand(INSERT_IMAGE_COMMAND, {
|
|
40
|
+
altText: file.name,
|
|
41
|
+
src: result,
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
})()
|
|
46
|
+
return true
|
|
47
|
+
},
|
|
48
|
+
COMMAND_PRIORITY_LOW
|
|
49
|
+
)
|
|
50
|
+
}, [editor])
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { JSX, useRef } from "react"
|
|
4
|
+
import { DraggableBlockPlugin_EXPERIMENTAL } from "@lexical/react/LexicalDraggableBlockPlugin"
|
|
5
|
+
import { GripVerticalIcon } from "lucide-react"
|
|
6
|
+
import { IconSize } from "../ui/typography"
|
|
7
|
+
|
|
8
|
+
const DRAGGABLE_BLOCK_MENU_CLASSNAME = "editor-draggable-menu"
|
|
9
|
+
|
|
10
|
+
function isOnMenu(element: HTMLElement): boolean {
|
|
11
|
+
return !!element.closest(`.${DRAGGABLE_BLOCK_MENU_CLASSNAME}`)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function DraggableBlockPlugin({
|
|
15
|
+
anchorElem,
|
|
16
|
+
}: {
|
|
17
|
+
anchorElem: HTMLElement | null
|
|
18
|
+
}): JSX.Element | null {
|
|
19
|
+
const menuRef = useRef<HTMLDivElement>(null)
|
|
20
|
+
const targetLineRef = useRef<HTMLDivElement>(null)
|
|
21
|
+
|
|
22
|
+
if (!anchorElem) {
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<DraggableBlockPlugin_EXPERIMENTAL
|
|
28
|
+
anchorElem={anchorElem}
|
|
29
|
+
menuRef={menuRef as React.RefObject<HTMLDivElement>}
|
|
30
|
+
targetLineRef={targetLineRef as React.RefObject<HTMLDivElement>}
|
|
31
|
+
menuComponent={
|
|
32
|
+
<div
|
|
33
|
+
ref={menuRef}
|
|
34
|
+
className="editor-draggable-menu"
|
|
35
|
+
>
|
|
36
|
+
<IconSize size="sm" className="editor-opacity-30">
|
|
37
|
+
<GripVerticalIcon />
|
|
38
|
+
</IconSize>
|
|
39
|
+
</div>
|
|
40
|
+
}
|
|
41
|
+
targetLineComponent={
|
|
42
|
+
<div
|
|
43
|
+
ref={targetLineRef}
|
|
44
|
+
className="editor-draggable-line"
|
|
45
|
+
/>
|
|
46
|
+
}
|
|
47
|
+
isOnMenu={isOnMenu}
|
|
48
|
+
/>
|
|
49
|
+
)
|
|
50
|
+
}
|