onchain-lexical-components 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/package.json +52 -0
- package/src/DraggableBlockPlugin/draggableBlockPlugin.tsx +665 -0
- package/src/DraggableBlockPlugin/index.module.less +48 -0
- package/src/DraggableBlockPlugin/index.tsx +91 -0
- package/src/DraggableBlockPlugin/shared/point.ts +55 -0
- package/src/DraggableBlockPlugin/shared/rect.ts +158 -0
- package/src/MarkdownShortcutPlugin/index.tsx +15 -0
- package/src/hooks/index.ts +8 -0
- package/src/hooks/useLatest/index.ts +18 -0
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# `@onchain/lexical-instance`
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "onchain-lexical-components",
|
|
3
|
+
"description": "This package provides Lexical components and hooks for React applications.",
|
|
4
|
+
"keywords": [
|
|
5
|
+
"react",
|
|
6
|
+
"lexical",
|
|
7
|
+
"editor",
|
|
8
|
+
"rich-text"
|
|
9
|
+
],
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"version": "0.0.1",
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@lexical/react": "^0.30.0",
|
|
14
|
+
"@lexical/rich-text": "^0.30.0",
|
|
15
|
+
"@lexical/utils": "^0.30.0",
|
|
16
|
+
"lexical": "0.30.0",
|
|
17
|
+
"onchain-lexical-instance": "^0.0.1",
|
|
18
|
+
"onchain-lexical-markdown": "^0.0.1",
|
|
19
|
+
"onchain-utility": "^0.0.1"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"react": ">=17.x",
|
|
23
|
+
"react-dom": ">=17.x"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/facebook/lexical",
|
|
28
|
+
"directory": "packages/lexical-react"
|
|
29
|
+
},
|
|
30
|
+
"exports": {
|
|
31
|
+
"./DraggableBlockPlugin": {
|
|
32
|
+
"import": {
|
|
33
|
+
"types": "./DraggableBlockPlugin.d.ts",
|
|
34
|
+
"default": "./dist/DraggableBlockPlugin.mjs"
|
|
35
|
+
},
|
|
36
|
+
"require": {
|
|
37
|
+
"types": "./DraggableBlockPlugin.d.ts",
|
|
38
|
+
"default": "./dist/DraggableBlockPlugin.js"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"./MarkdownShortcutPlugin": {
|
|
42
|
+
"import": {
|
|
43
|
+
"types": "./MarkdownShortcutPlugin.d.ts",
|
|
44
|
+
"default": "./dist/MarkdownShortcutPlugin.mjs"
|
|
45
|
+
},
|
|
46
|
+
"require": {
|
|
47
|
+
"types": "./MarkdownShortcutPlugin.d.ts",
|
|
48
|
+
"default": "./dist/MarkdownShortcutPlugin.js"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {JSX} from 'react';
|
|
10
|
+
|
|
11
|
+
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
|
12
|
+
import {eventFiles} from '@lexical/rich-text';
|
|
13
|
+
import {
|
|
14
|
+
$findMatchingParent,
|
|
15
|
+
calculateZoomLevel,
|
|
16
|
+
isHTMLElement,
|
|
17
|
+
mergeRegister,
|
|
18
|
+
} from '@lexical/utils';
|
|
19
|
+
import {
|
|
20
|
+
$getNearestNodeFromDOMNode,
|
|
21
|
+
$getNodeByKey,
|
|
22
|
+
$getRoot,
|
|
23
|
+
$isElementNode,
|
|
24
|
+
$isRootNode,
|
|
25
|
+
COMMAND_PRIORITY_HIGH,
|
|
26
|
+
COMMAND_PRIORITY_LOW,
|
|
27
|
+
DRAGOVER_COMMAND,
|
|
28
|
+
DROP_COMMAND,
|
|
29
|
+
ElementNode,
|
|
30
|
+
LexicalEditor,
|
|
31
|
+
LexicalNode,
|
|
32
|
+
} from 'lexical';
|
|
33
|
+
import {$isInstanceNode} from 'onchain-lexical-instance';
|
|
34
|
+
import {dfs} from 'onchain-utility/traversal';
|
|
35
|
+
import {
|
|
36
|
+
DragEvent as ReactDragEvent,
|
|
37
|
+
ReactNode,
|
|
38
|
+
useCallback,
|
|
39
|
+
useEffect,
|
|
40
|
+
useRef,
|
|
41
|
+
useState,
|
|
42
|
+
} from 'react';
|
|
43
|
+
import {createPortal} from 'react-dom';
|
|
44
|
+
|
|
45
|
+
import {useLatest} from '../hooks';
|
|
46
|
+
import {Point} from './shared/point';
|
|
47
|
+
import {Rectangle} from './shared/rect';
|
|
48
|
+
|
|
49
|
+
const SPACE = 4;
|
|
50
|
+
const TARGET_LINE_HALF_HEIGHT = 2;
|
|
51
|
+
const DRAG_DATA_FORMAT = 'application/x-lexical-drag-block';
|
|
52
|
+
const TEXT_BOX_HORIZONTAL_PADDING = 28;
|
|
53
|
+
const BOX_INDENT_INSERT = 60;
|
|
54
|
+
|
|
55
|
+
const Downward = 1;
|
|
56
|
+
const Upward = -1;
|
|
57
|
+
const Indeterminate = 0;
|
|
58
|
+
|
|
59
|
+
let prevIndex = Infinity;
|
|
60
|
+
|
|
61
|
+
function getCurrentIndex(keysLength: number): number {
|
|
62
|
+
if (keysLength === 0) {
|
|
63
|
+
return Infinity;
|
|
64
|
+
}
|
|
65
|
+
if (prevIndex >= 0 && prevIndex < keysLength) {
|
|
66
|
+
return prevIndex;
|
|
67
|
+
}
|
|
68
|
+
return Math.floor(keysLength / 2);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getTopLevelNodeKeys(editor: LexicalEditor): string[] {
|
|
72
|
+
return editor
|
|
73
|
+
.getEditorState()
|
|
74
|
+
.read(() =>
|
|
75
|
+
dfs($getRoot().getChildren<LexicalNode & ElementNode>(), (node) => {
|
|
76
|
+
if (node.getChildren) {
|
|
77
|
+
return node.getChildren().filter((node) => $isInstanceNode(node));
|
|
78
|
+
}
|
|
79
|
+
return [];
|
|
80
|
+
}),
|
|
81
|
+
)
|
|
82
|
+
.map((node) => {
|
|
83
|
+
return node.getKey();
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getCollapsedMargins(elem: HTMLElement): {
|
|
88
|
+
marginTop: number;
|
|
89
|
+
marginBottom: number;
|
|
90
|
+
} {
|
|
91
|
+
const getMargin = (
|
|
92
|
+
element: Element | null,
|
|
93
|
+
margin: 'marginTop' | 'marginBottom',
|
|
94
|
+
): number =>
|
|
95
|
+
element ? parseFloat(window.getComputedStyle(element)[margin]) : 0;
|
|
96
|
+
|
|
97
|
+
const {marginTop, marginBottom} = window.getComputedStyle(elem);
|
|
98
|
+
const prevElemSiblingMarginBottom = getMargin(
|
|
99
|
+
elem.previousElementSibling,
|
|
100
|
+
'marginBottom',
|
|
101
|
+
);
|
|
102
|
+
const nextElemSiblingMarginTop = getMargin(
|
|
103
|
+
elem.nextElementSibling,
|
|
104
|
+
'marginTop',
|
|
105
|
+
);
|
|
106
|
+
const collapsedTopMargin = Math.max(
|
|
107
|
+
parseFloat(marginTop),
|
|
108
|
+
prevElemSiblingMarginBottom,
|
|
109
|
+
);
|
|
110
|
+
const collapsedBottomMargin = Math.max(
|
|
111
|
+
parseFloat(marginBottom),
|
|
112
|
+
nextElemSiblingMarginTop,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
return {marginBottom: collapsedBottomMargin, marginTop: collapsedTopMargin};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function getBlockElement(
|
|
119
|
+
anchorElem: HTMLElement,
|
|
120
|
+
editor: LexicalEditor,
|
|
121
|
+
event: MouseEvent,
|
|
122
|
+
useEdgeAsDefault = false,
|
|
123
|
+
): HTMLElement | null {
|
|
124
|
+
const anchorElementRect = anchorElem.getBoundingClientRect();
|
|
125
|
+
const topLevelNodeKeys = getTopLevelNodeKeys(editor);
|
|
126
|
+
|
|
127
|
+
let blockElem: HTMLElement | null = null;
|
|
128
|
+
|
|
129
|
+
editor.getEditorState().read(() => {
|
|
130
|
+
if (useEdgeAsDefault) {
|
|
131
|
+
const [firstNode, lastNode] = [
|
|
132
|
+
editor.getElementByKey(topLevelNodeKeys[0]),
|
|
133
|
+
editor.getElementByKey(topLevelNodeKeys[topLevelNodeKeys.length - 1]),
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
const [firstNodeRect, lastNodeRect] = [
|
|
137
|
+
firstNode != null ? firstNode.getBoundingClientRect() : undefined,
|
|
138
|
+
lastNode != null ? lastNode.getBoundingClientRect() : undefined,
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
if (firstNodeRect && lastNodeRect) {
|
|
142
|
+
const firstNodeZoom = calculateZoomLevel(firstNode);
|
|
143
|
+
const lastNodeZoom = calculateZoomLevel(lastNode);
|
|
144
|
+
if (event.y / firstNodeZoom < firstNodeRect.top) {
|
|
145
|
+
blockElem = firstNode;
|
|
146
|
+
} else if (event.y / lastNodeZoom > lastNodeRect.bottom) {
|
|
147
|
+
blockElem = lastNode;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (blockElem) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// 直接获取第一个有instance属性的祖先元素 并返回
|
|
156
|
+
if (event.target instanceof Element) {
|
|
157
|
+
blockElem = event.target.closest(`[instance=true]`);
|
|
158
|
+
return blockElem;
|
|
159
|
+
}
|
|
160
|
+
let index = getCurrentIndex(topLevelNodeKeys.length);
|
|
161
|
+
let direction = Indeterminate;
|
|
162
|
+
while (index >= 0 && index < topLevelNodeKeys.length) {
|
|
163
|
+
const key = topLevelNodeKeys[index];
|
|
164
|
+
const elem = editor.getElementByKey(key);
|
|
165
|
+
if (elem === null) {
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
const zoom = calculateZoomLevel(elem);
|
|
169
|
+
|
|
170
|
+
const point = new Point(event.x / zoom, event.y / zoom);
|
|
171
|
+
const domRect = Rectangle.fromDOM(elem);
|
|
172
|
+
// console.log({direction, elem, index, zoom, point, domRect}, "elem")
|
|
173
|
+
const {marginTop, marginBottom} = getCollapsedMargins(elem);
|
|
174
|
+
const rect = domRect.generateNewRect({
|
|
175
|
+
bottom: domRect.bottom + marginBottom,
|
|
176
|
+
left: anchorElementRect.left,
|
|
177
|
+
right: anchorElementRect.right,
|
|
178
|
+
top: domRect.top - marginTop,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const {
|
|
182
|
+
result,
|
|
183
|
+
reason: {isOnTopSide, isOnBottomSide},
|
|
184
|
+
} = rect.contains(point);
|
|
185
|
+
if (result) {
|
|
186
|
+
blockElem = elem;
|
|
187
|
+
prevIndex = index;
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
if (direction === Indeterminate) {
|
|
191
|
+
if (isOnTopSide) {
|
|
192
|
+
direction = Upward;
|
|
193
|
+
} else if (isOnBottomSide) {
|
|
194
|
+
direction = Downward;
|
|
195
|
+
} else {
|
|
196
|
+
// stop search block element
|
|
197
|
+
direction = Infinity;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
index += direction;
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
return blockElem;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function setMenuPosition(
|
|
208
|
+
targetElem: HTMLElement | null,
|
|
209
|
+
floatingElem: HTMLElement,
|
|
210
|
+
anchorElem: HTMLElement,
|
|
211
|
+
) {
|
|
212
|
+
if (!targetElem) {
|
|
213
|
+
floatingElem.style.opacity = '0';
|
|
214
|
+
floatingElem.style.transform = 'translate(-10000px, -10000px)';
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const targetRect = targetElem.getBoundingClientRect();
|
|
219
|
+
const targetStyle = window.getComputedStyle(targetElem);
|
|
220
|
+
// const floatingElemRect = floatingElem.getBoundingClientRect();
|
|
221
|
+
const anchorElementRect = anchorElem.getBoundingClientRect();
|
|
222
|
+
|
|
223
|
+
// top left
|
|
224
|
+
let targetCalculateHeight: number = parseInt(targetStyle.lineHeight, 10);
|
|
225
|
+
if (isNaN(targetCalculateHeight)) {
|
|
226
|
+
// middle
|
|
227
|
+
targetCalculateHeight = targetRect.bottom - targetRect.top;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// [源码修改]
|
|
231
|
+
// const top =
|
|
232
|
+
// targetRect.top +
|
|
233
|
+
// (targetCalculateHeight - floatingElemRect.height) / 2 -
|
|
234
|
+
// anchorElementRect.top;
|
|
235
|
+
const top = targetRect.top - anchorElementRect.top;
|
|
236
|
+
|
|
237
|
+
const left = SPACE;
|
|
238
|
+
|
|
239
|
+
floatingElem.style.opacity = '1';
|
|
240
|
+
floatingElem.style.transform = `translate(${left}px, ${top}px)`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function setDragImage(
|
|
244
|
+
dataTransfer: DataTransfer,
|
|
245
|
+
draggableBlockElem: HTMLElement,
|
|
246
|
+
) {
|
|
247
|
+
const {transform} = draggableBlockElem.style;
|
|
248
|
+
|
|
249
|
+
// Remove dragImage borders
|
|
250
|
+
draggableBlockElem.style.transform = 'translateZ(0)';
|
|
251
|
+
dataTransfer.setDragImage(draggableBlockElem, 0, 0);
|
|
252
|
+
|
|
253
|
+
setTimeout(() => {
|
|
254
|
+
draggableBlockElem.style.transform = transform;
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** 放置区域是否是自身 */
|
|
259
|
+
// function isPlacementAreaElemSelf(
|
|
260
|
+
// targetBlockElem: HTMLElement,
|
|
261
|
+
// latestDraggableBlockElem: React.MutableRefObject<HTMLElement | null>,
|
|
262
|
+
// ) {
|
|
263
|
+
// const isTargetSelf = latestDraggableBlockElem.current === targetBlockElem;
|
|
264
|
+
// return isTargetSelf;
|
|
265
|
+
// }
|
|
266
|
+
|
|
267
|
+
/** 放置区域是否是自身的子级 */
|
|
268
|
+
function $isPlacementAreaElemSelfRoChild(
|
|
269
|
+
targetBlockElem: HTMLElement,
|
|
270
|
+
latestDraggableBlockElem: React.MutableRefObject<HTMLElement | null>,
|
|
271
|
+
isPlacementDown = true,
|
|
272
|
+
) {
|
|
273
|
+
const key = targetBlockElem.getAttribute('key');
|
|
274
|
+
let node = key ? $getNodeByKey<ElementNode>(key) : null;
|
|
275
|
+
if (!isPlacementDown) {
|
|
276
|
+
node = node ? node.getPreviousSibling() : null;
|
|
277
|
+
}
|
|
278
|
+
if (node) {
|
|
279
|
+
return Boolean(
|
|
280
|
+
$findMatchingParent(node, (current) => {
|
|
281
|
+
const draggable = latestDraggableBlockElem.current;
|
|
282
|
+
if (current && draggable) {
|
|
283
|
+
return current.getKey() === draggable.getAttribute('key');
|
|
284
|
+
}
|
|
285
|
+
return false;
|
|
286
|
+
}),
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** 是否是插入到子节点 */
|
|
293
|
+
function isInsertChild(targetBlockMouseX: number) {
|
|
294
|
+
const isIndent = targetBlockMouseX > BOX_INDENT_INSERT;
|
|
295
|
+
return isIndent;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** 是否是放到节点下面 */
|
|
299
|
+
function isPlacementAreaDown(mouseY: number, targetBlockElemTop: number) {
|
|
300
|
+
return mouseY >= targetBlockElemTop + 40;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** 获取鼠标位置以及一些基础数据 */
|
|
304
|
+
function getMouseInfo({
|
|
305
|
+
target,
|
|
306
|
+
pageY,
|
|
307
|
+
pageX,
|
|
308
|
+
targetBlockElem,
|
|
309
|
+
}: {
|
|
310
|
+
target: HTMLElement;
|
|
311
|
+
pageY: number;
|
|
312
|
+
pageX: number;
|
|
313
|
+
targetBlockElem: HTMLElement;
|
|
314
|
+
}) {
|
|
315
|
+
const mouseY = pageY / calculateZoomLevel(target);
|
|
316
|
+
const mouseX = pageX / calculateZoomLevel(target);
|
|
317
|
+
const targetBlockElemDOMRect = targetBlockElem.getBoundingClientRect();
|
|
318
|
+
const {left: targetBlockElemLeft} = targetBlockElemDOMRect;
|
|
319
|
+
const targetBlockMouseX = mouseX - targetBlockElemLeft;
|
|
320
|
+
const isAddChild = isInsertChild(targetBlockMouseX);
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
isAddChild,
|
|
324
|
+
mouseX,
|
|
325
|
+
mouseY,
|
|
326
|
+
targetBlockElemDOMRect,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function setTargetLine(params: {
|
|
331
|
+
targetLineElem: HTMLElement;
|
|
332
|
+
targetBlockElem: HTMLElement;
|
|
333
|
+
mouseY: number;
|
|
334
|
+
mouseX: number;
|
|
335
|
+
anchorElem: HTMLElement;
|
|
336
|
+
targetLineIndent?: number;
|
|
337
|
+
targetBlockElemDOMRect?: DOMRect;
|
|
338
|
+
}) {
|
|
339
|
+
const {
|
|
340
|
+
targetBlockElem,
|
|
341
|
+
targetLineElem,
|
|
342
|
+
mouseY,
|
|
343
|
+
mouseX,
|
|
344
|
+
anchorElem,
|
|
345
|
+
targetLineIndent,
|
|
346
|
+
targetBlockElemDOMRect,
|
|
347
|
+
} = params;
|
|
348
|
+
const {
|
|
349
|
+
top: targetBlockElemTop,
|
|
350
|
+
height: targetBlockElemHeight,
|
|
351
|
+
left: targetBlockElemLeft,
|
|
352
|
+
width: targetBlockElemWidth,
|
|
353
|
+
} = targetBlockElemDOMRect || targetBlockElem.getBoundingClientRect();
|
|
354
|
+
const {top: anchorTop} = anchorElem.getBoundingClientRect();
|
|
355
|
+
const {marginTop, marginBottom} = getCollapsedMargins(targetBlockElem);
|
|
356
|
+
let lineTop = targetBlockElemTop;
|
|
357
|
+
if (isPlacementAreaDown(mouseY, targetBlockElemTop)) {
|
|
358
|
+
lineTop += targetBlockElemHeight + marginBottom / 2;
|
|
359
|
+
} else {
|
|
360
|
+
lineTop -= marginTop / 2;
|
|
361
|
+
}
|
|
362
|
+
const targetBlockMouseX = mouseX - targetBlockElemLeft;
|
|
363
|
+
const top = lineTop - anchorTop - TARGET_LINE_HALF_HEIGHT;
|
|
364
|
+
|
|
365
|
+
const lineIndent = targetLineIndent || TEXT_BOX_HORIZONTAL_PADDING;
|
|
366
|
+
const isAddChild = isInsertChild(targetBlockMouseX);
|
|
367
|
+
const left = isAddChild ? BOX_INDENT_INSERT + lineIndent : lineIndent;
|
|
368
|
+
|
|
369
|
+
targetLineElem.style.transform = `translate(${left}px, ${top}px)`;
|
|
370
|
+
targetLineElem.style.backgroundColor = isAddChild ? 'purple' : 'deepskyblue';
|
|
371
|
+
targetLineElem.style.width = `${
|
|
372
|
+
targetBlockElemWidth - (isAddChild ? BOX_INDENT_INSERT : 0)
|
|
373
|
+
}px`;
|
|
374
|
+
targetLineElem.style.opacity = '.4';
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function hideTargetLine(targetLineElem: HTMLElement | null) {
|
|
378
|
+
if (targetLineElem) {
|
|
379
|
+
targetLineElem.style.opacity = '0';
|
|
380
|
+
targetLineElem.style.transform = 'translate(-10000px, -10000px)';
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function useDraggableBlockMenu(
|
|
385
|
+
editor: LexicalEditor,
|
|
386
|
+
anchorElem: HTMLElement,
|
|
387
|
+
menuRef: React.RefObject<HTMLElement>,
|
|
388
|
+
targetLineRef: React.RefObject<HTMLElement>,
|
|
389
|
+
isEditable: boolean,
|
|
390
|
+
menuComponent: ReactNode,
|
|
391
|
+
targetLineComponent: ReactNode,
|
|
392
|
+
isOnMenu: (element: HTMLElement) => boolean,
|
|
393
|
+
targetLineIndent?: number,
|
|
394
|
+
onElementChanged?: (element: HTMLElement | null) => void,
|
|
395
|
+
): JSX.Element {
|
|
396
|
+
const scrollerElem = anchorElem.parentElement;
|
|
397
|
+
|
|
398
|
+
const isDraggingBlockRef = useRef<boolean>(false);
|
|
399
|
+
const [draggableBlockElem, setDraggableBlockElemState] =
|
|
400
|
+
useState<HTMLElement | null>(null);
|
|
401
|
+
const latestDraggableBlockElem = useLatest(draggableBlockElem);
|
|
402
|
+
const setDraggableBlockElem = useCallback(
|
|
403
|
+
(elem: HTMLElement | null) => {
|
|
404
|
+
setDraggableBlockElemState(elem);
|
|
405
|
+
if (onElementChanged) {
|
|
406
|
+
onElementChanged(elem);
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
[onElementChanged],
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
useEffect(() => {
|
|
413
|
+
function onMouseMove(event: MouseEvent) {
|
|
414
|
+
const target = event.target;
|
|
415
|
+
if (!isHTMLElement(target)) {
|
|
416
|
+
setDraggableBlockElem(null);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (isOnMenu(target as HTMLElement)) {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const _draggableBlockElem = getBlockElement(anchorElem, editor, event);
|
|
425
|
+
|
|
426
|
+
setDraggableBlockElem(_draggableBlockElem);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function onMouseLeave() {
|
|
430
|
+
setDraggableBlockElem(null);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (scrollerElem != null) {
|
|
434
|
+
scrollerElem.addEventListener('mousemove', onMouseMove);
|
|
435
|
+
scrollerElem.addEventListener('mouseleave', onMouseLeave);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return () => {
|
|
439
|
+
if (scrollerElem != null) {
|
|
440
|
+
scrollerElem.removeEventListener('mousemove', onMouseMove);
|
|
441
|
+
scrollerElem.removeEventListener('mouseleave', onMouseLeave);
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
}, [scrollerElem, anchorElem, editor, isOnMenu, setDraggableBlockElem]);
|
|
445
|
+
|
|
446
|
+
useEffect(() => {
|
|
447
|
+
if (menuRef.current) {
|
|
448
|
+
// 更新操作栏区域
|
|
449
|
+
setMenuPosition(draggableBlockElem, menuRef.current, anchorElem);
|
|
450
|
+
}
|
|
451
|
+
}, [anchorElem, draggableBlockElem, menuRef]);
|
|
452
|
+
|
|
453
|
+
useEffect(() => {
|
|
454
|
+
// 拖动
|
|
455
|
+
function $onDragover(event: DragEvent): boolean {
|
|
456
|
+
if (!isDraggingBlockRef.current) {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
const [isFileTransfer] = eventFiles(event);
|
|
460
|
+
if (isFileTransfer) {
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
const {pageY, pageX, target} = event;
|
|
464
|
+
if (!isHTMLElement(target)) {
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
const targetBlockElem = getBlockElement(anchorElem, editor, event, true);
|
|
468
|
+
const targetLineElem = targetLineRef.current;
|
|
469
|
+
if (targetBlockElem === null || targetLineElem === null) {
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
const {mouseY, mouseX, isAddChild, targetBlockElemDOMRect} = getMouseInfo(
|
|
473
|
+
{pageX, pageY, target, targetBlockElem},
|
|
474
|
+
);
|
|
475
|
+
const isPlacementDown = isPlacementAreaDown(
|
|
476
|
+
mouseY,
|
|
477
|
+
targetBlockElemDOMRect.top,
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
const isPlacementSelfRoChild = $isPlacementAreaElemSelfRoChild(
|
|
481
|
+
targetBlockElem,
|
|
482
|
+
latestDraggableBlockElem,
|
|
483
|
+
isPlacementDown,
|
|
484
|
+
);
|
|
485
|
+
if (isPlacementSelfRoChild) {
|
|
486
|
+
if (isAddChild) {
|
|
487
|
+
hideTargetLine(targetLineElem);
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
const key = targetBlockElem.getAttribute('key');
|
|
491
|
+
const targetNode = key ? $getNodeByKey(key) : null;
|
|
492
|
+
if (!targetNode || !$isRootNode(targetNode.getParent())) {
|
|
493
|
+
hideTargetLine(targetLineElem);
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
setTargetLine({
|
|
498
|
+
anchorElem,
|
|
499
|
+
mouseX,
|
|
500
|
+
mouseY,
|
|
501
|
+
targetBlockElem,
|
|
502
|
+
targetBlockElemDOMRect,
|
|
503
|
+
targetLineElem,
|
|
504
|
+
targetLineIndent,
|
|
505
|
+
});
|
|
506
|
+
// Prevent default event to be able to trigger onDrop events
|
|
507
|
+
event.preventDefault();
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// 放下
|
|
512
|
+
function $onDrop(event: DragEvent): boolean {
|
|
513
|
+
if (!isDraggingBlockRef.current) {
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
516
|
+
const [isFileTransfer] = eventFiles(event);
|
|
517
|
+
if (isFileTransfer) {
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
const {target, dataTransfer, pageY, pageX} = event;
|
|
521
|
+
const dragData =
|
|
522
|
+
dataTransfer != null ? dataTransfer.getData(DRAG_DATA_FORMAT) : '';
|
|
523
|
+
const draggedNode = $getNodeByKey(dragData);
|
|
524
|
+
if (!draggedNode) {
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
527
|
+
if (!isHTMLElement(target)) {
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
const targetBlockElem = getBlockElement(anchorElem, editor, event, true);
|
|
531
|
+
if (!targetBlockElem) {
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
const targetNode = $getNearestNodeFromDOMNode(targetBlockElem);
|
|
535
|
+
if (!targetNode) {
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
const {
|
|
539
|
+
mouseY,
|
|
540
|
+
isAddChild,
|
|
541
|
+
targetBlockElemDOMRect: {top: targetBlockElemTop},
|
|
542
|
+
} = getMouseInfo({pageX, pageY, target, targetBlockElem});
|
|
543
|
+
if (targetNode === draggedNode && !isAddChild) {
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
546
|
+
if (isPlacementAreaDown(mouseY, targetBlockElemTop)) {
|
|
547
|
+
if (isAddChild && $isElementNode(targetNode)) {
|
|
548
|
+
targetNode.append(draggedNode);
|
|
549
|
+
} else {
|
|
550
|
+
targetNode.insertAfter(draggedNode);
|
|
551
|
+
}
|
|
552
|
+
} else {
|
|
553
|
+
if (isAddChild && $isElementNode(targetNode)) {
|
|
554
|
+
const previous = targetNode.getPreviousSibling();
|
|
555
|
+
if ($isElementNode(previous)) {
|
|
556
|
+
previous.append(draggedNode);
|
|
557
|
+
}
|
|
558
|
+
} else {
|
|
559
|
+
targetNode.insertBefore(draggedNode);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
draggedNode.selectEnd();
|
|
563
|
+
setDraggableBlockElem(null);
|
|
564
|
+
|
|
565
|
+
return true;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// 添加拖拽事件
|
|
569
|
+
return mergeRegister(
|
|
570
|
+
editor.registerCommand(
|
|
571
|
+
DRAGOVER_COMMAND,
|
|
572
|
+
(event) => {
|
|
573
|
+
const result = $onDragover(event);
|
|
574
|
+
if (!result) {
|
|
575
|
+
if (event.dataTransfer) {
|
|
576
|
+
event.dataTransfer.dropEffect = 'none';
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return result;
|
|
580
|
+
},
|
|
581
|
+
COMMAND_PRIORITY_LOW,
|
|
582
|
+
),
|
|
583
|
+
editor.registerCommand(
|
|
584
|
+
DROP_COMMAND,
|
|
585
|
+
(event) => {
|
|
586
|
+
return $onDrop(event);
|
|
587
|
+
},
|
|
588
|
+
COMMAND_PRIORITY_HIGH,
|
|
589
|
+
),
|
|
590
|
+
);
|
|
591
|
+
}, [
|
|
592
|
+
anchorElem,
|
|
593
|
+
editor,
|
|
594
|
+
targetLineRef,
|
|
595
|
+
setDraggableBlockElem,
|
|
596
|
+
targetLineIndent,
|
|
597
|
+
latestDraggableBlockElem,
|
|
598
|
+
]);
|
|
599
|
+
|
|
600
|
+
function onDragStart(event: ReactDragEvent<HTMLDivElement>): void {
|
|
601
|
+
const dataTransfer = event.dataTransfer;
|
|
602
|
+
if (!dataTransfer || !draggableBlockElem) {
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
setDragImage(dataTransfer, draggableBlockElem);
|
|
606
|
+
let nodeKey = '';
|
|
607
|
+
editor.update(() => {
|
|
608
|
+
const node = $getNearestNodeFromDOMNode(draggableBlockElem);
|
|
609
|
+
if (node) {
|
|
610
|
+
nodeKey = node.getKey();
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
isDraggingBlockRef.current = true;
|
|
614
|
+
dataTransfer.setData(DRAG_DATA_FORMAT, nodeKey);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function onDragEnd(): void {
|
|
618
|
+
isDraggingBlockRef.current = false;
|
|
619
|
+
hideTargetLine(targetLineRef.current);
|
|
620
|
+
}
|
|
621
|
+
return createPortal(
|
|
622
|
+
<>
|
|
623
|
+
<div draggable={true} onDragStart={onDragStart} onDragEnd={onDragEnd}>
|
|
624
|
+
{isEditable && menuComponent}
|
|
625
|
+
</div>
|
|
626
|
+
{targetLineComponent}
|
|
627
|
+
</>,
|
|
628
|
+
anchorElem,
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
export function DraggableBlockPlugin_EXPERIMENTAL({
|
|
633
|
+
anchorElem = document.body,
|
|
634
|
+
menuRef,
|
|
635
|
+
targetLineRef,
|
|
636
|
+
menuComponent,
|
|
637
|
+
targetLineComponent,
|
|
638
|
+
targetLineIndent,
|
|
639
|
+
isOnMenu,
|
|
640
|
+
onElementChanged,
|
|
641
|
+
}: {
|
|
642
|
+
anchorElem?: HTMLElement;
|
|
643
|
+
menuRef: React.RefObject<HTMLElement>;
|
|
644
|
+
targetLineRef: React.RefObject<HTMLElement>;
|
|
645
|
+
menuComponent: ReactNode;
|
|
646
|
+
targetLineComponent: ReactNode;
|
|
647
|
+
isOnMenu: (element: HTMLElement) => boolean;
|
|
648
|
+
/** default 46 */
|
|
649
|
+
targetLineIndent?: number;
|
|
650
|
+
onElementChanged?: (element: HTMLElement | null) => void;
|
|
651
|
+
}): JSX.Element {
|
|
652
|
+
const [editor] = useLexicalComposerContext();
|
|
653
|
+
return useDraggableBlockMenu(
|
|
654
|
+
editor,
|
|
655
|
+
anchorElem,
|
|
656
|
+
menuRef,
|
|
657
|
+
targetLineRef,
|
|
658
|
+
editor._editable,
|
|
659
|
+
menuComponent,
|
|
660
|
+
targetLineComponent,
|
|
661
|
+
isOnMenu,
|
|
662
|
+
targetLineIndent,
|
|
663
|
+
onElementChanged,
|
|
664
|
+
);
|
|
665
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
.draggable-block-menu {
|
|
2
|
+
border-radius: 4px;
|
|
3
|
+
cursor: grab;
|
|
4
|
+
opacity: 0;
|
|
5
|
+
position: absolute;
|
|
6
|
+
left: 0;
|
|
7
|
+
top: 0;
|
|
8
|
+
will-change: transform;
|
|
9
|
+
display: flex;
|
|
10
|
+
gap: 2px;
|
|
11
|
+
height: 40px;
|
|
12
|
+
align-items: center;
|
|
13
|
+
/* opacity: 1 !important;
|
|
14
|
+
transform: translate(4px, 10px) !important; */
|
|
15
|
+
.icon {
|
|
16
|
+
display: flex;
|
|
17
|
+
justify-content: center;
|
|
18
|
+
align-items: center;
|
|
19
|
+
width: 16px;
|
|
20
|
+
height: 16px;
|
|
21
|
+
opacity: 0.3;
|
|
22
|
+
background-image: url(../../images/icons/draggable-block-menu.svg);
|
|
23
|
+
&:hover {
|
|
24
|
+
background-color: #efefef;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
.icon-plus {
|
|
28
|
+
display: inline-block;
|
|
29
|
+
border: none;
|
|
30
|
+
cursor: pointer;
|
|
31
|
+
background-color: transparent;
|
|
32
|
+
background-image: url(../../images/icons/plus.svg);
|
|
33
|
+
}
|
|
34
|
+
&:active {
|
|
35
|
+
cursor: grabbing;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.draggable-block-target-line {
|
|
40
|
+
pointer-events: none;
|
|
41
|
+
background: deepskyblue;
|
|
42
|
+
height: 4px;
|
|
43
|
+
position: absolute;
|
|
44
|
+
left: 0;
|
|
45
|
+
top: 0;
|
|
46
|
+
opacity: 0;
|
|
47
|
+
will-change: transform;
|
|
48
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
import type {JSX} from 'react';
|
|
9
|
+
|
|
10
|
+
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
|
11
|
+
// import {DraggableBlockPlugin_EXPERIMENTAL} from '@lexical/react/LexicalDraggableBlockPlugin';
|
|
12
|
+
import {$getNearestNodeFromDOMNode} from 'lexical';
|
|
13
|
+
import {$createInstanceNode} from 'onchain-lexical-instance';
|
|
14
|
+
import {useRef, useState} from 'react';
|
|
15
|
+
|
|
16
|
+
import {DraggableBlockPlugin_EXPERIMENTAL} from './draggableBlockPlugin';
|
|
17
|
+
import Styles from './index.module.less';
|
|
18
|
+
|
|
19
|
+
const DRAGGABLE_BLOCK_MENU_CLASSNAME = Styles['draggable-block-menu'];
|
|
20
|
+
|
|
21
|
+
function isOnMenu(element: HTMLElement): boolean {
|
|
22
|
+
return !!element.closest(`.${DRAGGABLE_BLOCK_MENU_CLASSNAME}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default function DraggableBlockPlugin({
|
|
26
|
+
anchorElem = document.body,
|
|
27
|
+
dragIcon,
|
|
28
|
+
targetLineIndent,
|
|
29
|
+
}: {
|
|
30
|
+
anchorElem?: HTMLElement;
|
|
31
|
+
targetLineIndent?: number;
|
|
32
|
+
dragIcon?: React.ReactNode;
|
|
33
|
+
}): JSX.Element {
|
|
34
|
+
const [editor] = useLexicalComposerContext();
|
|
35
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
36
|
+
const targetLineRef = useRef<HTMLDivElement>(null);
|
|
37
|
+
const [draggableElement, setDraggableElement] = useState<HTMLElement | null>(
|
|
38
|
+
null,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
function insertBlock(e: React.MouseEvent) {
|
|
42
|
+
if (!draggableElement || !editor) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
editor.update(() => {
|
|
47
|
+
const node = $getNearestNodeFromDOMNode(draggableElement);
|
|
48
|
+
if (!node) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const pNode = $createInstanceNode();
|
|
53
|
+
if (e.altKey || e.ctrlKey) {
|
|
54
|
+
node.insertBefore(pNode);
|
|
55
|
+
} else {
|
|
56
|
+
node.insertAfter(pNode);
|
|
57
|
+
}
|
|
58
|
+
pNode.select();
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<DraggableBlockPlugin_EXPERIMENTAL
|
|
64
|
+
anchorElem={anchorElem}
|
|
65
|
+
menuRef={menuRef}
|
|
66
|
+
targetLineRef={targetLineRef}
|
|
67
|
+
menuComponent={
|
|
68
|
+
<div
|
|
69
|
+
ref={menuRef}
|
|
70
|
+
className={`${Styles.icon} ${Styles['draggable-block-menu']}`}>
|
|
71
|
+
<button
|
|
72
|
+
title="Click to add below"
|
|
73
|
+
className={`${Styles.icon} ${Styles['icon-plus']}`}
|
|
74
|
+
onClick={insertBlock}
|
|
75
|
+
style={{display: 'none'}}
|
|
76
|
+
/>
|
|
77
|
+
<div className={Styles.icon}>{dragIcon}</div>
|
|
78
|
+
</div>
|
|
79
|
+
}
|
|
80
|
+
targetLineComponent={
|
|
81
|
+
<div
|
|
82
|
+
ref={targetLineRef}
|
|
83
|
+
className={`${Styles['draggable-block-target-line']}`}
|
|
84
|
+
/>
|
|
85
|
+
}
|
|
86
|
+
targetLineIndent={targetLineIndent}
|
|
87
|
+
isOnMenu={isOnMenu}
|
|
88
|
+
onElementChanged={setDraggableElement}
|
|
89
|
+
/>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
export class Point {
|
|
9
|
+
private readonly _x: number;
|
|
10
|
+
private readonly _y: number;
|
|
11
|
+
|
|
12
|
+
constructor(x: number, y: number) {
|
|
13
|
+
this._x = x;
|
|
14
|
+
this._y = y;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get x(): number {
|
|
18
|
+
return this._x;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get y(): number {
|
|
22
|
+
return this._y;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public equals({x, y}: Point): boolean {
|
|
26
|
+
return this.x === x && this.y === y;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public calcDeltaXTo({x}: Point): number {
|
|
30
|
+
return this.x - x;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public calcDeltaYTo({y}: Point): number {
|
|
34
|
+
return this.y - y;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public calcHorizontalDistanceTo(point: Point): number {
|
|
38
|
+
return Math.abs(this.calcDeltaXTo(point));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public calcVerticalDistance(point: Point): number {
|
|
42
|
+
return Math.abs(this.calcDeltaYTo(point));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public calcDistanceTo(point: Point): number {
|
|
46
|
+
return Math.sqrt(
|
|
47
|
+
Math.pow(this.calcDeltaXTo(point), 2) +
|
|
48
|
+
Math.pow(this.calcDeltaYTo(point), 2),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function isPoint(x: unknown): x is Point {
|
|
54
|
+
return x instanceof Point;
|
|
55
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
import {isPoint, Point} from './point';
|
|
9
|
+
|
|
10
|
+
type ContainsPointReturn = {
|
|
11
|
+
result: boolean;
|
|
12
|
+
reason: {
|
|
13
|
+
isOnTopSide: boolean;
|
|
14
|
+
isOnBottomSide: boolean;
|
|
15
|
+
isOnLeftSide: boolean;
|
|
16
|
+
isOnRightSide: boolean;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export class Rectangle {
|
|
21
|
+
private readonly _left: number;
|
|
22
|
+
private readonly _top: number;
|
|
23
|
+
private readonly _right: number;
|
|
24
|
+
private readonly _bottom: number;
|
|
25
|
+
|
|
26
|
+
constructor(left: number, top: number, right: number, bottom: number) {
|
|
27
|
+
const [physicTop, physicBottom] =
|
|
28
|
+
top <= bottom ? [top, bottom] : [bottom, top];
|
|
29
|
+
|
|
30
|
+
const [physicLeft, physicRight] =
|
|
31
|
+
left <= right ? [left, right] : [right, left];
|
|
32
|
+
|
|
33
|
+
this._top = physicTop;
|
|
34
|
+
this._right = physicRight;
|
|
35
|
+
this._left = physicLeft;
|
|
36
|
+
this._bottom = physicBottom;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get top(): number {
|
|
40
|
+
return this._top;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get right(): number {
|
|
44
|
+
return this._right;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get bottom(): number {
|
|
48
|
+
return this._bottom;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get left(): number {
|
|
52
|
+
return this._left;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get width(): number {
|
|
56
|
+
return Math.abs(this._left - this._right);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get height(): number {
|
|
60
|
+
return Math.abs(this._bottom - this._top);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public equals({top, left, bottom, right}: Rectangle): boolean {
|
|
64
|
+
return (
|
|
65
|
+
top === this._top &&
|
|
66
|
+
bottom === this._bottom &&
|
|
67
|
+
left === this._left &&
|
|
68
|
+
right === this._right
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public contains({x, y}: Point): ContainsPointReturn;
|
|
73
|
+
public contains({top, left, bottom, right}: Rectangle): boolean;
|
|
74
|
+
public contains(target: Point | Rectangle): boolean | ContainsPointReturn {
|
|
75
|
+
if (isPoint(target)) {
|
|
76
|
+
const {x, y} = target;
|
|
77
|
+
|
|
78
|
+
const isOnTopSide = y < this._top;
|
|
79
|
+
const isOnBottomSide = y > this._bottom;
|
|
80
|
+
const isOnLeftSide = x < this._left;
|
|
81
|
+
const isOnRightSide = x > this._right;
|
|
82
|
+
|
|
83
|
+
const result =
|
|
84
|
+
!isOnTopSide && !isOnBottomSide && !isOnLeftSide && !isOnRightSide;
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
reason: {
|
|
88
|
+
isOnBottomSide,
|
|
89
|
+
isOnLeftSide,
|
|
90
|
+
isOnRightSide,
|
|
91
|
+
isOnTopSide,
|
|
92
|
+
},
|
|
93
|
+
result,
|
|
94
|
+
};
|
|
95
|
+
} else {
|
|
96
|
+
const {top, left, bottom, right} = target;
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
top >= this._top &&
|
|
100
|
+
top <= this._bottom &&
|
|
101
|
+
bottom >= this._top &&
|
|
102
|
+
bottom <= this._bottom &&
|
|
103
|
+
left >= this._left &&
|
|
104
|
+
left <= this._right &&
|
|
105
|
+
right >= this._left &&
|
|
106
|
+
right <= this._right
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
public intersectsWith(rect: Rectangle): boolean {
|
|
112
|
+
const {left: x1, top: y1, width: w1, height: h1} = rect;
|
|
113
|
+
const {left: x2, top: y2, width: w2, height: h2} = this;
|
|
114
|
+
const maxX = x1 + w1 >= x2 + w2 ? x1 + w1 : x2 + w2;
|
|
115
|
+
const maxY = y1 + h1 >= y2 + h2 ? y1 + h1 : y2 + h2;
|
|
116
|
+
const minX = x1 <= x2 ? x1 : x2;
|
|
117
|
+
const minY = y1 <= y2 ? y1 : y2;
|
|
118
|
+
return maxX - minX <= w1 + w2 && maxY - minY <= h1 + h2;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
public generateNewRect({
|
|
122
|
+
left = this.left,
|
|
123
|
+
top = this.top,
|
|
124
|
+
right = this.right,
|
|
125
|
+
bottom = this.bottom,
|
|
126
|
+
}): Rectangle {
|
|
127
|
+
return new Rectangle(left, top, right, bottom);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
static fromLTRB(
|
|
131
|
+
left: number,
|
|
132
|
+
top: number,
|
|
133
|
+
right: number,
|
|
134
|
+
bottom: number,
|
|
135
|
+
): Rectangle {
|
|
136
|
+
return new Rectangle(left, top, right, bottom);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
static fromLWTH(
|
|
140
|
+
left: number,
|
|
141
|
+
width: number,
|
|
142
|
+
top: number,
|
|
143
|
+
height: number,
|
|
144
|
+
): Rectangle {
|
|
145
|
+
return new Rectangle(left, top, left + width, top + height);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
static fromPoints(startPoint: Point, endPoint: Point): Rectangle {
|
|
149
|
+
const {y: top, x: left} = startPoint;
|
|
150
|
+
const {y: bottom, x: right} = endPoint;
|
|
151
|
+
return Rectangle.fromLTRB(left, top, right, bottom);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
static fromDOM(dom: HTMLElement): Rectangle {
|
|
155
|
+
const {top, width, left, height} = dom.getBoundingClientRect();
|
|
156
|
+
return Rectangle.fromLWTH(left, width, top, height);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {MarkdownShortcutPlugin} from '@lexical/react/LexicalMarkdownShortcutPlugin';
|
|
10
|
+
import {getInstanceTransformers} from 'onchain-lexical-markdown';
|
|
11
|
+
import {type JSX} from 'react';
|
|
12
|
+
|
|
13
|
+
export default function MarkdownPlugin(): JSX.Element {
|
|
14
|
+
return <MarkdownShortcutPlugin transformers={getInstanceTransformers()} />;
|
|
15
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
import {useEffect, useRef} from 'react';
|
|
9
|
+
|
|
10
|
+
const useLatest = <T>(value: T) => {
|
|
11
|
+
const latest = useRef(value);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
latest.current = value;
|
|
14
|
+
}, [value]);
|
|
15
|
+
return latest;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default useLatest;
|