@haklex/rich-plugin-block-handle 0.0.108 → 0.0.110

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.
@@ -1 +1 @@
1
- {"version":3,"file":"BlockHandlePlugin.d.ts","sourceRoot":"","sources":["../src/BlockHandlePlugin.tsx"],"names":[],"mappings":"AAoDA,OAAO,KAAK,EAAgD,YAAY,EAAE,MAAM,OAAO,CAAC;AA+xBxF,wBAAgB,iBAAiB,IAAI,YAAY,CAGhD"}
1
+ {"version":3,"file":"BlockHandlePlugin.d.ts","sourceRoot":"","sources":["../src/BlockHandlePlugin.tsx"],"names":[],"mappings":"AAoDA,OAAO,KAAK,EAAgD,YAAY,EAAE,MAAM,OAAO,CAAC;AAgyBxF,wBAAgB,iBAAiB,IAAI,YAAY,CAGhD"}
@@ -1,4 +1,15 @@
1
1
  import { LexicalEditor, LexicalNode } from 'lexical';
2
+ export declare function setBlockClipboardDataTransfer(dataTransfer: Pick<DataTransfer, 'setData'>, clipboardData: Record<string, string>): void;
3
+ export declare function createDataTransferFromBlockClipboardData(clipboardData: Record<string, string>): DataTransfer;
4
+ export declare function readNativeClipboardDataTransfer(): Promise<DataTransfer | null>;
5
+ export declare function writeBlockClipboardDataToNativeClipboard(editor: LexicalEditor, clipboardData: Record<string, string>): boolean;
2
6
  export declare function buildBlockClipboardData(editor: LexicalEditor, nodes: readonly LexicalNode[]): Record<string, string>;
3
7
  export declare function removeTopLevelNodesAndRestoreSelection(nodes: readonly LexicalNode[]): void;
8
+ export declare function removeTopLevelNodesAndCreatePasteTarget(nodes: readonly LexicalNode[]): void;
9
+ export declare function getDataTransferFromPasteEvent(event: unknown): DataTransfer | null;
10
+ export declare function hasPasteableClipboardData(clipboardData: DataTransfer): boolean;
11
+ export declare function hasInsertableClipboardData(clipboardData: DataTransfer): boolean;
12
+ export declare function isDataTransferOnlyPasteEvent(event: unknown): boolean;
13
+ export declare function insertDataTransferForBlockSelectionPaste(editor: LexicalEditor, dataTransfer: DataTransfer): boolean;
14
+ export declare function replaceTopLevelNodesWithDataTransfer(editor: LexicalEditor, nodes: readonly LexicalNode[], dataTransfer: DataTransfer): boolean;
4
15
  //# sourceMappingURL=blockSelectionUtils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"blockSelectionUtils.d.ts","sourceRoot":"","sources":["../src/blockSelectionUtils.ts"],"names":[],"mappings":"AACA,OAAO,EAQL,KAAK,aAAa,EAClB,KAAK,WAAW,EACjB,MAAM,SAAS,CAAC;AA+BjB,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,SAAS,WAAW,EAAE,GAC5B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA2CxB;AAED,wBAAgB,sCAAsC,CAAC,KAAK,EAAE,SAAS,WAAW,EAAE,GAAG,IAAI,CAkC1F"}
1
+ {"version":3,"file":"blockSelectionUtils.d.ts","sourceRoot":"","sources":["../src/blockSelectionUtils.ts"],"names":[],"mappings":"AACA,OAAO,EAUL,KAAK,aAAa,EAClB,KAAK,WAAW,EACjB,MAAM,SAAS,CAAC;AA+BjB,wBAAgB,6BAA6B,CAC3C,YAAY,EAAE,IAAI,CAAC,YAAY,EAAE,SAAS,CAAC,EAC3C,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACpC,IAAI,CAIN;AAED,wBAAgB,wCAAwC,CACtD,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACpC,YAAY,CAad;AAED,wBAAsB,+BAA+B,IAAI,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAkCpF;AAED,wBAAgB,wCAAwC,CACtD,MAAM,EAAE,aAAa,EACrB,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACpC,OAAO,CAqBT;AAiBD,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,SAAS,WAAW,EAAE,GAC5B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAgBxB;AAED,wBAAgB,sCAAsC,CAAC,KAAK,EAAE,SAAS,WAAW,EAAE,GAAG,IAAI,CAkC1F;AAED,wBAAgB,uCAAuC,CAAC,KAAK,EAAE,SAAS,WAAW,EAAE,GAAG,IAAI,CAW3F;AAED,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY,GAAG,IAAI,CASjF;AAED,wBAAgB,yBAAyB,CAAC,aAAa,EAAE,YAAY,GAAG,OAAO,CAI9E;AAED,wBAAgB,0BAA0B,CAAC,aAAa,EAAE,YAAY,GAAG,OAAO,CAE/E;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CASpE;AA8BD,wBAAgB,wCAAwC,CACtD,MAAM,EAAE,aAAa,EACrB,YAAY,EAAE,YAAY,GACzB,OAAO,CA2CT;AAED,wBAAgB,oCAAoC,CAClD,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,SAAS,WAAW,EAAE,EAC7B,YAAY,EAAE,YAAY,GACzB,OAAO,CAKT"}
package/dist/index.mjs CHANGED
@@ -7,11 +7,11 @@ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext
7
7
  import { INSERT_HORIZONTAL_RULE_COMMAND } from "@lexical/react/LexicalHorizontalRuleNode";
8
8
  import { $createQuoteNode, $createHeadingNode } from "@lexical/rich-text";
9
9
  import { $setBlocksType } from "@lexical/selection";
10
- import { $getRoot, $createParagraphNode, createEditor, $parseSerializedNode, $isElementNode, $createNodeSelection, $setSelection, $getSelection, $isNodeSelection, KEY_ARROW_DOWN_COMMAND, COMMAND_PRIORITY_CRITICAL, KEY_ARROW_UP_COMMAND, SELECT_ALL_COMMAND, $isRangeSelection, COMMAND_PRIORITY_HIGH, KEY_ESCAPE_COMMAND, COPY_COMMAND, CUT_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DELETE_COMMAND, REMOVE_TEXT_COMMAND, DELETE_CHARACTER_COMMAND, $getNearestNodeFromDOMNode, $getNodeByKey, DRAGOVER_COMMAND, DROP_COMMAND, DRAGSTART_COMMAND } from "lexical";
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
11
  import { Plus, GripVertical, Type, Heading1, Heading2, Heading3, List, ListOrdered, ListChecks, TextQuote, Minus, Code2, Copy, ArrowUp, ArrowDown, Trash2 } from "lucide-react";
12
12
  import { useRef, useCallback, useEffect, useState } from "react";
13
13
  import { createPortal } from "react-dom";
14
- import { $generateHtmlFromNodes } from "@lexical/html";
14
+ import { $generateNodesFromDOM } from "@lexical/html";
15
15
  var handleContainer = "iihqkc0";
16
16
  var handleContainerVisible = "iihqkc1";
17
17
  var handleBtn = "iihqkc2";
@@ -21,6 +21,12 @@ 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"
29
+ ];
24
30
  function serializeNodeWithChildren(node) {
25
31
  const serialized = node.exportJSON();
26
32
  if ($isElementNode(node)) {
@@ -28,6 +34,76 @@ function serializeNodeWithChildren(node) {
28
34
  }
29
35
  return serialized;
30
36
  }
37
+ function getHtmlFromTopLevelNodes(editor, nodes) {
38
+ return nodes.map((node) => editor.getElementByKey(node.getKey())?.outerHTML ?? "").filter(Boolean).join("\n");
39
+ }
40
+ function setBlockClipboardDataTransfer(dataTransfer, clipboardData) {
41
+ for (const [mimeType, value] of Object.entries(clipboardData)) {
42
+ dataTransfer.setData(mimeType, value);
43
+ }
44
+ }
45
+ 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
+ };
58
+ }
59
+ 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;
87
+ }
88
+ 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
+ }
106
+ }
31
107
  function selectTopLevelNode(node, placement) {
32
108
  if ($isElementNode(node)) {
33
109
  if (placement === "end") {
@@ -45,31 +121,7 @@ function buildBlockClipboardData(editor, nodes) {
45
121
  const lexicalEditor = editor;
46
122
  const namespace = lexicalEditor._config.namespace;
47
123
  const serializedNodes = nodes.map(serializeNodeWithChildren);
48
- let html = "";
49
- try {
50
- const registeredNodeKlasses = [
51
- ...new Set([...lexicalEditor._nodes.values()].map((entry) => entry.klass))
52
- ];
53
- const tempEditor = createEditor({
54
- namespace: `${namespace}-block-selection-html`,
55
- nodes: registeredNodeKlasses,
56
- onError: (error) => {
57
- throw error;
58
- }
59
- });
60
- tempEditor.update(
61
- () => {
62
- const root = $getRoot();
63
- for (const serializedNode of serializedNodes) {
64
- root.append($parseSerializedNode(serializedNode));
65
- }
66
- html = $generateHtmlFromNodes(tempEditor, null);
67
- },
68
- { discrete: true }
69
- );
70
- } catch {
71
- html = "";
72
- }
124
+ const html = getHtmlFromTopLevelNodes(editor, nodes);
73
125
  return {
74
126
  "application/x-lexical-editor": JSON.stringify({
75
127
  namespace,
@@ -108,9 +160,102 @@ function removeTopLevelNodesAndRestoreSelection(nodes) {
108
160
  selectTopLevelNode(fallbackNode, "start");
109
161
  }
110
162
  }
163
+ 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();
171
+ }
172
+ function getDataTransferFromPasteEvent(event) {
173
+ if (!event || typeof event !== "object") return null;
174
+ const pasteEvent = event;
175
+ return pasteEvent.clipboardData ?? pasteEvent.dataTransfer ?? null;
176
+ }
177
+ function hasPasteableClipboardData(clipboardData) {
178
+ if (clipboardData.files.length > 0) return true;
179
+ return hasInsertableClipboardData(clipboardData);
180
+ }
181
+ function hasInsertableClipboardData(clipboardData) {
182
+ return PASTEABLE_CLIPBOARD_TYPES.some((type) => clipboardData.getData(type).length > 0);
183
+ }
184
+ function isDataTransferOnlyPasteEvent(event) {
185
+ if (!event || typeof event !== "object") return false;
186
+ const pasteEvent = event;
187
+ return !pasteEvent.clipboardData && Boolean(pasteEvent.dataTransfer);
188
+ }
189
+ 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;
210
+ }
211
+ 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;
240
+ }
241
+ function replaceTopLevelNodesWithDataTransfer(editor, nodes, dataTransfer) {
242
+ if (nodes.length === 0 || !hasInsertableClipboardData(dataTransfer)) return false;
243
+ removeTopLevelNodesAndCreatePasteTarget(nodes);
244
+ return insertDataTransferForBlockSelectionPaste(editor, dataTransfer);
245
+ }
246
+ function isPasteBeforeInputEvent(event) {
247
+ return event.inputType === "insertFromPaste" || event.inputType === "insertFromPasteAsQuotation";
248
+ }
111
249
  function $getTopLevelKeys() {
112
250
  return $getRoot().getChildren().map((c) => c.getKey());
113
251
  }
252
+ 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;
258
+ }
114
259
  function $selectBlockRange(anchorKey, focusKey) {
115
260
  const children = $getRoot().getChildren();
116
261
  const anchorIdx = children.findIndex((c) => c.getKey() === anchorKey);
@@ -128,6 +273,7 @@ function useBlockSelection(editor) {
128
273
  const anchorKeyRef = useRef(null);
129
274
  const focusKeyRef = useRef(null);
130
275
  const blockSelectionKeysRef = useRef(/* @__PURE__ */ new Set());
276
+ const latestBlockClipboardDataRef = useRef(null);
131
277
  const clearBlockSelectionState = useCallback(() => {
132
278
  blockSelectionKeysRef.current = /* @__PURE__ */ new Set();
133
279
  anchorKeyRef.current = null;
@@ -139,14 +285,54 @@ function useBlockSelection(editor) {
139
285
  }, []);
140
286
  const getOwnedSelectionNodes = useCallback(() => {
141
287
  const ownedKeys = blockSelectionKeysRef.current;
142
- if (ownedKeys.size === 0) return [];
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
+ }
143
298
  const selection = $getSelection();
144
- if (!$isNodeSelection(selection)) return [];
145
- const selectionKeys = new Set(selection.getNodes().map((node) => node.getKey()));
146
- const isOwnedSelection = selectionKeys.size === ownedKeys.size && [...selectionKeys].every((key) => ownedKeys.has(key));
147
- if (!isOwnedSelection) return [];
299
+ if (!$isNodeSelection(selection)) {
300
+ return getTopLevelNodesByKeys(ownedKeys);
301
+ }
148
302
  return getTopLevelNodesByKeys(ownedKeys);
149
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]);
150
336
  const deleteBlocksByKeys = useCallback(
151
337
  (keys) => {
152
338
  editor.update(
@@ -168,6 +354,7 @@ function useBlockSelection(editor) {
168
354
  if (!rootEl) return;
169
355
  const nextKeys = /* @__PURE__ */ new Set();
170
356
  let isNodeSel = false;
357
+ const rangeTopLevelKeys = [];
171
358
  let topLevelKeys = [];
172
359
  editorState.read(() => {
173
360
  const sel = $getSelection();
@@ -176,12 +363,23 @@ function useBlockSelection(editor) {
176
363
  for (const node of sel.getNodes()) {
177
364
  nextKeys.add(node.getKey());
178
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
+ }
179
373
  }
180
374
  topLevelKeys = $getTopLevelKeys();
181
375
  });
182
376
  if (blockSelectionKeysRef.current.size > 0) {
183
377
  if (!isNodeSel) {
184
- clearBlockSelectionState();
378
+ const owned = blockSelectionKeysRef.current;
379
+ const rangeStillInsideOwnedBlocks = rangeTopLevelKeys.length > 0 && rangeTopLevelKeys.every((key) => owned.has(key));
380
+ if (!rangeStillInsideOwnedBlocks) {
381
+ clearBlockSelectionState();
382
+ }
185
383
  } else {
186
384
  const owned = blockSelectionKeysRef.current;
187
385
  const stillOwned = nextKeys.size === owned.size && [...nextKeys].every((k) => owned.has(k));
@@ -199,7 +397,8 @@ function useBlockSelection(editor) {
199
397
  }
200
398
  }
201
399
  }
202
- const highlightKeys = blockSelectionKeysRef.current.size > 0 ? nextKeys : /* @__PURE__ */ new Set();
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();
203
402
  for (const key of prevKeys) {
204
403
  if (!highlightKeys.has(key)) {
205
404
  editor.getElementByKey(key)?.classList.remove(blockSelected);
@@ -219,6 +418,17 @@ function useBlockSelection(editor) {
219
418
  }
220
419
  };
221
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]);
222
432
  useEffect(() => {
223
433
  const rootEl = editor.getRootElement();
224
434
  if (!rootEl) return;
@@ -241,6 +451,45 @@ function useBlockSelection(editor) {
241
451
  return () => rootEl.removeEventListener("focusin", onFocusIn);
242
452
  }, [clearBlockSelectionState, editor]);
243
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
+ );
244
493
  const unregShiftDown = editor.registerCommand(
245
494
  KEY_ARROW_DOWN_COMMAND,
246
495
  (event) => {
@@ -357,43 +606,79 @@ function useBlockSelection(editor) {
357
606
  const unregCopy = editor.registerCommand(
358
607
  COPY_COMMAND,
359
608
  (event) => {
360
- if (!event || !("clipboardData" in event) || !event.clipboardData) return false;
361
- let handled = false;
362
- editor.getEditorState().read(() => {
363
- const nodes = getOwnedSelectionNodes();
364
- if (nodes.length === 0) return;
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") {
365
619
  event.preventDefault();
366
- const clipboardData = buildBlockClipboardData(editor, nodes);
367
- for (const [mimeType, value] of Object.entries(clipboardData)) {
368
- event.clipboardData?.setData(mimeType, value);
369
- }
370
- handled = true;
371
- });
372
- return handled;
620
+ }
621
+ writeBlockClipboardDataToNativeClipboard(editor, clipboardData);
622
+ return true;
373
623
  },
374
624
  COMMAND_PRIORITY_CRITICAL
375
625
  );
376
626
  const unregCut = editor.registerCommand(
377
627
  CUT_COMMAND,
378
628
  (event) => {
379
- if (!event || !("clipboardData" in event) || !event.clipboardData) return false;
629
+ const clipboardEvent = event && typeof event === "object" && "clipboardData" in event ? event : null;
380
630
  let keysToDelete = [];
631
+ let clipboardData = null;
381
632
  editor.getEditorState().read(() => {
382
633
  const nodes = getOwnedSelectionNodes();
383
634
  if (nodes.length === 0) return;
384
- event.preventDefault();
385
- const clipboardData = buildBlockClipboardData(editor, nodes);
386
- for (const [mimeType, value] of Object.entries(clipboardData)) {
387
- event.clipboardData?.setData(mimeType, value);
388
- }
635
+ clipboardData = buildBlockClipboardData(editor, nodes);
389
636
  keysToDelete = nodes.map((node) => node.getKey());
390
637
  });
391
- if (keysToDelete.length === 0) return false;
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
+ }
392
649
  deleteBlocksByKeys(keysToDelete);
393
650
  return true;
394
651
  },
395
652
  COMMAND_PRIORITY_CRITICAL
396
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
+ );
397
682
  const handleDeleteBlocks = (payload) => {
398
683
  let keysToDelete = [];
399
684
  editor.getEditorState().read(() => {
@@ -427,18 +712,27 @@ function useBlockSelection(editor) {
427
712
  COMMAND_PRIORITY_CRITICAL
428
713
  );
429
714
  return () => {
715
+ unregKeyDown();
430
716
  unregShiftDown();
431
717
  unregShiftUp();
432
718
  unregSelectAll();
433
719
  unregEscape();
434
720
  unregCopy();
435
721
  unregCut();
722
+ unregPaste();
436
723
  unregBackspace();
437
724
  unregDelete();
438
725
  unregRemoveText();
439
726
  unregDeleteCharacter();
440
727
  };
441
- }, [clearBlockSelectionState, deleteBlocksByKeys, editor, getOwnedSelectionNodes]);
728
+ }, [
729
+ clearBlockSelectionState,
730
+ deleteBlocksByKeys,
731
+ editor,
732
+ getCurrentBlockClipboardData,
733
+ getOwnedSelectionNodes,
734
+ replaceBlockSelectionWithDataTransfer
735
+ ]);
442
736
  const selectBlock = useCallback(
443
737
  (nodeKey, shiftKey) => {
444
738
  editor.update(() => {
@@ -900,9 +1194,11 @@ function BlockHandleInner({ editor }) {
900
1194
  return;
901
1195
  }
902
1196
  if (!handle.nodeKey) return;
903
- selectBlock(handle.nodeKey, event.shiftKey);
1197
+ const { nodeKey } = handle;
1198
+ const { shiftKey } = event;
1199
+ editor.focus(() => selectBlock(nodeKey, shiftKey));
904
1200
  },
905
- [handle.nodeKey, selectBlock]
1201
+ [editor, handle.nodeKey, selectBlock]
906
1202
  );
907
1203
  const onGripContextMenu = useCallback(
908
1204
  (event) => {
@@ -1050,8 +1346,7 @@ function BlockHandleInner({ editor }) {
1050
1346
  const nodeKey = getBlockKeyAtY(event.clientY);
1051
1347
  if (!nodeKey) return;
1052
1348
  event.preventDefault();
1053
- selectBlock(nodeKey, event.shiftKey);
1054
- editor.focus();
1349
+ editor.focus(() => selectBlock(nodeKey, event.shiftKey));
1055
1350
  isDragging = true;
1056
1351
  lastKey = nodeKey;
1057
1352
  };
@@ -1 +1 @@
1
- {"version":3,"file":"useBlockSelection.d.ts","sourceRoot":"","sources":["../src/useBlockSelection.ts"],"names":[],"mappings":"AAAA,OAAO,EAiBL,KAAK,aAAa,EAInB,MAAM,SAAS,CAAC;AA8BjB,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,aAAa;2BA4ZzC,MAAM,YAAY,OAAO;2BA6BG,MAAM,EAAE;kCAwB1C,OAAO;6CAZQ,MAAM,GAAG,IAAI;EAiBnC"}
1
+ {"version":3,"file":"useBlockSelection.d.ts","sourceRoot":"","sources":["../src/useBlockSelection.ts"],"names":[],"mappings":"AAAA,OAAO,EAkBL,KAAK,aAAa,EAMnB,MAAM,SAAS,CAAC;AAsDjB,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,aAAa;2BAsnBzC,MAAM,YAAY,OAAO;2BA6BG,MAAM,EAAE;kCAwB1C,OAAO;6CAZQ,MAAM,GAAG,IAAI;EAiBnC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haklex/rich-plugin-block-handle",
3
- "version": "0.0.108",
3
+ "version": "0.0.110",
4
4
  "description": "Block handle plugin with add button and context menu",
5
5
  "repository": {
6
6
  "type": "git",
@@ -22,8 +22,8 @@
22
22
  ],
23
23
  "dependencies": {
24
24
  "@lexical/code-core": "^0.43.0",
25
- "@haklex/rich-editor-ui": "0.0.108",
26
- "@haklex/rich-style-token": "0.0.108"
25
+ "@haklex/rich-editor-ui": "0.0.110",
26
+ "@haklex/rich-style-token": "0.0.110"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@lexical/html": "^0.43.0",