@haklex/rich-plugin-block-handle 0.0.109 → 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;
|
|
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,
|
|
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,
|
|
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 { $
|
|
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
|
-
|
|
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)
|
|
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))
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
26
|
-
"@haklex/rich-style-token": "0.0.
|
|
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",
|