@haklex/rich-plugin-block-handle 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,1455 +1,1324 @@
1
- import { jsx, jsxs } from "react/jsx-runtime";
2
- import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuGroup, DropdownMenuLabel, DropdownMenuItem, DropdownMenuSeparator } from "@haklex/rich-editor-ui";
1
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@haklex/rich-editor-ui";
3
2
  import { usePortalTheme } from "@haklex/rich-style-token";
4
3
  import { $createCodeNode } from "@lexical/code-core";
5
4
  import { INSERT_CHECK_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND } from "@lexical/list";
6
5
  import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
7
6
  import { INSERT_HORIZONTAL_RULE_COMMAND } from "@lexical/react/LexicalHorizontalRuleNode";
8
- import { $createQuoteNode, $createHeadingNode } from "@lexical/rich-text";
7
+ import { $createHeadingNode, $createQuoteNode } from "@lexical/rich-text";
9
8
  import { $setBlocksType } from "@lexical/selection";
10
- import { $getRoot, $createParagraphNode, $getSelection, $parseSerializedNode, $isElementNode, $createNodeSelection, $setSelection, $isRangeSelection, $createTabNode, $isNodeSelection, PASTE_TAG, KEY_DOWN_COMMAND, COMMAND_PRIORITY_CRITICAL, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_UP_COMMAND, SELECT_ALL_COMMAND, COMMAND_PRIORITY_HIGH, KEY_ESCAPE_COMMAND, COPY_COMMAND, CUT_COMMAND, PASTE_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DELETE_COMMAND, REMOVE_TEXT_COMMAND, DELETE_CHARACTER_COMMAND, $getNearestNodeFromDOMNode, $getNodeByKey, DRAGOVER_COMMAND, DROP_COMMAND, DRAGSTART_COMMAND } from "lexical";
11
- import { Plus, GripVertical, Type, Heading1, Heading2, Heading3, List, ListOrdered, ListChecks, TextQuote, Minus, Code2, Copy, ArrowUp, ArrowDown, Trash2 } from "lucide-react";
12
- import { useRef, useCallback, useEffect, useState } from "react";
9
+ import { $createNodeSelection, $createParagraphNode, $createTabNode, $getNearestNodeFromDOMNode, $getNodeByKey, $getRoot, $getSelection, $isElementNode, $isNodeSelection, $isRangeSelection, $parseSerializedNode, $setSelection, COMMAND_PRIORITY_CRITICAL, COMMAND_PRIORITY_HIGH, COPY_COMMAND, CUT_COMMAND, DELETE_CHARACTER_COMMAND, DRAGOVER_COMMAND, DRAGSTART_COMMAND, DROP_COMMAND, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_UP_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DELETE_COMMAND, KEY_DOWN_COMMAND, KEY_ESCAPE_COMMAND, PASTE_COMMAND, PASTE_TAG, REMOVE_TEXT_COMMAND, SELECT_ALL_COMMAND } from "lexical";
10
+ import { ArrowDown, ArrowUp, Code2, Copy, GripVertical, Heading1, Heading2, Heading3, List, ListChecks, ListOrdered, Minus, Plus, TextQuote, Trash2, Type } from "lucide-react";
11
+ import { useCallback, useEffect, useRef, useState } from "react";
13
12
  import { createPortal } from "react-dom";
14
13
  import { $generateNodesFromDOM } from "@lexical/html";
14
+ import { jsx, jsxs } from "react/jsx-runtime";
15
+ //#region src/styles.css.ts
15
16
  var handleContainer = "iihqkc0";
16
17
  var handleContainerVisible = "iihqkc1";
17
18
  var handleBtn = "iihqkc2";
18
19
  var draggingBlock = "iihqkc3";
19
20
  var dragPreview = "iihqkc4";
20
- var dropIndicator = "iihqkc5";
21
21
  var blockSelected = "iihqkc6";
22
22
  var dragCountBadge = "iihqkc7";
23
23
  var menuItemDestructive = "iihqkc8";
24
- const PASTEABLE_CLIPBOARD_TYPES = [
25
- "application/x-lexical-editor",
26
- "text/html",
27
- "text/plain",
28
- "text/uri-list"
24
+ //#endregion
25
+ //#region src/blockSelectionUtils.ts
26
+ var PASTEABLE_CLIPBOARD_TYPES = [
27
+ "application/x-lexical-editor",
28
+ "text/html",
29
+ "text/plain",
30
+ "text/uri-list"
29
31
  ];
30
32
  function serializeNodeWithChildren(node) {
31
- const serialized = node.exportJSON();
32
- if ($isElementNode(node)) {
33
- serialized.children = node.getChildren().map(serializeNodeWithChildren);
34
- }
35
- return serialized;
33
+ const serialized = node.exportJSON();
34
+ if ($isElementNode(node)) serialized.children = node.getChildren().map(serializeNodeWithChildren);
35
+ return serialized;
36
36
  }
37
37
  function getHtmlFromTopLevelNodes(editor, nodes) {
38
- return nodes.map((node) => editor.getElementByKey(node.getKey())?.outerHTML ?? "").filter(Boolean).join("\n");
38
+ return nodes.map((node) => editor.getElementByKey(node.getKey())?.outerHTML ?? "").filter(Boolean).join("\n");
39
39
  }
40
40
  function setBlockClipboardDataTransfer(dataTransfer, clipboardData) {
41
- for (const [mimeType, value] of Object.entries(clipboardData)) {
42
- dataTransfer.setData(mimeType, value);
43
- }
41
+ for (const [mimeType, value] of Object.entries(clipboardData)) dataTransfer.setData(mimeType, value);
44
42
  }
45
43
  function createDataTransferFromBlockClipboardData(clipboardData) {
46
- return {
47
- files: { length: 0 },
48
- get types() {
49
- return Object.keys(clipboardData);
50
- },
51
- getData(type) {
52
- return clipboardData[type] ?? "";
53
- },
54
- setData(type, value) {
55
- clipboardData[type] = value;
56
- }
57
- };
44
+ return {
45
+ files: { length: 0 },
46
+ get types() {
47
+ return Object.keys(clipboardData);
48
+ },
49
+ getData(type) {
50
+ return clipboardData[type] ?? "";
51
+ },
52
+ setData(type, value) {
53
+ clipboardData[type] = value;
54
+ }
55
+ };
58
56
  }
59
57
  async function readNativeClipboardDataTransfer() {
60
- const clipboard = globalThis.navigator?.clipboard;
61
- if (!clipboard) return null;
62
- const clipboardData = {};
63
- if (typeof clipboard.read === "function") {
64
- try {
65
- const items = await clipboard.read();
66
- for (const item of items) {
67
- for (const type of PASTEABLE_CLIPBOARD_TYPES) {
68
- if (!item.types.includes(type)) continue;
69
- const blob = await item.getType(type);
70
- clipboardData[type] = await blob.text();
71
- }
72
- }
73
- } catch {
74
- }
75
- }
76
- if (!clipboardData["text/plain"] && typeof clipboard.readText === "function") {
77
- try {
78
- const text = await clipboard.readText();
79
- if (text) {
80
- clipboardData["text/plain"] = text;
81
- }
82
- } catch {
83
- }
84
- }
85
- const dataTransfer = createDataTransferFromBlockClipboardData(clipboardData);
86
- return hasInsertableClipboardData(dataTransfer) ? dataTransfer : null;
58
+ const clipboard = globalThis.navigator?.clipboard;
59
+ if (!clipboard) return null;
60
+ const clipboardData = {};
61
+ if (typeof clipboard.read === "function") try {
62
+ const items = await clipboard.read();
63
+ for (const item of items) for (const type of PASTEABLE_CLIPBOARD_TYPES) {
64
+ if (!item.types.includes(type)) continue;
65
+ clipboardData[type] = await (await item.getType(type)).text();
66
+ }
67
+ } catch {}
68
+ if (!clipboardData["text/plain"] && typeof clipboard.readText === "function") try {
69
+ const text = await clipboard.readText();
70
+ if (text) clipboardData["text/plain"] = text;
71
+ } catch {}
72
+ const dataTransfer = createDataTransferFromBlockClipboardData(clipboardData);
73
+ return hasInsertableClipboardData(dataTransfer) ? dataTransfer : null;
87
74
  }
88
75
  function writeBlockClipboardDataToNativeClipboard(editor, clipboardData) {
89
- const rootElement = editor.getRootElement();
90
- const ownerDocument = rootElement?.ownerDocument ?? globalThis.document;
91
- if (!ownerDocument?.execCommand) return false;
92
- let wrote = false;
93
- const onCopy = (event) => {
94
- if (!event.clipboardData) return;
95
- event.preventDefault();
96
- event.stopImmediatePropagation();
97
- setBlockClipboardDataTransfer(event.clipboardData, clipboardData);
98
- wrote = true;
99
- };
100
- ownerDocument.addEventListener("copy", onCopy, true);
101
- try {
102
- return ownerDocument.execCommand("copy") && wrote;
103
- } finally {
104
- ownerDocument.removeEventListener("copy", onCopy, true);
105
- }
76
+ const ownerDocument = editor.getRootElement()?.ownerDocument ?? globalThis.document;
77
+ if (!ownerDocument?.execCommand) return false;
78
+ let wrote = false;
79
+ const onCopy = (event) => {
80
+ if (!event.clipboardData) return;
81
+ event.preventDefault();
82
+ event.stopImmediatePropagation();
83
+ setBlockClipboardDataTransfer(event.clipboardData, clipboardData);
84
+ wrote = true;
85
+ };
86
+ ownerDocument.addEventListener("copy", onCopy, true);
87
+ try {
88
+ return ownerDocument.execCommand("copy") && wrote;
89
+ } finally {
90
+ ownerDocument.removeEventListener("copy", onCopy, true);
91
+ }
106
92
  }
107
93
  function selectTopLevelNode(node, placement) {
108
- if ($isElementNode(node)) {
109
- if (placement === "end") {
110
- node.selectEnd();
111
- } else {
112
- node.selectStart();
113
- }
114
- return;
115
- }
116
- const selection = $createNodeSelection();
117
- selection.add(node.getKey());
118
- $setSelection(selection);
94
+ if ($isElementNode(node)) {
95
+ if (placement === "end") node.selectEnd();
96
+ else node.selectStart();
97
+ return;
98
+ }
99
+ const selection = $createNodeSelection();
100
+ selection.add(node.getKey());
101
+ $setSelection(selection);
119
102
  }
120
103
  function buildBlockClipboardData(editor, nodes) {
121
- const lexicalEditor = editor;
122
- const namespace = lexicalEditor._config.namespace;
123
- const serializedNodes = nodes.map(serializeNodeWithChildren);
124
- const html = getHtmlFromTopLevelNodes(editor, nodes);
125
- return {
126
- "application/x-lexical-editor": JSON.stringify({
127
- namespace,
128
- nodes: serializedNodes
129
- }),
130
- "text/html": html,
131
- "text/plain": nodes.map((node) => node.getTextContent()).join("\n\n")
132
- };
104
+ const namespace = editor._config.namespace;
105
+ const serializedNodes = nodes.map(serializeNodeWithChildren);
106
+ const html = getHtmlFromTopLevelNodes(editor, nodes);
107
+ return {
108
+ "application/x-lexical-editor": JSON.stringify({
109
+ namespace,
110
+ nodes: serializedNodes
111
+ }),
112
+ "text/html": html,
113
+ "text/plain": nodes.map((node) => node.getTextContent()).join("\n\n")
114
+ };
133
115
  }
134
116
  function removeTopLevelNodesAndRestoreSelection(nodes) {
135
- if (nodes.length === 0) return;
136
- const firstNode = nodes[0];
137
- const lastNode = nodes.at(-1);
138
- const previousSibling = firstNode.getPreviousSibling();
139
- const nextSibling = lastNode.getNextSibling();
140
- for (const node of nodes) {
141
- node.remove();
142
- }
143
- const root = $getRoot();
144
- if (root.getChildrenSize() === 0) {
145
- const paragraph = $createParagraphNode();
146
- root.append(paragraph);
147
- paragraph.selectStart();
148
- return;
149
- }
150
- if (nextSibling) {
151
- selectTopLevelNode(nextSibling, "start");
152
- return;
153
- }
154
- if (previousSibling) {
155
- selectTopLevelNode(previousSibling, "end");
156
- return;
157
- }
158
- const fallbackNode = root.getFirstChild();
159
- if (fallbackNode) {
160
- selectTopLevelNode(fallbackNode, "start");
161
- }
117
+ if (nodes.length === 0) return;
118
+ const firstNode = nodes[0];
119
+ const lastNode = nodes.at(-1);
120
+ const previousSibling = firstNode.getPreviousSibling();
121
+ const nextSibling = lastNode.getNextSibling();
122
+ for (const node of nodes) node.remove();
123
+ const root = $getRoot();
124
+ if (root.getChildrenSize() === 0) {
125
+ const paragraph = $createParagraphNode();
126
+ root.append(paragraph);
127
+ paragraph.selectStart();
128
+ return;
129
+ }
130
+ if (nextSibling) {
131
+ selectTopLevelNode(nextSibling, "start");
132
+ return;
133
+ }
134
+ if (previousSibling) {
135
+ selectTopLevelNode(previousSibling, "end");
136
+ return;
137
+ }
138
+ const fallbackNode = root.getFirstChild();
139
+ if (fallbackNode) selectTopLevelNode(fallbackNode, "start");
162
140
  }
163
141
  function removeTopLevelNodesAndCreatePasteTarget(nodes) {
164
- if (nodes.length === 0) return;
165
- const pasteTarget = $createParagraphNode();
166
- nodes[0].insertBefore(pasteTarget);
167
- for (const node of nodes) {
168
- node.remove();
169
- }
170
- pasteTarget.selectStart();
142
+ if (nodes.length === 0) return;
143
+ const pasteTarget = $createParagraphNode();
144
+ nodes[0].insertBefore(pasteTarget);
145
+ for (const node of nodes) node.remove();
146
+ pasteTarget.selectStart();
171
147
  }
172
148
  function getDataTransferFromPasteEvent(event) {
173
- if (!event || typeof event !== "object") return null;
174
- const pasteEvent = event;
175
- return pasteEvent.clipboardData ?? pasteEvent.dataTransfer ?? null;
149
+ if (!event || typeof event !== "object") return null;
150
+ const pasteEvent = event;
151
+ return pasteEvent.clipboardData ?? pasteEvent.dataTransfer ?? null;
176
152
  }
177
153
  function hasPasteableClipboardData(clipboardData) {
178
- if (clipboardData.files.length > 0) return true;
179
- return hasInsertableClipboardData(clipboardData);
154
+ if (clipboardData.files.length > 0) return true;
155
+ return hasInsertableClipboardData(clipboardData);
180
156
  }
181
157
  function hasInsertableClipboardData(clipboardData) {
182
- return PASTEABLE_CLIPBOARD_TYPES.some((type) => clipboardData.getData(type).length > 0);
158
+ return PASTEABLE_CLIPBOARD_TYPES.some((type) => clipboardData.getData(type).length > 0);
183
159
  }
184
160
  function isDataTransferOnlyPasteEvent(event) {
185
- if (!event || typeof event !== "object") return false;
186
- const pasteEvent = event;
187
- return !pasteEvent.clipboardData && Boolean(pasteEvent.dataTransfer);
161
+ if (!event || typeof event !== "object") return false;
162
+ const pasteEvent = event;
163
+ return !pasteEvent.clipboardData && Boolean(pasteEvent.dataTransfer);
188
164
  }
189
165
  function insertPlainText(text) {
190
- const selection = $getSelection();
191
- if (!selection) return false;
192
- if (!$isRangeSelection(selection)) {
193
- selection.insertRawText(text);
194
- return true;
195
- }
196
- const parts = text.split(/(\r?\n|\t)/);
197
- if (parts.at(-1) === "") parts.pop();
198
- for (const part of parts) {
199
- const currentSelection = $getSelection();
200
- if (!$isRangeSelection(currentSelection)) continue;
201
- if (part === "\n" || part === "\r\n") {
202
- currentSelection.insertParagraph();
203
- } else if (part === " ") {
204
- currentSelection.insertNodes([$createTabNode()]);
205
- } else {
206
- currentSelection.insertText(part);
207
- }
208
- }
209
- return true;
166
+ const selection = $getSelection();
167
+ if (!selection) return false;
168
+ if (!$isRangeSelection(selection)) {
169
+ selection.insertRawText(text);
170
+ return true;
171
+ }
172
+ const parts = text.split(/(\r?\n|\t)/);
173
+ if (parts.at(-1) === "") parts.pop();
174
+ for (const part of parts) {
175
+ const currentSelection = $getSelection();
176
+ if (!$isRangeSelection(currentSelection)) continue;
177
+ if (part === "\n" || part === "\r\n") currentSelection.insertParagraph();
178
+ else if (part === " ") currentSelection.insertNodes([$createTabNode()]);
179
+ else currentSelection.insertText(part);
180
+ }
181
+ return true;
210
182
  }
211
183
  function insertDataTransferForBlockSelectionPaste(editor, dataTransfer) {
212
- const lexicalString = dataTransfer.getData("application/x-lexical-editor");
213
- if (lexicalString) {
214
- try {
215
- const payload = JSON.parse(lexicalString);
216
- const lexicalEditor = editor;
217
- if (payload.namespace === lexicalEditor._config.namespace && Array.isArray(payload.nodes)) {
218
- const selection = $getSelection();
219
- if (!selection) return false;
220
- selection.insertNodes(payload.nodes.map($parseSerializedNode));
221
- return true;
222
- }
223
- } catch {
224
- }
225
- }
226
- const htmlString = dataTransfer.getData("text/html");
227
- const plainString = dataTransfer.getData("text/plain");
228
- if (htmlString && plainString !== htmlString) {
229
- try {
230
- const selection = $getSelection();
231
- if (!selection) return false;
232
- const dom = new DOMParser().parseFromString(htmlString, "text/html");
233
- selection.insertNodes($generateNodesFromDOM(editor, dom));
234
- return true;
235
- } catch {
236
- }
237
- }
238
- const text = plainString || dataTransfer.getData("text/uri-list");
239
- return text ? insertPlainText(text) : false;
184
+ const lexicalString = dataTransfer.getData("application/x-lexical-editor");
185
+ if (lexicalString) try {
186
+ const payload = JSON.parse(lexicalString);
187
+ const lexicalEditor = editor;
188
+ if (payload.namespace === lexicalEditor._config.namespace && Array.isArray(payload.nodes)) {
189
+ const selection = $getSelection();
190
+ if (!selection) return false;
191
+ selection.insertNodes(payload.nodes.map($parseSerializedNode));
192
+ return true;
193
+ }
194
+ } catch {}
195
+ const htmlString = dataTransfer.getData("text/html");
196
+ const plainString = dataTransfer.getData("text/plain");
197
+ if (htmlString && plainString !== htmlString) try {
198
+ const selection = $getSelection();
199
+ if (!selection) return false;
200
+ const dom = new DOMParser().parseFromString(htmlString, "text/html");
201
+ selection.insertNodes($generateNodesFromDOM(editor, dom));
202
+ return true;
203
+ } catch {}
204
+ const text = plainString || dataTransfer.getData("text/uri-list");
205
+ return text ? insertPlainText(text) : false;
240
206
  }
241
207
  function replaceTopLevelNodesWithDataTransfer(editor, nodes, dataTransfer) {
242
- if (nodes.length === 0 || !hasInsertableClipboardData(dataTransfer)) return false;
243
- removeTopLevelNodesAndCreatePasteTarget(nodes);
244
- return insertDataTransferForBlockSelectionPaste(editor, dataTransfer);
208
+ if (nodes.length === 0 || !hasInsertableClipboardData(dataTransfer)) return false;
209
+ removeTopLevelNodesAndCreatePasteTarget(nodes);
210
+ return insertDataTransferForBlockSelectionPaste(editor, dataTransfer);
245
211
  }
212
+ //#endregion
213
+ //#region src/useBlockSelection.ts
246
214
  function isPasteBeforeInputEvent(event) {
247
- return event.inputType === "insertFromPaste" || event.inputType === "insertFromPasteAsQuotation";
215
+ return event.inputType === "insertFromPaste" || event.inputType === "insertFromPasteAsQuotation";
248
216
  }
249
217
  function $getTopLevelKeys() {
250
- return $getRoot().getChildren().map((c) => c.getKey());
218
+ return $getRoot().getChildren().map((c) => c.getKey());
251
219
  }
252
220
  function getTopLevelKey(node) {
253
- let current = node;
254
- while (current && current.getParent() && current.getParent() !== $getRoot()) {
255
- current = current.getParent();
256
- }
257
- return current?.getParent() === $getRoot() ? current.getKey() : null;
221
+ let current = node;
222
+ while (current && current.getParent() && current.getParent() !== $getRoot()) current = current.getParent();
223
+ return current?.getParent() === $getRoot() ? current.getKey() : null;
258
224
  }
259
225
  function $selectBlockRange(anchorKey, focusKey) {
260
- const children = $getRoot().getChildren();
261
- const anchorIdx = children.findIndex((c) => c.getKey() === anchorKey);
262
- const focusIdx = children.findIndex((c) => c.getKey() === focusKey);
263
- if (anchorIdx === -1 || focusIdx === -1) return;
264
- const start = Math.min(anchorIdx, focusIdx);
265
- const end = Math.max(anchorIdx, focusIdx);
266
- const sel = $createNodeSelection();
267
- for (let i = start; i <= end; i++) {
268
- sel.add(children[i].getKey());
269
- }
270
- $setSelection(sel);
226
+ const children = $getRoot().getChildren();
227
+ const anchorIdx = children.findIndex((c) => c.getKey() === anchorKey);
228
+ const focusIdx = children.findIndex((c) => c.getKey() === focusKey);
229
+ if (anchorIdx === -1 || focusIdx === -1) return;
230
+ const start = Math.min(anchorIdx, focusIdx);
231
+ const end = Math.max(anchorIdx, focusIdx);
232
+ const sel = $createNodeSelection();
233
+ for (let i = start; i <= end; i++) sel.add(children[i].getKey());
234
+ $setSelection(sel);
271
235
  }
272
236
  function useBlockSelection(editor) {
273
- const anchorKeyRef = useRef(null);
274
- const focusKeyRef = useRef(null);
275
- const blockSelectionKeysRef = useRef(/* @__PURE__ */ new Set());
276
- const latestBlockClipboardDataRef = useRef(null);
277
- const clearBlockSelectionState = useCallback(() => {
278
- blockSelectionKeysRef.current = /* @__PURE__ */ new Set();
279
- anchorKeyRef.current = null;
280
- focusKeyRef.current = null;
281
- }, []);
282
- const getTopLevelNodesByKeys = useCallback((keys) => {
283
- if (keys.size === 0) return [];
284
- return $getRoot().getChildren().filter((node) => keys.has(node.getKey()));
285
- }, []);
286
- const getOwnedSelectionNodes = useCallback(() => {
287
- const ownedKeys = blockSelectionKeysRef.current;
288
- if (ownedKeys.size === 0) {
289
- const selection2 = $getSelection();
290
- if (!$isNodeSelection(selection2)) return [];
291
- const selectedNodes = selection2.getNodes();
292
- const topLevelFallbackNodes = selectedNodes.filter((node) => node.getParent() === $getRoot());
293
- if (topLevelFallbackNodes.length > 1 && topLevelFallbackNodes.length === selectedNodes.length) {
294
- return getTopLevelNodesByKeys(new Set(topLevelFallbackNodes.map((node) => node.getKey())));
295
- }
296
- return [];
297
- }
298
- const selection = $getSelection();
299
- if (!$isNodeSelection(selection)) {
300
- return getTopLevelNodesByKeys(ownedKeys);
301
- }
302
- return getTopLevelNodesByKeys(ownedKeys);
303
- }, [getTopLevelNodesByKeys]);
304
- const replaceBlockSelectionWithDataTransfer = useCallback(
305
- (dataTransfer) => {
306
- if (!hasInsertableClipboardData(dataTransfer)) {
307
- return false;
308
- }
309
- let handled = false;
310
- editor.update(
311
- () => {
312
- const nodes = getOwnedSelectionNodes();
313
- if (nodes.length === 0) {
314
- return;
315
- }
316
- handled = replaceTopLevelNodesWithDataTransfer(editor, nodes, dataTransfer);
317
- if (handled) {
318
- clearBlockSelectionState();
319
- }
320
- },
321
- { discrete: true, tag: PASTE_TAG }
322
- );
323
- return handled;
324
- },
325
- [clearBlockSelectionState, editor, getOwnedSelectionNodes]
326
- );
327
- const getCurrentBlockClipboardData = useCallback(() => {
328
- let clipboardData = null;
329
- editor.getEditorState().read(() => {
330
- const nodes = getOwnedSelectionNodes();
331
- if (nodes.length === 0) return;
332
- clipboardData = buildBlockClipboardData(editor, nodes);
333
- });
334
- return clipboardData;
335
- }, [editor, getOwnedSelectionNodes]);
336
- const deleteBlocksByKeys = useCallback(
337
- (keys) => {
338
- editor.update(
339
- () => {
340
- const nodes = getTopLevelNodesByKeys(new Set(keys));
341
- if (nodes.length === 0) return;
342
- removeTopLevelNodesAndRestoreSelection(nodes);
343
- clearBlockSelectionState();
344
- },
345
- { discrete: true }
346
- );
347
- },
348
- [clearBlockSelectionState, editor, getTopLevelNodesByKeys]
349
- );
350
- useEffect(() => {
351
- let prevKeys = /* @__PURE__ */ new Set();
352
- const unregister = editor.registerUpdateListener(({ editorState }) => {
353
- const rootEl = editor.getRootElement();
354
- if (!rootEl) return;
355
- const nextKeys = /* @__PURE__ */ new Set();
356
- let isNodeSel = false;
357
- const rangeTopLevelKeys = [];
358
- let topLevelKeys = [];
359
- editorState.read(() => {
360
- const sel = $getSelection();
361
- if ($isNodeSelection(sel)) {
362
- isNodeSel = true;
363
- for (const node of sel.getNodes()) {
364
- nextKeys.add(node.getKey());
365
- }
366
- } else if ($isRangeSelection(sel)) {
367
- const anchorTopLevelKey = getTopLevelKey(sel.anchor.getNode());
368
- const focusTopLevelKey = getTopLevelKey(sel.focus.getNode());
369
- if (anchorTopLevelKey) rangeTopLevelKeys.push(anchorTopLevelKey);
370
- if (focusTopLevelKey && focusTopLevelKey !== anchorTopLevelKey) {
371
- rangeTopLevelKeys.push(focusTopLevelKey);
372
- }
373
- }
374
- topLevelKeys = $getTopLevelKeys();
375
- });
376
- if (blockSelectionKeysRef.current.size > 0) {
377
- if (!isNodeSel) {
378
- const owned = blockSelectionKeysRef.current;
379
- const rangeStillInsideOwnedBlocks = rangeTopLevelKeys.length > 0 && rangeTopLevelKeys.every((key) => owned.has(key));
380
- if (!rangeStillInsideOwnedBlocks) {
381
- clearBlockSelectionState();
382
- }
383
- } else {
384
- const owned = blockSelectionKeysRef.current;
385
- const stillOwned = nextKeys.size === owned.size && [...nextKeys].every((k) => owned.has(k));
386
- if (!stillOwned) {
387
- const topLevelSet = new Set(topLevelKeys);
388
- const restoredTopLevel = [...nextKeys].filter((k) => topLevelSet.has(k));
389
- if (restoredTopLevel.length > 1) {
390
- const indices = restoredTopLevel.map((k) => topLevelKeys.indexOf(k)).sort((a, b) => a - b);
391
- anchorKeyRef.current = topLevelKeys[indices[0]];
392
- focusKeyRef.current = topLevelKeys[indices.at(-1)];
393
- blockSelectionKeysRef.current = new Set(restoredTopLevel);
394
- } else {
395
- clearBlockSelectionState();
396
- }
397
- }
398
- }
399
- }
400
- const topLevelKeySet = new Set(topLevelKeys);
401
- const highlightKeys = blockSelectionKeysRef.current.size > 0 ? new Set([...blockSelectionKeysRef.current].filter((key) => topLevelKeySet.has(key))) : /* @__PURE__ */ new Set();
402
- for (const key of prevKeys) {
403
- if (!highlightKeys.has(key)) {
404
- editor.getElementByKey(key)?.classList.remove(blockSelected);
405
- }
406
- }
407
- for (const key of highlightKeys) {
408
- if (!prevKeys.has(key)) {
409
- editor.getElementByKey(key)?.classList.add(blockSelected);
410
- }
411
- }
412
- prevKeys = highlightKeys;
413
- });
414
- return () => {
415
- unregister();
416
- for (const key of prevKeys) {
417
- editor.getElementByKey(key)?.classList.remove(blockSelected);
418
- }
419
- };
420
- }, [clearBlockSelectionState, editor]);
421
- useEffect(() => {
422
- const rootEl = editor.getRootElement();
423
- if (!rootEl) return;
424
- const onPointerDown = (event) => {
425
- if (blockSelectionKeysRef.current.size === 0) return;
426
- if (!(event.target instanceof Node) || !rootEl.contains(event.target)) return;
427
- clearBlockSelectionState();
428
- };
429
- rootEl.addEventListener("pointerdown", onPointerDown, true);
430
- return () => rootEl.removeEventListener("pointerdown", onPointerDown, true);
431
- }, [clearBlockSelectionState, editor]);
432
- useEffect(() => {
433
- const rootEl = editor.getRootElement();
434
- if (!rootEl) return;
435
- const onFocusIn = (e) => {
436
- if (blockSelectionKeysRef.current.size === 0) return;
437
- const target = e.target;
438
- if (!target) return;
439
- const nestedEditable = target.closest('[contenteditable="true"]');
440
- if (nestedEditable && nestedEditable !== rootEl) {
441
- clearBlockSelectionState();
442
- editor.update(() => {
443
- const sel = $getSelection();
444
- if ($isNodeSelection(sel)) {
445
- $setSelection(null);
446
- }
447
- });
448
- }
449
- };
450
- rootEl.addEventListener("focusin", onFocusIn);
451
- return () => rootEl.removeEventListener("focusin", onFocusIn);
452
- }, [clearBlockSelectionState, editor]);
453
- useEffect(() => {
454
- const rootEl = editor.getRootElement();
455
- if (!rootEl) return;
456
- const onBeforeInput = (event) => {
457
- if (!isPasteBeforeInputEvent(event)) return;
458
- if (blockSelectionKeysRef.current.size === 0 || !event.dataTransfer) return;
459
- const handled = replaceBlockSelectionWithDataTransfer(event.dataTransfer);
460
- if (!handled) return;
461
- event.preventDefault();
462
- event.stopPropagation();
463
- };
464
- rootEl.addEventListener("beforeinput", onBeforeInput, true);
465
- return () => rootEl.removeEventListener("beforeinput", onBeforeInput, true);
466
- }, [editor, replaceBlockSelectionWithDataTransfer]);
467
- useEffect(() => {
468
- const unregKeyDown = editor.registerCommand(
469
- KEY_DOWN_COMMAND,
470
- (event) => {
471
- const key = event.key.toLowerCase();
472
- if (!event.metaKey && !event.ctrlKey && key !== "escape") return false;
473
- if (!["a", "c", "v", "x", "escape"].includes(key)) return false;
474
- if ((event.metaKey || event.ctrlKey) && key === "v" && blockSelectionKeysRef.current.size > 0) {
475
- event.preventDefault();
476
- const cachedClipboardData = latestBlockClipboardDataRef.current ? { ...latestBlockClipboardDataRef.current } : null;
477
- void (async () => {
478
- const dataTransfer = cachedClipboardData ? createDataTransferFromBlockClipboardData(cachedClipboardData) : await readNativeClipboardDataTransfer();
479
- if (!dataTransfer) {
480
- return;
481
- }
482
- if (blockSelectionKeysRef.current.size === 0) {
483
- return;
484
- }
485
- replaceBlockSelectionWithDataTransfer(dataTransfer);
486
- })();
487
- return true;
488
- }
489
- return false;
490
- },
491
- COMMAND_PRIORITY_CRITICAL
492
- );
493
- const unregShiftDown = editor.registerCommand(
494
- KEY_ARROW_DOWN_COMMAND,
495
- (event) => {
496
- if (!event?.shiftKey) return false;
497
- if (blockSelectionKeysRef.current.size === 0 || !focusKeyRef.current) return false;
498
- const sel = $getSelection();
499
- if (!$isNodeSelection(sel)) return false;
500
- const children = $getRoot().getChildren();
501
- const focusIdx = children.findIndex((c) => c.getKey() === focusKeyRef.current);
502
- if (focusIdx === -1 || focusIdx >= children.length - 1) return false;
503
- event.preventDefault();
504
- focusKeyRef.current = children[focusIdx + 1].getKey();
505
- $selectBlockRange(anchorKeyRef.current, focusKeyRef.current);
506
- const start = Math.min(
507
- children.findIndex((c) => c.getKey() === anchorKeyRef.current),
508
- focusIdx + 1
509
- );
510
- const end = Math.max(
511
- children.findIndex((c) => c.getKey() === anchorKeyRef.current),
512
- focusIdx + 1
513
- );
514
- blockSelectionKeysRef.current = new Set(
515
- children.slice(start, end + 1).map((c) => c.getKey())
516
- );
517
- return true;
518
- },
519
- COMMAND_PRIORITY_CRITICAL
520
- );
521
- const unregShiftUp = editor.registerCommand(
522
- KEY_ARROW_UP_COMMAND,
523
- (event) => {
524
- if (!event?.shiftKey) return false;
525
- if (blockSelectionKeysRef.current.size === 0 || !focusKeyRef.current) return false;
526
- const sel = $getSelection();
527
- if (!$isNodeSelection(sel)) return false;
528
- const children = $getRoot().getChildren();
529
- const focusIdx = children.findIndex((c) => c.getKey() === focusKeyRef.current);
530
- if (focusIdx === -1 || focusIdx <= 0) return false;
531
- event.preventDefault();
532
- focusKeyRef.current = children[focusIdx - 1].getKey();
533
- $selectBlockRange(anchorKeyRef.current, focusKeyRef.current);
534
- const anchorIdx = children.findIndex((c) => c.getKey() === anchorKeyRef.current);
535
- const start = Math.min(anchorIdx, focusIdx - 1);
536
- const end = Math.max(anchorIdx, focusIdx - 1);
537
- blockSelectionKeysRef.current = new Set(
538
- children.slice(start, end + 1).map((c) => c.getKey())
539
- );
540
- return true;
541
- },
542
- COMMAND_PRIORITY_CRITICAL
543
- );
544
- const unregSelectAll = editor.registerCommand(
545
- SELECT_ALL_COMMAND,
546
- () => {
547
- if (blockSelectionKeysRef.current.size > 0) {
548
- const children = $getRoot().getChildren();
549
- const allKeys = children.map((c) => c.getKey());
550
- if (blockSelectionKeysRef.current.size >= allKeys.length) {
551
- return true;
552
- }
553
- anchorKeyRef.current = allKeys[0];
554
- focusKeyRef.current = allKeys.at(-1);
555
- blockSelectionKeysRef.current = new Set(allKeys);
556
- const nodeSel = $createNodeSelection();
557
- for (const key of allKeys) nodeSel.add(key);
558
- $setSelection(nodeSel);
559
- return true;
560
- }
561
- const sel = $getSelection();
562
- let topLevelKey = null;
563
- if ($isRangeSelection(sel)) {
564
- let node = sel.anchor.getNode();
565
- while (node.getParent() && node.getParent() !== $getRoot()) {
566
- node = node.getParent();
567
- }
568
- if (node.getParent() === $getRoot()) {
569
- topLevelKey = node.getKey();
570
- }
571
- } else if ($isNodeSelection(sel)) {
572
- const nodes = sel.getNodes();
573
- if (nodes.length > 0) {
574
- let node = nodes[0];
575
- while (node.getParent() && node.getParent() !== $getRoot()) {
576
- node = node.getParent();
577
- }
578
- if (node.getParent() === $getRoot()) {
579
- topLevelKey = node.getKey();
580
- }
581
- }
582
- }
583
- if (topLevelKey) {
584
- anchorKeyRef.current = topLevelKey;
585
- focusKeyRef.current = topLevelKey;
586
- blockSelectionKeysRef.current = /* @__PURE__ */ new Set([topLevelKey]);
587
- const nodeSel = $createNodeSelection();
588
- nodeSel.add(topLevelKey);
589
- $setSelection(nodeSel);
590
- return true;
591
- }
592
- return false;
593
- },
594
- COMMAND_PRIORITY_HIGH
595
- );
596
- const unregEscape = editor.registerCommand(
597
- KEY_ESCAPE_COMMAND,
598
- () => {
599
- if (blockSelectionKeysRef.current.size === 0) return false;
600
- clearBlockSelectionState();
601
- $setSelection(null);
602
- return true;
603
- },
604
- COMMAND_PRIORITY_HIGH
605
- );
606
- const unregCopy = editor.registerCommand(
607
- COPY_COMMAND,
608
- (event) => {
609
- const clipboardEvent = event && typeof event === "object" && "clipboardData" in event ? event : null;
610
- const clipboardData = getCurrentBlockClipboardData();
611
- if (!clipboardData) return false;
612
- latestBlockClipboardDataRef.current = clipboardData;
613
- if (clipboardEvent?.clipboardData) {
614
- clipboardEvent.preventDefault();
615
- setBlockClipboardDataTransfer(clipboardEvent.clipboardData, clipboardData);
616
- return true;
617
- }
618
- if (event && typeof event === "object" && "preventDefault" in event && typeof event.preventDefault === "function") {
619
- event.preventDefault();
620
- }
621
- writeBlockClipboardDataToNativeClipboard(editor, clipboardData);
622
- return true;
623
- },
624
- COMMAND_PRIORITY_CRITICAL
625
- );
626
- const unregCut = editor.registerCommand(
627
- CUT_COMMAND,
628
- (event) => {
629
- const clipboardEvent = event && typeof event === "object" && "clipboardData" in event ? event : null;
630
- let keysToDelete = [];
631
- let clipboardData = null;
632
- editor.getEditorState().read(() => {
633
- const nodes = getOwnedSelectionNodes();
634
- if (nodes.length === 0) return;
635
- clipboardData = buildBlockClipboardData(editor, nodes);
636
- keysToDelete = nodes.map((node) => node.getKey());
637
- });
638
- if (keysToDelete.length === 0 || !clipboardData) return false;
639
- latestBlockClipboardDataRef.current = clipboardData;
640
- if (clipboardEvent?.clipboardData) {
641
- clipboardEvent.preventDefault();
642
- setBlockClipboardDataTransfer(clipboardEvent.clipboardData, clipboardData);
643
- } else {
644
- if (event && typeof event === "object" && "preventDefault" in event && typeof event.preventDefault === "function") {
645
- event.preventDefault();
646
- }
647
- writeBlockClipboardDataToNativeClipboard(editor, clipboardData);
648
- }
649
- deleteBlocksByKeys(keysToDelete);
650
- return true;
651
- },
652
- COMMAND_PRIORITY_CRITICAL
653
- );
654
- const unregPaste = editor.registerCommand(
655
- PASTE_COMMAND,
656
- (event) => {
657
- const clipboardData = getDataTransferFromPasteEvent(event);
658
- if (!clipboardData) {
659
- return false;
660
- }
661
- const dataTransferOnly = isDataTransferOnlyPasteEvent(event);
662
- const hasPasteData = dataTransferOnly ? hasInsertableClipboardData(clipboardData) : hasPasteableClipboardData(clipboardData);
663
- if (!hasPasteData) {
664
- return false;
665
- }
666
- const nodes = getOwnedSelectionNodes();
667
- if (nodes.length === 0) {
668
- return false;
669
- }
670
- removeTopLevelNodesAndCreatePasteTarget(nodes);
671
- clearBlockSelectionState();
672
- if (dataTransferOnly) {
673
- if (event && typeof event === "object" && "preventDefault" in event && typeof event.preventDefault === "function") {
674
- event.preventDefault();
675
- }
676
- return insertDataTransferForBlockSelectionPaste(editor, clipboardData);
677
- }
678
- return false;
679
- },
680
- COMMAND_PRIORITY_CRITICAL
681
- );
682
- const handleDeleteBlocks = (payload) => {
683
- let keysToDelete = [];
684
- editor.getEditorState().read(() => {
685
- keysToDelete = getOwnedSelectionNodes().map((node) => node.getKey());
686
- });
687
- if (keysToDelete.length === 0) return false;
688
- if (payload && typeof payload !== "boolean" && "preventDefault" in payload && typeof payload.preventDefault === "function") {
689
- payload.preventDefault();
690
- }
691
- deleteBlocksByKeys(keysToDelete);
692
- return true;
693
- };
694
- const unregBackspace = editor.registerCommand(
695
- KEY_BACKSPACE_COMMAND,
696
- handleDeleteBlocks,
697
- COMMAND_PRIORITY_CRITICAL
698
- );
699
- const unregDelete = editor.registerCommand(
700
- KEY_DELETE_COMMAND,
701
- handleDeleteBlocks,
702
- COMMAND_PRIORITY_CRITICAL
703
- );
704
- const unregRemoveText = editor.registerCommand(
705
- REMOVE_TEXT_COMMAND,
706
- handleDeleteBlocks,
707
- COMMAND_PRIORITY_CRITICAL
708
- );
709
- const unregDeleteCharacter = editor.registerCommand(
710
- DELETE_CHARACTER_COMMAND,
711
- handleDeleteBlocks,
712
- COMMAND_PRIORITY_CRITICAL
713
- );
714
- return () => {
715
- unregKeyDown();
716
- unregShiftDown();
717
- unregShiftUp();
718
- unregSelectAll();
719
- unregEscape();
720
- unregCopy();
721
- unregCut();
722
- unregPaste();
723
- unregBackspace();
724
- unregDelete();
725
- unregRemoveText();
726
- unregDeleteCharacter();
727
- };
728
- }, [
729
- clearBlockSelectionState,
730
- deleteBlocksByKeys,
731
- editor,
732
- getCurrentBlockClipboardData,
733
- getOwnedSelectionNodes,
734
- replaceBlockSelectionWithDataTransfer
735
- ]);
736
- const selectBlock = useCallback(
737
- (nodeKey, shiftKey) => {
738
- editor.update(() => {
739
- if (shiftKey && anchorKeyRef.current) {
740
- focusKeyRef.current = nodeKey;
741
- $selectBlockRange(anchorKeyRef.current, nodeKey);
742
- const children = $getRoot().getChildren();
743
- const anchorIdx = children.findIndex((c) => c.getKey() === anchorKeyRef.current);
744
- const focusIdx = children.findIndex((c) => c.getKey() === nodeKey);
745
- if (anchorIdx !== -1 && focusIdx !== -1) {
746
- const start = Math.min(anchorIdx, focusIdx);
747
- const end = Math.max(anchorIdx, focusIdx);
748
- blockSelectionKeysRef.current = new Set(
749
- children.slice(start, end + 1).map((c) => c.getKey())
750
- );
751
- }
752
- } else {
753
- anchorKeyRef.current = nodeKey;
754
- focusKeyRef.current = nodeKey;
755
- blockSelectionKeysRef.current = /* @__PURE__ */ new Set([nodeKey]);
756
- const sel = $createNodeSelection();
757
- sel.add(nodeKey);
758
- $setSelection(sel);
759
- }
760
- });
761
- },
762
- [editor]
763
- );
764
- const getSelectedKeys = useCallback(() => {
765
- if (blockSelectionKeysRef.current.size === 0) return [];
766
- let keys = [];
767
- editor.getEditorState().read(() => {
768
- keys = getTopLevelNodesByKeys(blockSelectionKeysRef.current).map((node) => node.getKey());
769
- });
770
- return keys;
771
- }, [editor, getTopLevelNodesByKeys]);
772
- const deleteSelectedBlocks = useCallback(
773
- (fallbackNodeKey) => {
774
- const selectedKeys = getSelectedKeys();
775
- const keys = selectedKeys.length > 0 ? selectedKeys : fallbackNodeKey ? [fallbackNodeKey] : [];
776
- if (keys.length === 0) return;
777
- deleteBlocksByKeys(keys);
778
- },
779
- [deleteBlocksByKeys, getSelectedKeys]
780
- );
781
- const isBlockSelectionActive = useCallback(
782
- () => blockSelectionKeysRef.current.size > 0,
783
- []
784
- );
785
- return { selectBlock, getSelectedKeys, isBlockSelectionActive, deleteSelectedBlocks };
237
+ const anchorKeyRef = useRef(null);
238
+ const focusKeyRef = useRef(null);
239
+ const blockSelectionKeysRef = useRef(/* @__PURE__ */ new Set());
240
+ const latestBlockClipboardDataRef = useRef(null);
241
+ const clearBlockSelectionState = useCallback(() => {
242
+ blockSelectionKeysRef.current = /* @__PURE__ */ new Set();
243
+ anchorKeyRef.current = null;
244
+ focusKeyRef.current = null;
245
+ }, []);
246
+ const getTopLevelNodesByKeys = useCallback((keys) => {
247
+ if (keys.size === 0) return [];
248
+ return $getRoot().getChildren().filter((node) => keys.has(node.getKey()));
249
+ }, []);
250
+ const getOwnedSelectionNodes = useCallback(() => {
251
+ const ownedKeys = blockSelectionKeysRef.current;
252
+ if (ownedKeys.size === 0) {
253
+ const selection = $getSelection();
254
+ if (!$isNodeSelection(selection)) return [];
255
+ const selectedNodes = selection.getNodes();
256
+ const topLevelFallbackNodes = selectedNodes.filter((node) => node.getParent() === $getRoot());
257
+ if (topLevelFallbackNodes.length > 1 && topLevelFallbackNodes.length === selectedNodes.length) return getTopLevelNodesByKeys(new Set(topLevelFallbackNodes.map((node) => node.getKey())));
258
+ return [];
259
+ }
260
+ if (!$isNodeSelection($getSelection())) return getTopLevelNodesByKeys(ownedKeys);
261
+ return getTopLevelNodesByKeys(ownedKeys);
262
+ }, [getTopLevelNodesByKeys]);
263
+ const replaceBlockSelectionWithDataTransfer = useCallback((dataTransfer) => {
264
+ if (!hasInsertableClipboardData(dataTransfer)) return false;
265
+ let handled = false;
266
+ editor.update(() => {
267
+ const nodes = getOwnedSelectionNodes();
268
+ if (nodes.length === 0) return;
269
+ handled = replaceTopLevelNodesWithDataTransfer(editor, nodes, dataTransfer);
270
+ if (handled) clearBlockSelectionState();
271
+ }, {
272
+ discrete: true,
273
+ tag: PASTE_TAG
274
+ });
275
+ return handled;
276
+ }, [
277
+ clearBlockSelectionState,
278
+ editor,
279
+ getOwnedSelectionNodes
280
+ ]);
281
+ const getCurrentBlockClipboardData = useCallback(() => {
282
+ let clipboardData = null;
283
+ editor.getEditorState().read(() => {
284
+ const nodes = getOwnedSelectionNodes();
285
+ if (nodes.length === 0) return;
286
+ clipboardData = buildBlockClipboardData(editor, nodes);
287
+ });
288
+ return clipboardData;
289
+ }, [editor, getOwnedSelectionNodes]);
290
+ const deleteBlocksByKeys = useCallback((keys) => {
291
+ editor.update(() => {
292
+ const nodes = getTopLevelNodesByKeys(new Set(keys));
293
+ if (nodes.length === 0) return;
294
+ removeTopLevelNodesAndRestoreSelection(nodes);
295
+ clearBlockSelectionState();
296
+ }, { discrete: true });
297
+ }, [
298
+ clearBlockSelectionState,
299
+ editor,
300
+ getTopLevelNodesByKeys
301
+ ]);
302
+ useEffect(() => {
303
+ let prevKeys = /* @__PURE__ */ new Set();
304
+ const unregister = editor.registerUpdateListener(({ editorState }) => {
305
+ if (!editor.getRootElement()) return;
306
+ const nextKeys = /* @__PURE__ */ new Set();
307
+ let isNodeSel = false;
308
+ const rangeTopLevelKeys = [];
309
+ let topLevelKeys = [];
310
+ editorState.read(() => {
311
+ const sel = $getSelection();
312
+ if ($isNodeSelection(sel)) {
313
+ isNodeSel = true;
314
+ for (const node of sel.getNodes()) nextKeys.add(node.getKey());
315
+ } else if ($isRangeSelection(sel)) {
316
+ const anchorTopLevelKey = getTopLevelKey(sel.anchor.getNode());
317
+ const focusTopLevelKey = getTopLevelKey(sel.focus.getNode());
318
+ if (anchorTopLevelKey) rangeTopLevelKeys.push(anchorTopLevelKey);
319
+ if (focusTopLevelKey && focusTopLevelKey !== anchorTopLevelKey) rangeTopLevelKeys.push(focusTopLevelKey);
320
+ }
321
+ topLevelKeys = $getTopLevelKeys();
322
+ });
323
+ if (blockSelectionKeysRef.current.size > 0) if (!isNodeSel) {
324
+ const owned = blockSelectionKeysRef.current;
325
+ if (!(rangeTopLevelKeys.length > 0 && rangeTopLevelKeys.every((key) => owned.has(key)))) clearBlockSelectionState();
326
+ } else {
327
+ const owned = blockSelectionKeysRef.current;
328
+ if (!(nextKeys.size === owned.size && [...nextKeys].every((k) => owned.has(k)))) {
329
+ const topLevelSet = new Set(topLevelKeys);
330
+ const restoredTopLevel = [...nextKeys].filter((k) => topLevelSet.has(k));
331
+ if (restoredTopLevel.length > 1) {
332
+ const indices = restoredTopLevel.map((k) => topLevelKeys.indexOf(k)).sort((a, b) => a - b);
333
+ anchorKeyRef.current = topLevelKeys[indices[0]];
334
+ focusKeyRef.current = topLevelKeys[indices.at(-1)];
335
+ blockSelectionKeysRef.current = new Set(restoredTopLevel);
336
+ } else clearBlockSelectionState();
337
+ }
338
+ }
339
+ const topLevelKeySet = new Set(topLevelKeys);
340
+ const highlightKeys = blockSelectionKeysRef.current.size > 0 ? new Set([...blockSelectionKeysRef.current].filter((key) => topLevelKeySet.has(key))) : /* @__PURE__ */ new Set();
341
+ for (const key of prevKeys) if (!highlightKeys.has(key)) editor.getElementByKey(key)?.classList.remove(blockSelected);
342
+ for (const key of highlightKeys) if (!prevKeys.has(key)) editor.getElementByKey(key)?.classList.add(blockSelected);
343
+ prevKeys = highlightKeys;
344
+ });
345
+ return () => {
346
+ unregister();
347
+ for (const key of prevKeys) editor.getElementByKey(key)?.classList.remove(blockSelected);
348
+ };
349
+ }, [clearBlockSelectionState, editor]);
350
+ useEffect(() => {
351
+ const rootEl = editor.getRootElement();
352
+ if (!rootEl) return;
353
+ const onPointerDown = (event) => {
354
+ if (blockSelectionKeysRef.current.size === 0) return;
355
+ if (!(event.target instanceof Node) || !rootEl.contains(event.target)) return;
356
+ clearBlockSelectionState();
357
+ };
358
+ rootEl.addEventListener("pointerdown", onPointerDown, true);
359
+ return () => rootEl.removeEventListener("pointerdown", onPointerDown, true);
360
+ }, [clearBlockSelectionState, editor]);
361
+ useEffect(() => {
362
+ const rootEl = editor.getRootElement();
363
+ if (!rootEl) return;
364
+ const onFocusIn = (e) => {
365
+ if (blockSelectionKeysRef.current.size === 0) return;
366
+ const target = e.target;
367
+ if (!target) return;
368
+ const nestedEditable = target.closest("[contenteditable=\"true\"]");
369
+ if (nestedEditable && nestedEditable !== rootEl) {
370
+ clearBlockSelectionState();
371
+ editor.update(() => {
372
+ if ($isNodeSelection($getSelection())) $setSelection(null);
373
+ });
374
+ }
375
+ };
376
+ rootEl.addEventListener("focusin", onFocusIn);
377
+ return () => rootEl.removeEventListener("focusin", onFocusIn);
378
+ }, [clearBlockSelectionState, editor]);
379
+ useEffect(() => {
380
+ const rootEl = editor.getRootElement();
381
+ if (!rootEl) return;
382
+ const onBeforeInput = (event) => {
383
+ if (!isPasteBeforeInputEvent(event)) return;
384
+ if (blockSelectionKeysRef.current.size === 0 || !event.dataTransfer) return;
385
+ if (!replaceBlockSelectionWithDataTransfer(event.dataTransfer)) return;
386
+ event.preventDefault();
387
+ event.stopPropagation();
388
+ };
389
+ rootEl.addEventListener("beforeinput", onBeforeInput, true);
390
+ return () => rootEl.removeEventListener("beforeinput", onBeforeInput, true);
391
+ }, [editor, replaceBlockSelectionWithDataTransfer]);
392
+ useEffect(() => {
393
+ const unregKeyDown = editor.registerCommand(KEY_DOWN_COMMAND, (event) => {
394
+ const key = event.key.toLowerCase();
395
+ if (!event.metaKey && !event.ctrlKey && key !== "escape") return false;
396
+ if (![
397
+ "a",
398
+ "c",
399
+ "v",
400
+ "x",
401
+ "escape"
402
+ ].includes(key)) return false;
403
+ if ((event.metaKey || event.ctrlKey) && key === "v" && blockSelectionKeysRef.current.size > 0) {
404
+ event.preventDefault();
405
+ const cachedClipboardData = latestBlockClipboardDataRef.current ? { ...latestBlockClipboardDataRef.current } : null;
406
+ (async () => {
407
+ const dataTransfer = cachedClipboardData ? createDataTransferFromBlockClipboardData(cachedClipboardData) : await readNativeClipboardDataTransfer();
408
+ if (!dataTransfer) return;
409
+ if (blockSelectionKeysRef.current.size === 0) return;
410
+ replaceBlockSelectionWithDataTransfer(dataTransfer);
411
+ })();
412
+ return true;
413
+ }
414
+ return false;
415
+ }, COMMAND_PRIORITY_CRITICAL);
416
+ const unregShiftDown = editor.registerCommand(KEY_ARROW_DOWN_COMMAND, (event) => {
417
+ if (!event?.shiftKey) return false;
418
+ if (blockSelectionKeysRef.current.size === 0 || !focusKeyRef.current) return false;
419
+ if (!$isNodeSelection($getSelection())) return false;
420
+ const children = $getRoot().getChildren();
421
+ const focusIdx = children.findIndex((c) => c.getKey() === focusKeyRef.current);
422
+ if (focusIdx === -1 || focusIdx >= children.length - 1) return false;
423
+ event.preventDefault();
424
+ focusKeyRef.current = children[focusIdx + 1].getKey();
425
+ $selectBlockRange(anchorKeyRef.current, focusKeyRef.current);
426
+ const start = Math.min(children.findIndex((c) => c.getKey() === anchorKeyRef.current), focusIdx + 1);
427
+ const end = Math.max(children.findIndex((c) => c.getKey() === anchorKeyRef.current), focusIdx + 1);
428
+ blockSelectionKeysRef.current = new Set(children.slice(start, end + 1).map((c) => c.getKey()));
429
+ return true;
430
+ }, COMMAND_PRIORITY_CRITICAL);
431
+ const unregShiftUp = editor.registerCommand(KEY_ARROW_UP_COMMAND, (event) => {
432
+ if (!event?.shiftKey) return false;
433
+ if (blockSelectionKeysRef.current.size === 0 || !focusKeyRef.current) return false;
434
+ if (!$isNodeSelection($getSelection())) return false;
435
+ const children = $getRoot().getChildren();
436
+ const focusIdx = children.findIndex((c) => c.getKey() === focusKeyRef.current);
437
+ if (focusIdx === -1 || focusIdx <= 0) return false;
438
+ event.preventDefault();
439
+ focusKeyRef.current = children[focusIdx - 1].getKey();
440
+ $selectBlockRange(anchorKeyRef.current, focusKeyRef.current);
441
+ const anchorIdx = children.findIndex((c) => c.getKey() === anchorKeyRef.current);
442
+ const start = Math.min(anchorIdx, focusIdx - 1);
443
+ const end = Math.max(anchorIdx, focusIdx - 1);
444
+ blockSelectionKeysRef.current = new Set(children.slice(start, end + 1).map((c) => c.getKey()));
445
+ return true;
446
+ }, COMMAND_PRIORITY_CRITICAL);
447
+ const unregSelectAll = editor.registerCommand(SELECT_ALL_COMMAND, () => {
448
+ if (blockSelectionKeysRef.current.size > 0) {
449
+ const allKeys = $getRoot().getChildren().map((c) => c.getKey());
450
+ if (blockSelectionKeysRef.current.size >= allKeys.length) return true;
451
+ anchorKeyRef.current = allKeys[0];
452
+ focusKeyRef.current = allKeys.at(-1);
453
+ blockSelectionKeysRef.current = new Set(allKeys);
454
+ const nodeSel = $createNodeSelection();
455
+ for (const key of allKeys) nodeSel.add(key);
456
+ $setSelection(nodeSel);
457
+ return true;
458
+ }
459
+ const sel = $getSelection();
460
+ let topLevelKey = null;
461
+ if ($isRangeSelection(sel)) {
462
+ let node = sel.anchor.getNode();
463
+ while (node.getParent() && node.getParent() !== $getRoot()) node = node.getParent();
464
+ if (node.getParent() === $getRoot()) topLevelKey = node.getKey();
465
+ } else if ($isNodeSelection(sel)) {
466
+ const nodes = sel.getNodes();
467
+ if (nodes.length > 0) {
468
+ let node = nodes[0];
469
+ while (node.getParent() && node.getParent() !== $getRoot()) node = node.getParent();
470
+ if (node.getParent() === $getRoot()) topLevelKey = node.getKey();
471
+ }
472
+ }
473
+ if (topLevelKey) {
474
+ anchorKeyRef.current = topLevelKey;
475
+ focusKeyRef.current = topLevelKey;
476
+ blockSelectionKeysRef.current = new Set([topLevelKey]);
477
+ const nodeSel = $createNodeSelection();
478
+ nodeSel.add(topLevelKey);
479
+ $setSelection(nodeSel);
480
+ return true;
481
+ }
482
+ return false;
483
+ }, COMMAND_PRIORITY_HIGH);
484
+ const unregEscape = editor.registerCommand(KEY_ESCAPE_COMMAND, () => {
485
+ if (blockSelectionKeysRef.current.size === 0) return false;
486
+ clearBlockSelectionState();
487
+ $setSelection(null);
488
+ return true;
489
+ }, COMMAND_PRIORITY_HIGH);
490
+ const unregCopy = editor.registerCommand(COPY_COMMAND, (event) => {
491
+ const clipboardEvent = event && typeof event === "object" && "clipboardData" in event ? event : null;
492
+ const clipboardData = getCurrentBlockClipboardData();
493
+ if (!clipboardData) return false;
494
+ latestBlockClipboardDataRef.current = clipboardData;
495
+ if (clipboardEvent?.clipboardData) {
496
+ clipboardEvent.preventDefault();
497
+ setBlockClipboardDataTransfer(clipboardEvent.clipboardData, clipboardData);
498
+ return true;
499
+ }
500
+ if (event && typeof event === "object" && "preventDefault" in event && typeof event.preventDefault === "function") event.preventDefault();
501
+ writeBlockClipboardDataToNativeClipboard(editor, clipboardData);
502
+ return true;
503
+ }, COMMAND_PRIORITY_CRITICAL);
504
+ const unregCut = editor.registerCommand(CUT_COMMAND, (event) => {
505
+ const clipboardEvent = event && typeof event === "object" && "clipboardData" in event ? event : null;
506
+ let keysToDelete = [];
507
+ let clipboardData = null;
508
+ editor.getEditorState().read(() => {
509
+ const nodes = getOwnedSelectionNodes();
510
+ if (nodes.length === 0) return;
511
+ clipboardData = buildBlockClipboardData(editor, nodes);
512
+ keysToDelete = nodes.map((node) => node.getKey());
513
+ });
514
+ if (keysToDelete.length === 0 || !clipboardData) return false;
515
+ latestBlockClipboardDataRef.current = clipboardData;
516
+ if (clipboardEvent?.clipboardData) {
517
+ clipboardEvent.preventDefault();
518
+ setBlockClipboardDataTransfer(clipboardEvent.clipboardData, clipboardData);
519
+ } else {
520
+ if (event && typeof event === "object" && "preventDefault" in event && typeof event.preventDefault === "function") event.preventDefault();
521
+ writeBlockClipboardDataToNativeClipboard(editor, clipboardData);
522
+ }
523
+ deleteBlocksByKeys(keysToDelete);
524
+ return true;
525
+ }, COMMAND_PRIORITY_CRITICAL);
526
+ const unregPaste = editor.registerCommand(PASTE_COMMAND, (event) => {
527
+ const clipboardData = getDataTransferFromPasteEvent(event);
528
+ if (!clipboardData) return false;
529
+ const dataTransferOnly = isDataTransferOnlyPasteEvent(event);
530
+ if (!(dataTransferOnly ? hasInsertableClipboardData(clipboardData) : hasPasteableClipboardData(clipboardData))) return false;
531
+ const nodes = getOwnedSelectionNodes();
532
+ if (nodes.length === 0) return false;
533
+ removeTopLevelNodesAndCreatePasteTarget(nodes);
534
+ clearBlockSelectionState();
535
+ if (dataTransferOnly) {
536
+ if (event && typeof event === "object" && "preventDefault" in event && typeof event.preventDefault === "function") event.preventDefault();
537
+ return insertDataTransferForBlockSelectionPaste(editor, clipboardData);
538
+ }
539
+ return false;
540
+ }, COMMAND_PRIORITY_CRITICAL);
541
+ const handleDeleteBlocks = (payload) => {
542
+ let keysToDelete = [];
543
+ editor.getEditorState().read(() => {
544
+ keysToDelete = getOwnedSelectionNodes().map((node) => node.getKey());
545
+ });
546
+ if (keysToDelete.length === 0) return false;
547
+ if (payload && typeof payload !== "boolean" && "preventDefault" in payload && typeof payload.preventDefault === "function") payload.preventDefault();
548
+ deleteBlocksByKeys(keysToDelete);
549
+ return true;
550
+ };
551
+ const unregBackspace = editor.registerCommand(KEY_BACKSPACE_COMMAND, handleDeleteBlocks, COMMAND_PRIORITY_CRITICAL);
552
+ const unregDelete = editor.registerCommand(KEY_DELETE_COMMAND, handleDeleteBlocks, COMMAND_PRIORITY_CRITICAL);
553
+ const unregRemoveText = editor.registerCommand(REMOVE_TEXT_COMMAND, handleDeleteBlocks, COMMAND_PRIORITY_CRITICAL);
554
+ const unregDeleteCharacter = editor.registerCommand(DELETE_CHARACTER_COMMAND, handleDeleteBlocks, COMMAND_PRIORITY_CRITICAL);
555
+ return () => {
556
+ unregKeyDown();
557
+ unregShiftDown();
558
+ unregShiftUp();
559
+ unregSelectAll();
560
+ unregEscape();
561
+ unregCopy();
562
+ unregCut();
563
+ unregPaste();
564
+ unregBackspace();
565
+ unregDelete();
566
+ unregRemoveText();
567
+ unregDeleteCharacter();
568
+ };
569
+ }, [
570
+ clearBlockSelectionState,
571
+ deleteBlocksByKeys,
572
+ editor,
573
+ getCurrentBlockClipboardData,
574
+ getOwnedSelectionNodes,
575
+ replaceBlockSelectionWithDataTransfer
576
+ ]);
577
+ const selectBlock = useCallback((nodeKey, shiftKey) => {
578
+ editor.update(() => {
579
+ if (shiftKey && anchorKeyRef.current) {
580
+ focusKeyRef.current = nodeKey;
581
+ $selectBlockRange(anchorKeyRef.current, nodeKey);
582
+ const children = $getRoot().getChildren();
583
+ const anchorIdx = children.findIndex((c) => c.getKey() === anchorKeyRef.current);
584
+ const focusIdx = children.findIndex((c) => c.getKey() === nodeKey);
585
+ if (anchorIdx !== -1 && focusIdx !== -1) {
586
+ const start = Math.min(anchorIdx, focusIdx);
587
+ const end = Math.max(anchorIdx, focusIdx);
588
+ blockSelectionKeysRef.current = new Set(children.slice(start, end + 1).map((c) => c.getKey()));
589
+ }
590
+ } else {
591
+ anchorKeyRef.current = nodeKey;
592
+ focusKeyRef.current = nodeKey;
593
+ blockSelectionKeysRef.current = new Set([nodeKey]);
594
+ const sel = $createNodeSelection();
595
+ sel.add(nodeKey);
596
+ $setSelection(sel);
597
+ }
598
+ });
599
+ }, [editor]);
600
+ const getSelectedKeys = useCallback(() => {
601
+ if (blockSelectionKeysRef.current.size === 0) return [];
602
+ let keys = [];
603
+ editor.getEditorState().read(() => {
604
+ keys = getTopLevelNodesByKeys(blockSelectionKeysRef.current).map((node) => node.getKey());
605
+ });
606
+ return keys;
607
+ }, [editor, getTopLevelNodesByKeys]);
608
+ const deleteSelectedBlocks = useCallback((fallbackNodeKey) => {
609
+ const selectedKeys = getSelectedKeys();
610
+ const keys = selectedKeys.length > 0 ? selectedKeys : fallbackNodeKey ? [fallbackNodeKey] : [];
611
+ if (keys.length === 0) return;
612
+ deleteBlocksByKeys(keys);
613
+ }, [deleteBlocksByKeys, getSelectedKeys]);
614
+ return {
615
+ selectBlock,
616
+ getSelectedKeys,
617
+ isBlockSelectionActive: useCallback(() => blockSelectionKeysRef.current.size > 0, []),
618
+ deleteSelectedBlocks
619
+ };
786
620
  }
787
- const DRAG_DATA_KEY = "application/x-rich-editor-drag";
788
- const HIDE_DELAY = 300;
789
- const HANDLE_OFFSET = 52;
790
- const TURN_INTO_ITEMS = [
791
- { key: "paragraph", label: "Text", icon: Type },
792
- { key: "h1", label: "Heading 1", icon: Heading1 },
793
- { key: "h2", label: "Heading 2", icon: Heading2 },
794
- { key: "h3", label: "Heading 3", icon: Heading3 },
795
- { key: "bullet", label: "Bullet List", icon: List },
796
- { key: "numbered", label: "Numbered List", icon: ListOrdered },
797
- { key: "todo", label: "To-do", icon: ListChecks },
798
- { key: "quote", label: "Quote", icon: TextQuote },
799
- { key: "divider", label: "Divider", icon: Minus },
800
- { key: "code", label: "Code", icon: Code2 }
621
+ //#endregion
622
+ //#region src/BlockHandlePlugin.tsx
623
+ var DRAG_DATA_KEY = "application/x-rich-editor-drag";
624
+ var HIDE_DELAY = 300;
625
+ var HANDLE_OFFSET = 52;
626
+ var TURN_INTO_ITEMS = [
627
+ {
628
+ key: "paragraph",
629
+ label: "Text",
630
+ icon: Type
631
+ },
632
+ {
633
+ key: "h1",
634
+ label: "Heading 1",
635
+ icon: Heading1
636
+ },
637
+ {
638
+ key: "h2",
639
+ label: "Heading 2",
640
+ icon: Heading2
641
+ },
642
+ {
643
+ key: "h3",
644
+ label: "Heading 3",
645
+ icon: Heading3
646
+ },
647
+ {
648
+ key: "bullet",
649
+ label: "Bullet List",
650
+ icon: List
651
+ },
652
+ {
653
+ key: "numbered",
654
+ label: "Numbered List",
655
+ icon: ListOrdered
656
+ },
657
+ {
658
+ key: "todo",
659
+ label: "To-do",
660
+ icon: ListChecks
661
+ },
662
+ {
663
+ key: "quote",
664
+ label: "Quote",
665
+ icon: TextQuote
666
+ },
667
+ {
668
+ key: "divider",
669
+ label: "Divider",
670
+ icon: Minus
671
+ },
672
+ {
673
+ key: "code",
674
+ label: "Code",
675
+ icon: Code2
676
+ }
801
677
  ];
802
678
  function getBlockElement(editor, target) {
803
- const rootElement = editor.getRootElement();
804
- if (!rootElement) return null;
805
- let current = target;
806
- while (current && current !== rootElement) {
807
- if (current.parentElement === rootElement) return current;
808
- current = current.parentElement;
809
- }
810
- return null;
679
+ const rootElement = editor.getRootElement();
680
+ if (!rootElement) return null;
681
+ let current = target;
682
+ while (current && current !== rootElement) {
683
+ if (current.parentElement === rootElement) return current;
684
+ current = current.parentElement;
685
+ }
686
+ return null;
811
687
  }
812
688
  function getNearestBlockByY(rootElement, clientY) {
813
- const blocks = [...rootElement.children].filter(
814
- (child) => child instanceof HTMLElement
815
- );
816
- if (!blocks.length) return null;
817
- let nearestBlock = null;
818
- let nearestDistance = Number.POSITIVE_INFINITY;
819
- for (const block of blocks) {
820
- const rect = block.getBoundingClientRect();
821
- if (rect.height <= 0) continue;
822
- if (clientY >= rect.top && clientY <= rect.bottom) return block;
823
- const distance = clientY < rect.top ? rect.top - clientY : clientY - rect.bottom;
824
- if (distance < nearestDistance) {
825
- nearestDistance = distance;
826
- nearestBlock = block;
827
- }
828
- }
829
- return nearestBlock;
689
+ const blocks = [...rootElement.children].filter((child) => child instanceof HTMLElement);
690
+ if (!blocks.length) return null;
691
+ let nearestBlock = null;
692
+ let nearestDistance = Number.POSITIVE_INFINITY;
693
+ for (const block of blocks) {
694
+ const rect = block.getBoundingClientRect();
695
+ if (rect.height <= 0) continue;
696
+ if (clientY >= rect.top && clientY <= rect.bottom) return block;
697
+ const distance = clientY < rect.top ? rect.top - clientY : clientY - rect.bottom;
698
+ if (distance < nearestDistance) {
699
+ nearestDistance = distance;
700
+ nearestBlock = block;
701
+ }
702
+ }
703
+ return nearestBlock;
830
704
  }
831
705
  function getDropTargetBlock(editor, rootElement, event) {
832
- const rootRect = rootElement.getBoundingClientRect();
833
- if (rootRect.width <= 0 || rootRect.height <= 0) return null;
834
- if (event.clientY < rootRect.top || event.clientY > rootRect.bottom) {
835
- return null;
836
- }
837
- const points = [{ x: event.clientX, y: event.clientY }];
838
- const clampedX = Math.min(rootRect.right - 1, Math.max(rootRect.left + 1, event.clientX));
839
- if (clampedX !== event.clientX) {
840
- points.unshift({ x: clampedX, y: event.clientY });
841
- }
842
- for (const point of points) {
843
- const element = document.elementFromPoint(point.x, point.y);
844
- if (!(element instanceof HTMLElement)) continue;
845
- const block = getBlockElement(editor, element);
846
- if (block) return block;
847
- }
848
- const { target } = event;
849
- if (target instanceof HTMLElement) {
850
- const block = getBlockElement(editor, target);
851
- if (block) return block;
852
- }
853
- return getNearestBlockByY(rootElement, event.clientY);
706
+ const rootRect = rootElement.getBoundingClientRect();
707
+ if (rootRect.width <= 0 || rootRect.height <= 0) return null;
708
+ if (event.clientY < rootRect.top || event.clientY > rootRect.bottom) return null;
709
+ const points = [{
710
+ x: event.clientX,
711
+ y: event.clientY
712
+ }];
713
+ const clampedX = Math.min(rootRect.right - 1, Math.max(rootRect.left + 1, event.clientX));
714
+ if (clampedX !== event.clientX) points.unshift({
715
+ x: clampedX,
716
+ y: event.clientY
717
+ });
718
+ for (const point of points) {
719
+ const element = document.elementFromPoint(point.x, point.y);
720
+ if (!(element instanceof HTMLElement)) continue;
721
+ const block = getBlockElement(editor, element);
722
+ if (block) return block;
723
+ }
724
+ const { target } = event;
725
+ if (target instanceof HTMLElement) {
726
+ const block = getBlockElement(editor, target);
727
+ if (block) return block;
728
+ }
729
+ return getNearestBlockByY(rootElement, event.clientY);
854
730
  }
855
731
  function toPagePosition(rect) {
856
- return {
857
- top: rect.top + window.scrollY,
858
- left: rect.left + window.scrollX
859
- };
732
+ return {
733
+ top: rect.top + window.scrollY,
734
+ left: rect.left + window.scrollX
735
+ };
860
736
  }
861
737
  function $cloneNode(node) {
862
- const Klass = node.constructor;
863
- const serialized = node.exportJSON();
864
- const clone = Klass.importJSON(serialized);
865
- if ($isElementNode(node) && $isElementNode(clone)) {
866
- for (const child of node.getChildren()) {
867
- clone.append($cloneNode(child));
868
- }
869
- }
870
- return clone;
738
+ const Klass = node.constructor;
739
+ const serialized = node.exportJSON();
740
+ const clone = Klass.importJSON(serialized);
741
+ if ($isElementNode(node) && $isElementNode(clone)) for (const child of node.getChildren()) clone.append($cloneNode(child));
742
+ return clone;
871
743
  }
872
744
  function BlockHandleInner({ editor }) {
873
- const { className: portalClassName, theme } = usePortalTheme();
874
- const [handle, setHandle] = useState({
875
- visible: false,
876
- top: 0,
877
- left: 0,
878
- nodeKey: null
879
- });
880
- const [dropLine, setDropLine] = useState({
881
- visible: false,
882
- top: 0,
883
- left: 0,
884
- width: 0
885
- });
886
- const activeBlockRef = useRef(null);
887
- const hideTimerRef = useRef(null);
888
- const hoveringHandleRef = useRef(false);
889
- const menuOpenCountRef = useRef(0);
890
- const dragPreviewRef = useRef(null);
891
- const draggingBlockRef = useRef(null);
892
- const draggingBlockKeysRef = useRef(null);
893
- const { selectBlock, getSelectedKeys, deleteSelectedBlocks } = useBlockSelection(editor);
894
- const clearHideTimer = useCallback(() => {
895
- if (hideTimerRef.current) {
896
- clearTimeout(hideTimerRef.current);
897
- hideTimerRef.current = null;
898
- }
899
- }, []);
900
- const scheduleHide = useCallback(() => {
901
- clearHideTimer();
902
- hideTimerRef.current = setTimeout(() => {
903
- if (!hoveringHandleRef.current && menuOpenCountRef.current === 0) {
904
- activeBlockRef.current = null;
905
- setHandle((state) => ({ ...state, visible: false, nodeKey: null }));
906
- }
907
- }, HIDE_DELAY);
908
- }, [clearHideTimer]);
909
- const onHandleEnter = useCallback(() => {
910
- hoveringHandleRef.current = true;
911
- clearHideTimer();
912
- }, [clearHideTimer]);
913
- const onHandleLeave = useCallback(() => {
914
- hoveringHandleRef.current = false;
915
- scheduleHide();
916
- }, [scheduleHide]);
917
- const onMenuOpenChange = useCallback(
918
- (open) => {
919
- menuOpenCountRef.current += open ? 1 : -1;
920
- if (!open) scheduleHide();
921
- else clearHideTimer();
922
- },
923
- [clearHideTimer, scheduleHide]
924
- );
925
- const updatePositionFromBlock = useCallback(
926
- (block) => {
927
- if (block !== void 0) activeBlockRef.current = block;
928
- const element = activeBlockRef.current;
929
- if (!element || !element.isConnected) {
930
- activeBlockRef.current = null;
931
- setHandle((state) => state.visible ? { ...state, visible: false, nodeKey: null } : state);
932
- return;
933
- }
934
- const rootElement = editor.getRootElement();
935
- if (!rootElement) return;
936
- const blockRect = element.getBoundingClientRect();
937
- const rootRect = rootElement.getBoundingClientRect();
938
- const page = toPagePosition(blockRect);
939
- let nodeKey = null;
940
- editor.read(() => {
941
- const node = $getNearestNodeFromDOMNode(element);
942
- if (node) nodeKey = node.getKey();
943
- });
944
- setHandle({
945
- visible: true,
946
- top: page.top,
947
- left: toPagePosition(rootRect).left - HANDLE_OFFSET,
948
- nodeKey
949
- });
950
- },
951
- [editor]
952
- );
953
- useEffect(() => {
954
- const rootElement = editor.getRootElement();
955
- if (!rootElement) return;
956
- let rafId = null;
957
- const onMouseMove = (event) => {
958
- if (rafId !== null) return;
959
- rafId = requestAnimationFrame(() => {
960
- rafId = null;
961
- const target = event.target;
962
- const block = getBlockElement(editor, target);
963
- if (block) {
964
- clearHideTimer();
965
- updatePositionFromBlock(block);
966
- }
967
- });
968
- };
969
- const onMouseLeave = () => {
970
- if (!hoveringHandleRef.current && menuOpenCountRef.current === 0) {
971
- scheduleHide();
972
- }
973
- };
974
- rootElement.addEventListener("mousemove", onMouseMove);
975
- rootElement.addEventListener("mouseleave", onMouseLeave);
976
- return () => {
977
- if (rafId !== null) cancelAnimationFrame(rafId);
978
- rootElement.removeEventListener("mousemove", onMouseMove);
979
- rootElement.removeEventListener("mouseleave", onMouseLeave);
980
- clearHideTimer();
981
- };
982
- }, [clearHideTimer, editor, scheduleHide, updatePositionFromBlock]);
983
- useEffect(() => {
984
- const update = () => updatePositionFromBlock();
985
- window.addEventListener("scroll", update, true);
986
- window.addEventListener("resize", update);
987
- return () => {
988
- window.removeEventListener("scroll", update, true);
989
- window.removeEventListener("resize", update);
990
- };
991
- }, [updatePositionFromBlock]);
992
- useEffect(
993
- () => editor.registerUpdateListener(() => updatePositionFromBlock()),
994
- [editor, updatePositionFromBlock]
995
- );
996
- const handleAddBlock = useCallback(() => {
997
- if (!handle.nodeKey) return;
998
- editor.update(() => {
999
- const node = $getNodeByKey(handle.nodeKey);
1000
- if (!node) return;
1001
- const paragraph = $createParagraphNode();
1002
- node.insertAfter(paragraph);
1003
- paragraph.selectStart();
1004
- });
1005
- }, [editor, handle.nodeKey]);
1006
- const handleTurnInto = useCallback(
1007
- (type) => {
1008
- const { nodeKey } = handle;
1009
- if (!nodeKey) return;
1010
- if (["bullet", "numbered", "todo", "divider"].includes(type)) {
1011
- editor.update(() => {
1012
- const node = $getNodeByKey(nodeKey);
1013
- if (!node) return;
1014
- if ($isElementNode(node)) node.selectStart();
1015
- });
1016
- const commands = {
1017
- bullet: INSERT_UNORDERED_LIST_COMMAND,
1018
- numbered: INSERT_ORDERED_LIST_COMMAND,
1019
- todo: INSERT_CHECK_LIST_COMMAND,
1020
- divider: INSERT_HORIZONTAL_RULE_COMMAND
1021
- };
1022
- editor.dispatchCommand(commands[type], void 0);
1023
- return;
1024
- }
1025
- editor.update(() => {
1026
- const node = $getNodeByKey(nodeKey);
1027
- if (!node || !$isElementNode(node)) return;
1028
- node.selectStart();
1029
- const selection = $getSelection();
1030
- if (!$isRangeSelection(selection)) return;
1031
- const creators = {
1032
- paragraph: () => $createParagraphNode(),
1033
- h1: () => $createHeadingNode("h1"),
1034
- h2: () => $createHeadingNode("h2"),
1035
- h3: () => $createHeadingNode("h3"),
1036
- quote: () => $createQuoteNode(),
1037
- code: () => $createCodeNode()
1038
- };
1039
- const create = creators[type];
1040
- if (create) $setBlocksType(selection, create);
1041
- });
1042
- },
1043
- [editor, handle]
1044
- );
1045
- const handleDelete = useCallback(() => {
1046
- deleteSelectedBlocks(handle.nodeKey);
1047
- setHandle((state) => ({ ...state, visible: false, nodeKey: null }));
1048
- }, [deleteSelectedBlocks, handle.nodeKey]);
1049
- const handleDuplicate = useCallback(() => {
1050
- const selectedKeys = getSelectedKeys();
1051
- const keys = selectedKeys.length > 0 ? selectedKeys : handle.nodeKey ? [handle.nodeKey] : [];
1052
- if (!keys.length) return;
1053
- editor.update(() => {
1054
- const root = $getRoot();
1055
- const children = root.getChildren();
1056
- const keySet = new Set(keys);
1057
- const nodesToDuplicate = children.filter((child) => keySet.has(child.getKey()));
1058
- if (!nodesToDuplicate.length) return;
1059
- let insertAfter = nodesToDuplicate.at(-1);
1060
- for (const node of nodesToDuplicate) {
1061
- const clone = $cloneNode(node);
1062
- insertAfter.insertAfter(clone);
1063
- insertAfter = clone;
1064
- }
1065
- });
1066
- }, [editor, getSelectedKeys, handle.nodeKey]);
1067
- const handleMoveUp = useCallback(() => {
1068
- const selectedKeys = getSelectedKeys();
1069
- const keys = selectedKeys.length > 0 ? selectedKeys : handle.nodeKey ? [handle.nodeKey] : [];
1070
- if (!keys.length) return;
1071
- editor.update(() => {
1072
- const root = $getRoot();
1073
- const children = root.getChildren();
1074
- const keySet = new Set(keys);
1075
- const selectedNodes = children.filter((child) => keySet.has(child.getKey()));
1076
- if (!selectedNodes.length) return;
1077
- const firstSelected = selectedNodes[0];
1078
- const previousSibling = firstSelected.getPreviousSibling();
1079
- if (!previousSibling || keySet.has(previousSibling.getKey())) return;
1080
- const lastSelected = selectedNodes.at(-1);
1081
- previousSibling.remove();
1082
- lastSelected.insertAfter(previousSibling);
1083
- });
1084
- }, [editor, getSelectedKeys, handle.nodeKey]);
1085
- const handleMoveDown = useCallback(() => {
1086
- const selectedKeys = getSelectedKeys();
1087
- const keys = selectedKeys.length > 0 ? selectedKeys : handle.nodeKey ? [handle.nodeKey] : [];
1088
- if (!keys.length) return;
1089
- editor.update(() => {
1090
- const root = $getRoot();
1091
- const children = root.getChildren();
1092
- const keySet = new Set(keys);
1093
- const selectedNodes = children.filter((child) => keySet.has(child.getKey()));
1094
- if (!selectedNodes.length) return;
1095
- const lastSelected = selectedNodes.at(-1);
1096
- const nextSibling = lastSelected.getNextSibling();
1097
- if (!nextSibling || keySet.has(nextSibling.getKey())) return;
1098
- const firstSelected = selectedNodes[0];
1099
- nextSibling.remove();
1100
- firstSelected.insertBefore(nextSibling);
1101
- });
1102
- }, [editor, getSelectedKeys, handle.nodeKey]);
1103
- const [gripMenuOpen, setGripMenuOpen] = useState(false);
1104
- const dragStartedRef = useRef(false);
1105
- const clearDragVisualState = useCallback(() => {
1106
- const preview = dragPreviewRef.current;
1107
- if (preview) {
1108
- preview.remove();
1109
- dragPreviewRef.current = null;
1110
- }
1111
- const draggingBlock$1 = draggingBlockRef.current;
1112
- if (draggingBlock$1) {
1113
- draggingBlock$1.classList.remove(draggingBlock);
1114
- draggingBlockRef.current = null;
1115
- }
1116
- const draggingKeys = draggingBlockKeysRef.current;
1117
- if (draggingKeys) {
1118
- for (const key of draggingKeys) {
1119
- editor.getElementByKey(key)?.classList.remove(draggingBlock);
1120
- }
1121
- draggingBlockKeysRef.current = null;
1122
- }
1123
- }, [editor]);
1124
- const onGripDragStart = useCallback(
1125
- (event) => {
1126
- dragStartedRef.current = true;
1127
- if (!event.dataTransfer || !handle.nodeKey) return;
1128
- const selectedKeys = getSelectedKeys();
1129
- const dragKeys = selectedKeys.length > 0 && selectedKeys.includes(handle.nodeKey) ? selectedKeys : [handle.nodeKey];
1130
- event.dataTransfer.setData(DRAG_DATA_KEY, JSON.stringify(dragKeys));
1131
- event.dataTransfer.effectAllowed = "move";
1132
- const block = activeBlockRef.current;
1133
- if (!block) return;
1134
- clearDragVisualState();
1135
- const rect = block.getBoundingClientRect();
1136
- const preview = block.cloneNode(true);
1137
- preview.classList.add(dragPreview);
1138
- preview.style.width = `${rect.width}px`;
1139
- preview.style.position = "relative";
1140
- if (dragKeys.length > 1) {
1141
- const badge = document.createElement("div");
1142
- badge.className = dragCountBadge;
1143
- badge.textContent = String(dragKeys.length);
1144
- preview.appendChild(badge);
1145
- }
1146
- if (portalClassName) {
1147
- const wrapper = document.createElement("div");
1148
- wrapper.className = portalClassName;
1149
- wrapper.setAttribute("data-theme", theme);
1150
- wrapper.style.cssText = "position:fixed;top:-10000px;left:-10000px;pointer-events:none";
1151
- wrapper.appendChild(preview);
1152
- document.body.append(wrapper);
1153
- dragPreviewRef.current = wrapper;
1154
- } else {
1155
- document.body.append(preview);
1156
- dragPreviewRef.current = preview;
1157
- }
1158
- if (dragKeys.length > 1) {
1159
- for (const key of dragKeys) {
1160
- editor.getElementByKey(key)?.classList.add(draggingBlock);
1161
- }
1162
- draggingBlockKeysRef.current = dragKeys;
1163
- } else {
1164
- draggingBlockRef.current = block;
1165
- block.classList.add(draggingBlock);
1166
- }
1167
- const offsetX = Math.max(12, Math.min(rect.width - 12, event.clientX - rect.left));
1168
- const offsetY = Math.max(8, Math.min(rect.height - 8, event.clientY - rect.top));
1169
- event.dataTransfer.setDragImage(preview, offsetX, offsetY);
1170
- },
1171
- [clearDragVisualState, editor, getSelectedKeys, handle.nodeKey, portalClassName, theme]
1172
- );
1173
- const onGripOpenChange = useCallback(
1174
- (open) => {
1175
- setGripMenuOpen((previous) => {
1176
- if (previous === open) return previous;
1177
- onMenuOpenChange(open);
1178
- return open;
1179
- });
1180
- },
1181
- [onMenuOpenChange]
1182
- );
1183
- const onGripMouseDownCapture = useCallback((event) => {
1184
- dragStartedRef.current = false;
1185
- if (event.button === 0) event.stopPropagation();
1186
- }, []);
1187
- const onGripClick = useCallback(
1188
- (event) => {
1189
- if (event.detail === 0) return;
1190
- event.preventDefault();
1191
- event.stopPropagation();
1192
- if (dragStartedRef.current) {
1193
- dragStartedRef.current = false;
1194
- return;
1195
- }
1196
- if (!handle.nodeKey) return;
1197
- const { nodeKey } = handle;
1198
- const { shiftKey } = event;
1199
- editor.focus(() => selectBlock(nodeKey, shiftKey));
1200
- },
1201
- [editor, handle.nodeKey, selectBlock]
1202
- );
1203
- const onGripContextMenu = useCallback(
1204
- (event) => {
1205
- event.preventDefault();
1206
- if (handle.nodeKey) {
1207
- const currentKeys = getSelectedKeys();
1208
- if (!currentKeys.includes(handle.nodeKey)) {
1209
- selectBlock(handle.nodeKey, false);
1210
- }
1211
- }
1212
- onGripOpenChange(true);
1213
- },
1214
- [getSelectedKeys, handle.nodeKey, onGripOpenChange, selectBlock]
1215
- );
1216
- useEffect(() => {
1217
- const rootElement = editor.getRootElement();
1218
- if (!rootElement) return;
1219
- const unregDragOver = editor.registerCommand(
1220
- DRAGOVER_COMMAND,
1221
- (event) => {
1222
- if (!event.dataTransfer?.types.includes(DRAG_DATA_KEY)) return false;
1223
- event.preventDefault();
1224
- event.dataTransfer.dropEffect = "move";
1225
- const block = getDropTargetBlock(editor, rootElement, event);
1226
- if (!block) {
1227
- setDropLine((state) => state.visible ? { ...state, visible: false } : state);
1228
- return true;
1229
- }
1230
- const rect = block.getBoundingClientRect();
1231
- const rootRect = rootElement.getBoundingClientRect();
1232
- const midY = rect.top + rect.height / 2;
1233
- const y = event.clientY < midY ? rect.top : rect.bottom;
1234
- setDropLine({
1235
- visible: true,
1236
- top: y + window.scrollY,
1237
- left: rootRect.left + window.scrollX,
1238
- width: rootRect.width
1239
- });
1240
- return true;
1241
- },
1242
- COMMAND_PRIORITY_HIGH
1243
- );
1244
- const unregDrop = editor.registerCommand(
1245
- DROP_COMMAND,
1246
- (event) => {
1247
- const raw = event.dataTransfer?.getData(DRAG_DATA_KEY);
1248
- if (!raw) return false;
1249
- event.preventDefault();
1250
- setDropLine((state) => ({ ...state, visible: false }));
1251
- clearDragVisualState();
1252
- let draggedKeys;
1253
- try {
1254
- const parsed = JSON.parse(raw);
1255
- draggedKeys = Array.isArray(parsed) && parsed.every((key) => typeof key === "string") ? parsed : [raw];
1256
- } catch {
1257
- draggedKeys = [raw];
1258
- }
1259
- if (!draggedKeys.length) return false;
1260
- const block = getDropTargetBlock(editor, rootElement, event);
1261
- if (!block) return false;
1262
- editor.update(() => {
1263
- const targetNode = $getNearestNodeFromDOMNode(block);
1264
- if (!targetNode) return;
1265
- if (draggedKeys.includes(targetNode.getKey())) return;
1266
- const rect = block.getBoundingClientRect();
1267
- const insertBefore = event.clientY < rect.top + rect.height / 2;
1268
- const root = $getRoot();
1269
- const children = root.getChildren();
1270
- const keySet = new Set(draggedKeys);
1271
- const draggedNodes = children.filter((child) => keySet.has(child.getKey()));
1272
- for (const node of draggedNodes) {
1273
- node.remove();
1274
- }
1275
- const freshTarget = $getNodeByKey(targetNode.getKey());
1276
- if (!freshTarget) return;
1277
- if (insertBefore) {
1278
- for (let i = draggedNodes.length - 1; i >= 0; i--) {
1279
- freshTarget.insertBefore(draggedNodes[i]);
1280
- }
1281
- } else {
1282
- let cursor = freshTarget;
1283
- for (const node of draggedNodes) {
1284
- cursor.insertAfter(node);
1285
- cursor = node;
1286
- }
1287
- }
1288
- });
1289
- return true;
1290
- },
1291
- COMMAND_PRIORITY_HIGH
1292
- );
1293
- const unregDragStart = editor.registerCommand(
1294
- DRAGSTART_COMMAND,
1295
- (event) => {
1296
- if (!event.dataTransfer?.types.includes(DRAG_DATA_KEY)) return false;
1297
- return true;
1298
- },
1299
- COMMAND_PRIORITY_HIGH
1300
- );
1301
- const clearDropLine = () => {
1302
- setDropLine((state) => state.visible ? { ...state, visible: false } : state);
1303
- };
1304
- const clearDragState = () => {
1305
- clearDropLine();
1306
- clearDragVisualState();
1307
- };
1308
- const onDragLeave = (event) => {
1309
- if (event.relatedTarget === null || !rootElement.contains(event.relatedTarget)) {
1310
- clearDropLine();
1311
- }
1312
- };
1313
- window.addEventListener("dragend", clearDragState);
1314
- window.addEventListener("drop", clearDragState);
1315
- rootElement.addEventListener("dragleave", onDragLeave);
1316
- return () => {
1317
- unregDragOver();
1318
- unregDrop();
1319
- unregDragStart();
1320
- window.removeEventListener("dragend", clearDragState);
1321
- window.removeEventListener("drop", clearDragState);
1322
- rootElement.removeEventListener("dragleave", onDragLeave);
1323
- clearDragState();
1324
- };
1325
- }, [clearDragVisualState, editor]);
1326
- useEffect(() => {
1327
- const rootElement = editor.getRootElement();
1328
- if (!rootElement) return;
1329
- const outerContainer = rootElement.closest(".rich-editor");
1330
- if (!outerContainer) return;
1331
- let isDragging = false;
1332
- let lastKey = null;
1333
- const getBlockKeyAtY = (clientY) => {
1334
- const block = getNearestBlockByY(rootElement, clientY);
1335
- if (!block) return null;
1336
- let nodeKey = null;
1337
- editor.read(() => {
1338
- nodeKey = $getNearestNodeFromDOMNode(block)?.getKey() ?? null;
1339
- });
1340
- return nodeKey;
1341
- };
1342
- const onMouseDown = (event) => {
1343
- if (event.button !== 0) return;
1344
- const target = event.target;
1345
- if (rootElement.contains(target)) return;
1346
- const nodeKey = getBlockKeyAtY(event.clientY);
1347
- if (!nodeKey) return;
1348
- event.preventDefault();
1349
- editor.focus(() => selectBlock(nodeKey, event.shiftKey));
1350
- isDragging = true;
1351
- lastKey = nodeKey;
1352
- };
1353
- const onMouseMove = (event) => {
1354
- if (!isDragging) return;
1355
- const nodeKey = getBlockKeyAtY(event.clientY);
1356
- if (!nodeKey || nodeKey === lastKey) return;
1357
- selectBlock(nodeKey, true);
1358
- lastKey = nodeKey;
1359
- };
1360
- const onMouseUp = () => {
1361
- isDragging = false;
1362
- lastKey = null;
1363
- };
1364
- outerContainer.addEventListener("mousedown", onMouseDown);
1365
- window.addEventListener("mousemove", onMouseMove);
1366
- window.addEventListener("mouseup", onMouseUp);
1367
- return () => {
1368
- outerContainer.removeEventListener("mousedown", onMouseDown);
1369
- window.removeEventListener("mousemove", onMouseMove);
1370
- window.removeEventListener("mouseup", onMouseUp);
1371
- };
1372
- }, [editor, selectBlock]);
1373
- const themeWrapperProps = portalClassName ? {
1374
- "className": portalClassName,
1375
- "data-theme": theme,
1376
- "style": { display: "contents" }
1377
- } : {};
1378
- return /* @__PURE__ */ jsxs("div", { ...themeWrapperProps, children: [
1379
- /* @__PURE__ */ jsxs(
1380
- "div",
1381
- {
1382
- className: `${handleContainer} ${handle.visible ? handleContainerVisible : ""}`,
1383
- style: { top: handle.top, left: handle.left },
1384
- onMouseEnter: onHandleEnter,
1385
- onMouseLeave: onHandleLeave,
1386
- children: [
1387
- /* @__PURE__ */ jsx("button", { "aria-label": "Add block", className: handleBtn, onClick: handleAddBlock, children: /* @__PURE__ */ jsx(Plus, { size: 14 }) }),
1388
- /* @__PURE__ */ jsxs(DropdownMenu, { open: gripMenuOpen, onOpenChange: onGripOpenChange, children: [
1389
- /* @__PURE__ */ jsx(
1390
- DropdownMenuTrigger,
1391
- {
1392
- draggable: true,
1393
- "aria-label": "Block actions",
1394
- className: handleBtn,
1395
- onClick: onGripClick,
1396
- onContextMenu: onGripContextMenu,
1397
- onDragStart: onGripDragStart,
1398
- onMouseDownCapture: onGripMouseDownCapture,
1399
- children: /* @__PURE__ */ jsx(GripVertical, { size: 14 })
1400
- }
1401
- ),
1402
- /* @__PURE__ */ jsxs(DropdownMenuContent, { align: "start", side: "bottom", sideOffset: 4, children: [
1403
- /* @__PURE__ */ jsxs(DropdownMenuGroup, { children: [
1404
- /* @__PURE__ */ jsx(DropdownMenuLabel, { children: "TURN INTO" }),
1405
- TURN_INTO_ITEMS.map((item) => /* @__PURE__ */ jsxs(DropdownMenuItem, { onClick: () => handleTurnInto(item.key), children: [
1406
- /* @__PURE__ */ jsx(item.icon, {}),
1407
- item.label
1408
- ] }, item.key))
1409
- ] }),
1410
- /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
1411
- /* @__PURE__ */ jsxs(DropdownMenuGroup, { children: [
1412
- /* @__PURE__ */ jsx(DropdownMenuLabel, { children: "ACTIONS" }),
1413
- /* @__PURE__ */ jsxs(DropdownMenuItem, { onClick: handleDuplicate, children: [
1414
- /* @__PURE__ */ jsx(Copy, {}),
1415
- "Duplicate"
1416
- ] }),
1417
- /* @__PURE__ */ jsxs(DropdownMenuItem, { onClick: handleMoveUp, children: [
1418
- /* @__PURE__ */ jsx(ArrowUp, {}),
1419
- "Move Up"
1420
- ] }),
1421
- /* @__PURE__ */ jsxs(DropdownMenuItem, { onClick: handleMoveDown, children: [
1422
- /* @__PURE__ */ jsx(ArrowDown, {}),
1423
- "Move Down"
1424
- ] })
1425
- ] }),
1426
- /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
1427
- /* @__PURE__ */ jsxs(DropdownMenuItem, { className: menuItemDestructive, onClick: handleDelete, children: [
1428
- /* @__PURE__ */ jsx(Trash2, {}),
1429
- "Delete"
1430
- ] })
1431
- ] })
1432
- ] })
1433
- ]
1434
- }
1435
- ),
1436
- dropLine.visible && /* @__PURE__ */ jsx(
1437
- "div",
1438
- {
1439
- className: dropIndicator,
1440
- style: {
1441
- top: dropLine.top,
1442
- left: dropLine.left,
1443
- width: dropLine.width
1444
- }
1445
- }
1446
- )
1447
- ] });
745
+ const { className: portalClassName, theme } = usePortalTheme();
746
+ const [handle, setHandle] = useState({
747
+ visible: false,
748
+ top: 0,
749
+ left: 0,
750
+ nodeKey: null
751
+ });
752
+ const [dropLine, setDropLine] = useState({
753
+ visible: false,
754
+ top: 0,
755
+ left: 0,
756
+ width: 0
757
+ });
758
+ const activeBlockRef = useRef(null);
759
+ const hideTimerRef = useRef(null);
760
+ const hoveringHandleRef = useRef(false);
761
+ const menuOpenCountRef = useRef(0);
762
+ const dragPreviewRef = useRef(null);
763
+ const draggingBlockRef = useRef(null);
764
+ const draggingBlockKeysRef = useRef(null);
765
+ const { selectBlock, getSelectedKeys, deleteSelectedBlocks } = useBlockSelection(editor);
766
+ const clearHideTimer = useCallback(() => {
767
+ if (hideTimerRef.current) {
768
+ clearTimeout(hideTimerRef.current);
769
+ hideTimerRef.current = null;
770
+ }
771
+ }, []);
772
+ const scheduleHide = useCallback(() => {
773
+ clearHideTimer();
774
+ hideTimerRef.current = setTimeout(() => {
775
+ if (!hoveringHandleRef.current && menuOpenCountRef.current === 0) {
776
+ activeBlockRef.current = null;
777
+ setHandle((state) => ({
778
+ ...state,
779
+ visible: false,
780
+ nodeKey: null
781
+ }));
782
+ }
783
+ }, HIDE_DELAY);
784
+ }, [clearHideTimer]);
785
+ const onHandleEnter = useCallback(() => {
786
+ hoveringHandleRef.current = true;
787
+ clearHideTimer();
788
+ }, [clearHideTimer]);
789
+ const onHandleLeave = useCallback(() => {
790
+ hoveringHandleRef.current = false;
791
+ scheduleHide();
792
+ }, [scheduleHide]);
793
+ const onMenuOpenChange = useCallback((open) => {
794
+ menuOpenCountRef.current += open ? 1 : -1;
795
+ if (!open) scheduleHide();
796
+ else clearHideTimer();
797
+ }, [clearHideTimer, scheduleHide]);
798
+ const updatePositionFromBlock = useCallback((block) => {
799
+ if (block !== void 0) activeBlockRef.current = block;
800
+ const element = activeBlockRef.current;
801
+ if (!element || !element.isConnected) {
802
+ activeBlockRef.current = null;
803
+ setHandle((state) => state.visible ? {
804
+ ...state,
805
+ visible: false,
806
+ nodeKey: null
807
+ } : state);
808
+ return;
809
+ }
810
+ const rootElement = editor.getRootElement();
811
+ if (!rootElement) return;
812
+ const blockRect = element.getBoundingClientRect();
813
+ const rootRect = rootElement.getBoundingClientRect();
814
+ const page = toPagePosition(blockRect);
815
+ let nodeKey = null;
816
+ editor.read(() => {
817
+ const node = $getNearestNodeFromDOMNode(element);
818
+ if (node) nodeKey = node.getKey();
819
+ });
820
+ setHandle({
821
+ visible: true,
822
+ top: page.top,
823
+ left: toPagePosition(rootRect).left - HANDLE_OFFSET,
824
+ nodeKey
825
+ });
826
+ }, [editor]);
827
+ useEffect(() => {
828
+ const rootElement = editor.getRootElement();
829
+ if (!rootElement) return;
830
+ let rafId = null;
831
+ const onMouseMove = (event) => {
832
+ if (rafId !== null) return;
833
+ rafId = requestAnimationFrame(() => {
834
+ rafId = null;
835
+ const target = event.target;
836
+ const block = getBlockElement(editor, target);
837
+ if (block) {
838
+ clearHideTimer();
839
+ updatePositionFromBlock(block);
840
+ }
841
+ });
842
+ };
843
+ const onMouseLeave = () => {
844
+ if (!hoveringHandleRef.current && menuOpenCountRef.current === 0) scheduleHide();
845
+ };
846
+ rootElement.addEventListener("mousemove", onMouseMove);
847
+ rootElement.addEventListener("mouseleave", onMouseLeave);
848
+ return () => {
849
+ if (rafId !== null) cancelAnimationFrame(rafId);
850
+ rootElement.removeEventListener("mousemove", onMouseMove);
851
+ rootElement.removeEventListener("mouseleave", onMouseLeave);
852
+ clearHideTimer();
853
+ };
854
+ }, [
855
+ clearHideTimer,
856
+ editor,
857
+ scheduleHide,
858
+ updatePositionFromBlock
859
+ ]);
860
+ useEffect(() => {
861
+ const update = () => updatePositionFromBlock();
862
+ window.addEventListener("scroll", update, true);
863
+ window.addEventListener("resize", update);
864
+ return () => {
865
+ window.removeEventListener("scroll", update, true);
866
+ window.removeEventListener("resize", update);
867
+ };
868
+ }, [updatePositionFromBlock]);
869
+ useEffect(() => editor.registerUpdateListener(() => updatePositionFromBlock()), [editor, updatePositionFromBlock]);
870
+ const handleAddBlock = useCallback(() => {
871
+ if (!handle.nodeKey) return;
872
+ editor.update(() => {
873
+ const node = $getNodeByKey(handle.nodeKey);
874
+ if (!node) return;
875
+ const paragraph = $createParagraphNode();
876
+ node.insertAfter(paragraph);
877
+ paragraph.selectStart();
878
+ });
879
+ }, [editor, handle.nodeKey]);
880
+ const handleTurnInto = useCallback((type) => {
881
+ const { nodeKey } = handle;
882
+ if (!nodeKey) return;
883
+ if ([
884
+ "bullet",
885
+ "numbered",
886
+ "todo",
887
+ "divider"
888
+ ].includes(type)) {
889
+ editor.update(() => {
890
+ const node = $getNodeByKey(nodeKey);
891
+ if (!node) return;
892
+ if ($isElementNode(node)) node.selectStart();
893
+ });
894
+ const commands = {
895
+ bullet: INSERT_UNORDERED_LIST_COMMAND,
896
+ numbered: INSERT_ORDERED_LIST_COMMAND,
897
+ todo: INSERT_CHECK_LIST_COMMAND,
898
+ divider: INSERT_HORIZONTAL_RULE_COMMAND
899
+ };
900
+ editor.dispatchCommand(commands[type], void 0);
901
+ return;
902
+ }
903
+ editor.update(() => {
904
+ const node = $getNodeByKey(nodeKey);
905
+ if (!node || !$isElementNode(node)) return;
906
+ node.selectStart();
907
+ const selection = $getSelection();
908
+ if (!$isRangeSelection(selection)) return;
909
+ const create = {
910
+ paragraph: () => $createParagraphNode(),
911
+ h1: () => $createHeadingNode("h1"),
912
+ h2: () => $createHeadingNode("h2"),
913
+ h3: () => $createHeadingNode("h3"),
914
+ quote: () => $createQuoteNode(),
915
+ code: () => $createCodeNode()
916
+ }[type];
917
+ if (create) $setBlocksType(selection, create);
918
+ });
919
+ }, [editor, handle]);
920
+ const handleDelete = useCallback(() => {
921
+ deleteSelectedBlocks(handle.nodeKey);
922
+ setHandle((state) => ({
923
+ ...state,
924
+ visible: false,
925
+ nodeKey: null
926
+ }));
927
+ }, [deleteSelectedBlocks, handle.nodeKey]);
928
+ const handleDuplicate = useCallback(() => {
929
+ const selectedKeys = getSelectedKeys();
930
+ const keys = selectedKeys.length > 0 ? selectedKeys : handle.nodeKey ? [handle.nodeKey] : [];
931
+ if (!keys.length) return;
932
+ editor.update(() => {
933
+ const children = $getRoot().getChildren();
934
+ const keySet = new Set(keys);
935
+ const nodesToDuplicate = children.filter((child) => keySet.has(child.getKey()));
936
+ if (!nodesToDuplicate.length) return;
937
+ let insertAfter = nodesToDuplicate.at(-1);
938
+ for (const node of nodesToDuplicate) {
939
+ const clone = $cloneNode(node);
940
+ insertAfter.insertAfter(clone);
941
+ insertAfter = clone;
942
+ }
943
+ });
944
+ }, [
945
+ editor,
946
+ getSelectedKeys,
947
+ handle.nodeKey
948
+ ]);
949
+ const handleMoveUp = useCallback(() => {
950
+ const selectedKeys = getSelectedKeys();
951
+ const keys = selectedKeys.length > 0 ? selectedKeys : handle.nodeKey ? [handle.nodeKey] : [];
952
+ if (!keys.length) return;
953
+ editor.update(() => {
954
+ const children = $getRoot().getChildren();
955
+ const keySet = new Set(keys);
956
+ const selectedNodes = children.filter((child) => keySet.has(child.getKey()));
957
+ if (!selectedNodes.length) return;
958
+ const previousSibling = selectedNodes[0].getPreviousSibling();
959
+ if (!previousSibling || keySet.has(previousSibling.getKey())) return;
960
+ const lastSelected = selectedNodes.at(-1);
961
+ previousSibling.remove();
962
+ lastSelected.insertAfter(previousSibling);
963
+ });
964
+ }, [
965
+ editor,
966
+ getSelectedKeys,
967
+ handle.nodeKey
968
+ ]);
969
+ const handleMoveDown = useCallback(() => {
970
+ const selectedKeys = getSelectedKeys();
971
+ const keys = selectedKeys.length > 0 ? selectedKeys : handle.nodeKey ? [handle.nodeKey] : [];
972
+ if (!keys.length) return;
973
+ editor.update(() => {
974
+ const children = $getRoot().getChildren();
975
+ const keySet = new Set(keys);
976
+ const selectedNodes = children.filter((child) => keySet.has(child.getKey()));
977
+ if (!selectedNodes.length) return;
978
+ const nextSibling = selectedNodes.at(-1).getNextSibling();
979
+ if (!nextSibling || keySet.has(nextSibling.getKey())) return;
980
+ const firstSelected = selectedNodes[0];
981
+ nextSibling.remove();
982
+ firstSelected.insertBefore(nextSibling);
983
+ });
984
+ }, [
985
+ editor,
986
+ getSelectedKeys,
987
+ handle.nodeKey
988
+ ]);
989
+ const [gripMenuOpen, setGripMenuOpen] = useState(false);
990
+ const dragStartedRef = useRef(false);
991
+ const clearDragVisualState = useCallback(() => {
992
+ const preview = dragPreviewRef.current;
993
+ if (preview) {
994
+ preview.remove();
995
+ dragPreviewRef.current = null;
996
+ }
997
+ const draggingBlock$1 = draggingBlockRef.current;
998
+ if (draggingBlock$1) {
999
+ draggingBlock$1.classList.remove(draggingBlock);
1000
+ draggingBlockRef.current = null;
1001
+ }
1002
+ const draggingKeys = draggingBlockKeysRef.current;
1003
+ if (draggingKeys) {
1004
+ for (const key of draggingKeys) editor.getElementByKey(key)?.classList.remove(draggingBlock);
1005
+ draggingBlockKeysRef.current = null;
1006
+ }
1007
+ }, [editor]);
1008
+ const onGripDragStart = useCallback((event) => {
1009
+ dragStartedRef.current = true;
1010
+ if (!event.dataTransfer || !handle.nodeKey) return;
1011
+ const selectedKeys = getSelectedKeys();
1012
+ const dragKeys = selectedKeys.length > 0 && selectedKeys.includes(handle.nodeKey) ? selectedKeys : [handle.nodeKey];
1013
+ event.dataTransfer.setData(DRAG_DATA_KEY, JSON.stringify(dragKeys));
1014
+ event.dataTransfer.effectAllowed = "move";
1015
+ const block = activeBlockRef.current;
1016
+ if (!block) return;
1017
+ clearDragVisualState();
1018
+ const rect = block.getBoundingClientRect();
1019
+ const preview = block.cloneNode(true);
1020
+ preview.classList.add(dragPreview);
1021
+ preview.style.width = `${rect.width}px`;
1022
+ preview.style.position = "relative";
1023
+ if (dragKeys.length > 1) {
1024
+ const badge = document.createElement("div");
1025
+ badge.className = dragCountBadge;
1026
+ badge.textContent = String(dragKeys.length);
1027
+ preview.appendChild(badge);
1028
+ }
1029
+ if (portalClassName) {
1030
+ const wrapper = document.createElement("div");
1031
+ wrapper.className = portalClassName;
1032
+ wrapper.setAttribute("data-theme", theme);
1033
+ wrapper.style.cssText = "position:fixed;top:-10000px;left:-10000px;pointer-events:none";
1034
+ wrapper.appendChild(preview);
1035
+ document.body.append(wrapper);
1036
+ dragPreviewRef.current = wrapper;
1037
+ } else {
1038
+ document.body.append(preview);
1039
+ dragPreviewRef.current = preview;
1040
+ }
1041
+ if (dragKeys.length > 1) {
1042
+ for (const key of dragKeys) editor.getElementByKey(key)?.classList.add(draggingBlock);
1043
+ draggingBlockKeysRef.current = dragKeys;
1044
+ } else {
1045
+ draggingBlockRef.current = block;
1046
+ block.classList.add(draggingBlock);
1047
+ }
1048
+ const offsetX = Math.max(12, Math.min(rect.width - 12, event.clientX - rect.left));
1049
+ const offsetY = Math.max(8, Math.min(rect.height - 8, event.clientY - rect.top));
1050
+ event.dataTransfer.setDragImage(preview, offsetX, offsetY);
1051
+ }, [
1052
+ clearDragVisualState,
1053
+ editor,
1054
+ getSelectedKeys,
1055
+ handle.nodeKey,
1056
+ portalClassName,
1057
+ theme
1058
+ ]);
1059
+ const onGripOpenChange = useCallback((open) => {
1060
+ setGripMenuOpen((previous) => {
1061
+ if (previous === open) return previous;
1062
+ onMenuOpenChange(open);
1063
+ return open;
1064
+ });
1065
+ }, [onMenuOpenChange]);
1066
+ const onGripMouseDownCapture = useCallback((event) => {
1067
+ dragStartedRef.current = false;
1068
+ if (event.button === 0) event.stopPropagation();
1069
+ }, []);
1070
+ const onGripClick = useCallback((event) => {
1071
+ if (event.detail === 0) return;
1072
+ event.preventDefault();
1073
+ event.stopPropagation();
1074
+ if (dragStartedRef.current) {
1075
+ dragStartedRef.current = false;
1076
+ return;
1077
+ }
1078
+ if (!handle.nodeKey) return;
1079
+ const { nodeKey } = handle;
1080
+ const { shiftKey } = event;
1081
+ editor.focus(() => selectBlock(nodeKey, shiftKey));
1082
+ }, [
1083
+ editor,
1084
+ handle.nodeKey,
1085
+ selectBlock
1086
+ ]);
1087
+ const onGripContextMenu = useCallback((event) => {
1088
+ event.preventDefault();
1089
+ if (handle.nodeKey) {
1090
+ if (!getSelectedKeys().includes(handle.nodeKey)) selectBlock(handle.nodeKey, false);
1091
+ }
1092
+ onGripOpenChange(true);
1093
+ }, [
1094
+ getSelectedKeys,
1095
+ handle.nodeKey,
1096
+ onGripOpenChange,
1097
+ selectBlock
1098
+ ]);
1099
+ useEffect(() => {
1100
+ const rootElement = editor.getRootElement();
1101
+ if (!rootElement) return;
1102
+ const unregDragOver = editor.registerCommand(DRAGOVER_COMMAND, (event) => {
1103
+ if (!event.dataTransfer?.types.includes(DRAG_DATA_KEY)) return false;
1104
+ event.preventDefault();
1105
+ event.dataTransfer.dropEffect = "move";
1106
+ const block = getDropTargetBlock(editor, rootElement, event);
1107
+ if (!block) {
1108
+ setDropLine((state) => state.visible ? {
1109
+ ...state,
1110
+ visible: false
1111
+ } : state);
1112
+ return true;
1113
+ }
1114
+ const rect = block.getBoundingClientRect();
1115
+ const rootRect = rootElement.getBoundingClientRect();
1116
+ const midY = rect.top + rect.height / 2;
1117
+ setDropLine({
1118
+ visible: true,
1119
+ top: (event.clientY < midY ? rect.top : rect.bottom) + window.scrollY,
1120
+ left: rootRect.left + window.scrollX,
1121
+ width: rootRect.width
1122
+ });
1123
+ return true;
1124
+ }, COMMAND_PRIORITY_HIGH);
1125
+ const unregDrop = editor.registerCommand(DROP_COMMAND, (event) => {
1126
+ const raw = event.dataTransfer?.getData(DRAG_DATA_KEY);
1127
+ if (!raw) return false;
1128
+ event.preventDefault();
1129
+ setDropLine((state) => ({
1130
+ ...state,
1131
+ visible: false
1132
+ }));
1133
+ clearDragVisualState();
1134
+ let draggedKeys;
1135
+ try {
1136
+ const parsed = JSON.parse(raw);
1137
+ draggedKeys = Array.isArray(parsed) && parsed.every((key) => typeof key === "string") ? parsed : [raw];
1138
+ } catch {
1139
+ draggedKeys = [raw];
1140
+ }
1141
+ if (!draggedKeys.length) return false;
1142
+ const block = getDropTargetBlock(editor, rootElement, event);
1143
+ if (!block) return false;
1144
+ editor.update(() => {
1145
+ const targetNode = $getNearestNodeFromDOMNode(block);
1146
+ if (!targetNode) return;
1147
+ if (draggedKeys.includes(targetNode.getKey())) return;
1148
+ const rect = block.getBoundingClientRect();
1149
+ const insertBefore = event.clientY < rect.top + rect.height / 2;
1150
+ const children = $getRoot().getChildren();
1151
+ const keySet = new Set(draggedKeys);
1152
+ const draggedNodes = children.filter((child) => keySet.has(child.getKey()));
1153
+ for (const node of draggedNodes) node.remove();
1154
+ const freshTarget = $getNodeByKey(targetNode.getKey());
1155
+ if (!freshTarget) return;
1156
+ if (insertBefore) for (let i = draggedNodes.length - 1; i >= 0; i--) freshTarget.insertBefore(draggedNodes[i]);
1157
+ else {
1158
+ let cursor = freshTarget;
1159
+ for (const node of draggedNodes) {
1160
+ cursor.insertAfter(node);
1161
+ cursor = node;
1162
+ }
1163
+ }
1164
+ });
1165
+ return true;
1166
+ }, COMMAND_PRIORITY_HIGH);
1167
+ const unregDragStart = editor.registerCommand(DRAGSTART_COMMAND, (event) => {
1168
+ if (!event.dataTransfer?.types.includes(DRAG_DATA_KEY)) return false;
1169
+ return true;
1170
+ }, COMMAND_PRIORITY_HIGH);
1171
+ const clearDropLine = () => {
1172
+ setDropLine((state) => state.visible ? {
1173
+ ...state,
1174
+ visible: false
1175
+ } : state);
1176
+ };
1177
+ const clearDragState = () => {
1178
+ clearDropLine();
1179
+ clearDragVisualState();
1180
+ };
1181
+ const onDragLeave = (event) => {
1182
+ if (event.relatedTarget === null || !rootElement.contains(event.relatedTarget)) clearDropLine();
1183
+ };
1184
+ window.addEventListener("dragend", clearDragState);
1185
+ window.addEventListener("drop", clearDragState);
1186
+ rootElement.addEventListener("dragleave", onDragLeave);
1187
+ return () => {
1188
+ unregDragOver();
1189
+ unregDrop();
1190
+ unregDragStart();
1191
+ window.removeEventListener("dragend", clearDragState);
1192
+ window.removeEventListener("drop", clearDragState);
1193
+ rootElement.removeEventListener("dragleave", onDragLeave);
1194
+ clearDragState();
1195
+ };
1196
+ }, [clearDragVisualState, editor]);
1197
+ useEffect(() => {
1198
+ const rootElement = editor.getRootElement();
1199
+ if (!rootElement) return;
1200
+ const outerContainer = rootElement.closest(".rich-editor");
1201
+ if (!outerContainer) return;
1202
+ let isDragging = false;
1203
+ let lastKey = null;
1204
+ const getBlockKeyAtY = (clientY) => {
1205
+ const block = getNearestBlockByY(rootElement, clientY);
1206
+ if (!block) return null;
1207
+ let nodeKey = null;
1208
+ editor.read(() => {
1209
+ nodeKey = $getNearestNodeFromDOMNode(block)?.getKey() ?? null;
1210
+ });
1211
+ return nodeKey;
1212
+ };
1213
+ const onMouseDown = (event) => {
1214
+ if (event.button !== 0) return;
1215
+ const target = event.target;
1216
+ if (rootElement.contains(target)) return;
1217
+ const nodeKey = getBlockKeyAtY(event.clientY);
1218
+ if (!nodeKey) return;
1219
+ event.preventDefault();
1220
+ editor.focus(() => selectBlock(nodeKey, event.shiftKey));
1221
+ isDragging = true;
1222
+ lastKey = nodeKey;
1223
+ };
1224
+ const onMouseMove = (event) => {
1225
+ if (!isDragging) return;
1226
+ const nodeKey = getBlockKeyAtY(event.clientY);
1227
+ if (!nodeKey || nodeKey === lastKey) return;
1228
+ selectBlock(nodeKey, true);
1229
+ lastKey = nodeKey;
1230
+ };
1231
+ const onMouseUp = () => {
1232
+ isDragging = false;
1233
+ lastKey = null;
1234
+ };
1235
+ outerContainer.addEventListener("mousedown", onMouseDown);
1236
+ window.addEventListener("mousemove", onMouseMove);
1237
+ window.addEventListener("mouseup", onMouseUp);
1238
+ return () => {
1239
+ outerContainer.removeEventListener("mousedown", onMouseDown);
1240
+ window.removeEventListener("mousemove", onMouseMove);
1241
+ window.removeEventListener("mouseup", onMouseUp);
1242
+ };
1243
+ }, [editor, selectBlock]);
1244
+ return /* @__PURE__ */ jsxs("div", {
1245
+ ...portalClassName ? {
1246
+ "className": portalClassName,
1247
+ "data-theme": theme,
1248
+ "style": { display: "contents" }
1249
+ } : {},
1250
+ children: [/* @__PURE__ */ jsxs("div", {
1251
+ className: `${handleContainer} ${handle.visible ? handleContainerVisible : ""}`,
1252
+ style: {
1253
+ top: handle.top,
1254
+ left: handle.left
1255
+ },
1256
+ onMouseEnter: onHandleEnter,
1257
+ onMouseLeave: onHandleLeave,
1258
+ children: [/* @__PURE__ */ jsx("button", {
1259
+ "aria-label": "Add block",
1260
+ className: handleBtn,
1261
+ onClick: handleAddBlock,
1262
+ children: /* @__PURE__ */ jsx(Plus, { size: 14 })
1263
+ }), /* @__PURE__ */ jsxs(DropdownMenu, {
1264
+ open: gripMenuOpen,
1265
+ onOpenChange: onGripOpenChange,
1266
+ children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
1267
+ draggable: true,
1268
+ "aria-label": "Block actions",
1269
+ className: handleBtn,
1270
+ onClick: onGripClick,
1271
+ onContextMenu: onGripContextMenu,
1272
+ onDragStart: onGripDragStart,
1273
+ onMouseDownCapture: onGripMouseDownCapture,
1274
+ children: /* @__PURE__ */ jsx(GripVertical, { size: 14 })
1275
+ }), /* @__PURE__ */ jsxs(DropdownMenuContent, {
1276
+ align: "start",
1277
+ side: "bottom",
1278
+ sideOffset: 4,
1279
+ children: [
1280
+ /* @__PURE__ */ jsxs(DropdownMenuGroup, { children: [/* @__PURE__ */ jsx(DropdownMenuLabel, { children: "TURN INTO" }), TURN_INTO_ITEMS.map((item) => /* @__PURE__ */ jsxs(DropdownMenuItem, {
1281
+ onClick: () => handleTurnInto(item.key),
1282
+ children: [/* @__PURE__ */ jsx(item.icon, {}), item.label]
1283
+ }, item.key))] }),
1284
+ /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
1285
+ /* @__PURE__ */ jsxs(DropdownMenuGroup, { children: [
1286
+ /* @__PURE__ */ jsx(DropdownMenuLabel, { children: "ACTIONS" }),
1287
+ /* @__PURE__ */ jsxs(DropdownMenuItem, {
1288
+ onClick: handleDuplicate,
1289
+ children: [/* @__PURE__ */ jsx(Copy, {}), "Duplicate"]
1290
+ }),
1291
+ /* @__PURE__ */ jsxs(DropdownMenuItem, {
1292
+ onClick: handleMoveUp,
1293
+ children: [/* @__PURE__ */ jsx(ArrowUp, {}), "Move Up"]
1294
+ }),
1295
+ /* @__PURE__ */ jsxs(DropdownMenuItem, {
1296
+ onClick: handleMoveDown,
1297
+ children: [/* @__PURE__ */ jsx(ArrowDown, {}), "Move Down"]
1298
+ })
1299
+ ] }),
1300
+ /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
1301
+ /* @__PURE__ */ jsxs(DropdownMenuItem, {
1302
+ className: menuItemDestructive,
1303
+ onClick: handleDelete,
1304
+ children: [/* @__PURE__ */ jsx(Trash2, {}), "Delete"]
1305
+ })
1306
+ ]
1307
+ })]
1308
+ })]
1309
+ }), dropLine.visible && /* @__PURE__ */ jsx("div", {
1310
+ className: "iihqkc5",
1311
+ style: {
1312
+ top: dropLine.top,
1313
+ left: dropLine.left,
1314
+ width: dropLine.width
1315
+ }
1316
+ })]
1317
+ });
1448
1318
  }
1449
1319
  function BlockHandlePlugin() {
1450
- const [editor] = useLexicalComposerContext();
1451
- return createPortal(/* @__PURE__ */ jsx(BlockHandleInner, { editor }), document.body);
1320
+ const [editor] = useLexicalComposerContext();
1321
+ return createPortal(/* @__PURE__ */ jsx(BlockHandleInner, { editor }), document.body);
1452
1322
  }
1453
- export {
1454
- BlockHandlePlugin
1455
- };
1323
+ //#endregion
1324
+ export { BlockHandlePlugin };