@octanejs/lexical 0.1.2
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/LICENSE +21 -0
- package/README.md +67 -0
- package/package.json +104 -0
- package/src/LexicalAutoEmbedPlugin.tsrx +144 -0
- package/src/LexicalAutoFocusPlugin.tsrx +25 -0
- package/src/LexicalAutoLinkPlugin.tsrx +32 -0
- package/src/LexicalBlockWithAlignableContents.tsrx +85 -0
- package/src/LexicalCharacterLimitPlugin.tsrx +66 -0
- package/src/LexicalCheckListPlugin.tsrx +16 -0
- package/src/LexicalClearEditorPlugin.tsrx +12 -0
- package/src/LexicalClickableLinkPlugin.tsrx +17 -0
- package/src/LexicalCollaborationContext.ts +67 -0
- package/src/LexicalComposer.tsrx +100 -0
- package/src/LexicalComposerContext.ts +49 -0
- package/src/LexicalContentEditable.tsrx +48 -0
- package/src/LexicalDecoratorBlockNode.ts +78 -0
- package/src/LexicalDraggableBlockPlugin.tsrx +499 -0
- package/src/LexicalEditorRefPlugin.tsrx +19 -0
- package/src/LexicalErrorBoundary.tsrx +23 -0
- package/src/LexicalHashtagPlugin.tsrx +17 -0
- package/src/LexicalHistoryPlugin.tsrx +23 -0
- package/src/LexicalHorizontalRuleNode.tsrx +99 -0
- package/src/LexicalHorizontalRulePlugin.tsrx +34 -0
- package/src/LexicalLinkPlugin.tsrx +27 -0
- package/src/LexicalListPlugin.tsrx +37 -0
- package/src/LexicalMarkdownShortcutPlugin.tsrx +39 -0
- package/src/LexicalNestedComposer.tsrx +124 -0
- package/src/LexicalNodeContextMenuPlugin.tsrx +252 -0
- package/src/LexicalNodeEventPlugin.tsrx +48 -0
- package/src/LexicalNodeMenuPlugin.tsrx +84 -0
- package/src/LexicalOnChangePlugin.tsrx +36 -0
- package/src/LexicalPlainTextPlugin.tsrx +33 -0
- package/src/LexicalRichTextPlugin.tsrx +35 -0
- package/src/LexicalSelectionAlwaysOnDisplay.tsrx +11 -0
- package/src/LexicalTabIndentationPlugin.tsrx +21 -0
- package/src/LexicalTableOfContentsPlugin.tsrx +213 -0
- package/src/LexicalTablePlugin.tsrx +67 -0
- package/src/LexicalTypeaheadMenuPlugin.tsrx +141 -0
- package/src/autoEmbedShared.ts +43 -0
- package/src/index.ts +99 -0
- package/src/shared/LexicalContentEditableElement.tsrx +101 -0
- package/src/shared/LexicalDecorators.tsrx +88 -0
- package/src/shared/LexicalMenu.tsrx +275 -0
- package/src/shared/internal.ts +32 -0
- package/src/shared/menuShared.ts +152 -0
- package/src/shared/point.ts +46 -0
- package/src/shared/rect.ts +136 -0
- package/src/shared/useCanShowPlaceholder.ts +38 -0
- package/src/shared/useCharacterLimit.ts +288 -0
- package/src/shared/useDynamicPositioning.ts +72 -0
- package/src/shared/useMenuAnchorRef.ts +131 -0
- package/src/shared/usePlainTextSetup.ts +15 -0
- package/src/shared/useRichTextSetup.ts +16 -0
- package/src/tsrx-modules.d.ts +4 -0
- package/src/typeaheadShared.ts +132 -0
- package/src/types.ts +29 -0
- package/src/useLexicalEditable.ts +22 -0
- package/src/useLexicalIsTextContentEmpty.ts +35 -0
- package/src/useLexicalNodeSelection.ts +91 -0
- package/src/useLexicalSlotRef.ts +26 -0
- package/src/useLexicalSubscription.ts +55 -0
- package/src/useLexicalTextEntity.ts +24 -0
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
// Ported from @lexical/react/src/LexicalDraggableBlockPlugin.tsx. Renders a drag
|
|
2
|
+
// handle + drop-target line (portaled into anchorElem) to reorder top-level blocks.
|
|
3
|
+
// The DOM math helpers are framework-agnostic and copied as-is; the portal body is a
|
|
4
|
+
// component (DraggableContainer) so its <div> events wire through the template path.
|
|
5
|
+
import { useLexicalComposerContext } from './LexicalComposerContext';
|
|
6
|
+
import { eventFiles } from '@lexical/rich-text';
|
|
7
|
+
import { calculateZoomLevel } from '@lexical/utils';
|
|
8
|
+
import {
|
|
9
|
+
$getNearestNodeFromDOMNode,
|
|
10
|
+
$getNodeByKey,
|
|
11
|
+
$getRoot,
|
|
12
|
+
$getSelection,
|
|
13
|
+
$onUpdate,
|
|
14
|
+
BLUR_COMMAND,
|
|
15
|
+
COMMAND_PRIORITY_HIGH,
|
|
16
|
+
COMMAND_PRIORITY_LOW,
|
|
17
|
+
DRAGOVER_COMMAND,
|
|
18
|
+
DROP_COMMAND,
|
|
19
|
+
getActiveElement,
|
|
20
|
+
getActiveElementDeep,
|
|
21
|
+
getComposedEventTarget,
|
|
22
|
+
getParentElement,
|
|
23
|
+
getRootOwnerDocument,
|
|
24
|
+
IS_FIREFOX,
|
|
25
|
+
isHTMLElement,
|
|
26
|
+
mergeRegister,
|
|
27
|
+
} from 'lexical';
|
|
28
|
+
import { createPortal, useCallback, useEffect, useRef, useState } from 'octane';
|
|
29
|
+
|
|
30
|
+
import { Point } from './shared/point';
|
|
31
|
+
import { Rectangle } from './shared/rect';
|
|
32
|
+
|
|
33
|
+
const SPACE = 4;
|
|
34
|
+
const TARGET_LINE_HALF_HEIGHT = 2;
|
|
35
|
+
const DRAG_DATA_FORMAT = 'application/x-lexical-drag-block';
|
|
36
|
+
const TEXT_BOX_HORIZONTAL_PADDING = 28;
|
|
37
|
+
|
|
38
|
+
const Downward = 1;
|
|
39
|
+
const Upward = -1;
|
|
40
|
+
const Indeterminate = 0;
|
|
41
|
+
|
|
42
|
+
let prevIndex = Infinity;
|
|
43
|
+
|
|
44
|
+
function getCurrentIndex(keysLength) {
|
|
45
|
+
if (keysLength === 0) {
|
|
46
|
+
return Infinity;
|
|
47
|
+
}
|
|
48
|
+
if (prevIndex >= 0 && prevIndex < keysLength) {
|
|
49
|
+
return prevIndex;
|
|
50
|
+
}
|
|
51
|
+
return Math.floor(keysLength / 2);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getTopLevelNodeKeys(editor) {
|
|
55
|
+
return editor.read('latest', () => $getRoot().getChildrenKeys());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getCollapsedMargins(elem) {
|
|
59
|
+
const getMargin = (element, margin) => element
|
|
60
|
+
? parseFloat(window.getComputedStyle(element)[margin])
|
|
61
|
+
: 0;
|
|
62
|
+
|
|
63
|
+
const { marginTop, marginBottom } = window.getComputedStyle(elem);
|
|
64
|
+
const prevElemSiblingMarginBottom = getMargin(elem.previousElementSibling, 'marginBottom');
|
|
65
|
+
const nextElemSiblingMarginTop = getMargin(elem.nextElementSibling, 'marginTop');
|
|
66
|
+
const collapsedTopMargin = Math.max(parseFloat(marginTop), prevElemSiblingMarginBottom);
|
|
67
|
+
const collapsedBottomMargin = Math.max(parseFloat(marginBottom), nextElemSiblingMarginTop);
|
|
68
|
+
|
|
69
|
+
return { marginBottom: collapsedBottomMargin, marginTop: collapsedTopMargin };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getBlockElement(anchorElem, editor, event, useEdgeAsDefault = false) {
|
|
73
|
+
const anchorElementRect = anchorElem.getBoundingClientRect();
|
|
74
|
+
const topLevelNodeKeys = getTopLevelNodeKeys(editor);
|
|
75
|
+
|
|
76
|
+
let blockElem = null;
|
|
77
|
+
|
|
78
|
+
editor.read('latest', () => {
|
|
79
|
+
if (useEdgeAsDefault) {
|
|
80
|
+
const [firstNode, lastNode] = [
|
|
81
|
+
editor.getElementByKey(topLevelNodeKeys[0]),
|
|
82
|
+
editor.getElementByKey(topLevelNodeKeys[topLevelNodeKeys.length - 1]),
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
const [firstNodeRect, lastNodeRect] = [
|
|
86
|
+
firstNode != null ? firstNode.getBoundingClientRect() : undefined,
|
|
87
|
+
lastNode != null ? lastNode.getBoundingClientRect() : undefined,
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
if (firstNodeRect && lastNodeRect) {
|
|
91
|
+
const firstNodeZoom = calculateZoomLevel(firstNode);
|
|
92
|
+
const lastNodeZoom = calculateZoomLevel(lastNode);
|
|
93
|
+
if (event.y / firstNodeZoom < firstNodeRect.top) {
|
|
94
|
+
blockElem = firstNode;
|
|
95
|
+
} else if (event.y / lastNodeZoom > lastNodeRect.bottom) {
|
|
96
|
+
blockElem = lastNode;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (blockElem) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let index = getCurrentIndex(topLevelNodeKeys.length);
|
|
106
|
+
let direction = Indeterminate;
|
|
107
|
+
|
|
108
|
+
while (index >= 0 && index < topLevelNodeKeys.length) {
|
|
109
|
+
const key = topLevelNodeKeys[index];
|
|
110
|
+
const elem = editor.getElementByKey(key);
|
|
111
|
+
if (elem === null) {
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
const zoom = calculateZoomLevel(elem);
|
|
115
|
+
const point = new Point(event.x / zoom, event.y / zoom);
|
|
116
|
+
const domRect = Rectangle.fromDOM(elem);
|
|
117
|
+
const { marginTop, marginBottom } = getCollapsedMargins(elem);
|
|
118
|
+
const rect = domRect.generateNewRect({
|
|
119
|
+
bottom: domRect.bottom + marginBottom,
|
|
120
|
+
left: anchorElementRect.left,
|
|
121
|
+
right: anchorElementRect.right,
|
|
122
|
+
top: domRect.top - marginTop,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const { result, reason: { isOnTopSide, isOnBottomSide } } = rect.contains(point);
|
|
126
|
+
|
|
127
|
+
if (result) {
|
|
128
|
+
blockElem = elem;
|
|
129
|
+
prevIndex = index;
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (direction === Indeterminate) {
|
|
134
|
+
if (isOnTopSide) {
|
|
135
|
+
direction = Upward;
|
|
136
|
+
} else if (isOnBottomSide) {
|
|
137
|
+
direction = Downward;
|
|
138
|
+
} else {
|
|
139
|
+
direction = Infinity;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
index += direction;
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return blockElem;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function setMenuPosition(targetElem, floatingElem, anchorElem, zoomLevel) {
|
|
151
|
+
if (!targetElem) {
|
|
152
|
+
floatingElem.style.display = 'none';
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const targetRect = targetElem.getBoundingClientRect();
|
|
157
|
+
const targetStyle = window.getComputedStyle(targetElem);
|
|
158
|
+
const floatingElemRect = floatingElem.getBoundingClientRect();
|
|
159
|
+
const anchorElementRect = anchorElem.getBoundingClientRect();
|
|
160
|
+
|
|
161
|
+
let targetCalculateHeight = parseInt(targetStyle.lineHeight, 10);
|
|
162
|
+
if (isNaN(targetCalculateHeight)) {
|
|
163
|
+
targetCalculateHeight = targetRect.bottom - targetRect.top;
|
|
164
|
+
}
|
|
165
|
+
const top =
|
|
166
|
+
(targetRect.top +
|
|
167
|
+
(targetCalculateHeight - (floatingElemRect.height || targetCalculateHeight)) / 2 -
|
|
168
|
+
anchorElementRect.top +
|
|
169
|
+
anchorElem.scrollTop) /
|
|
170
|
+
zoomLevel;
|
|
171
|
+
|
|
172
|
+
const left = SPACE;
|
|
173
|
+
|
|
174
|
+
floatingElem.style.display = 'flex';
|
|
175
|
+
floatingElem.style.opacity = '1';
|
|
176
|
+
floatingElem.style.transform = `translate(${left}px, ${top}px)`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function setDragImage(dataTransfer, draggableBlockElem) {
|
|
180
|
+
const { transform } = draggableBlockElem.style;
|
|
181
|
+
|
|
182
|
+
draggableBlockElem.style.transform = 'translateZ(0)';
|
|
183
|
+
dataTransfer.setDragImage(draggableBlockElem, 0, 0);
|
|
184
|
+
|
|
185
|
+
setTimeout(() => {
|
|
186
|
+
draggableBlockElem.style.transform = transform;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function setTargetLine(targetLineElem, targetBlockElem, mouseY, anchorElem) {
|
|
191
|
+
const { top: targetBlockElemTop, height: targetBlockElemHeight } =
|
|
192
|
+
targetBlockElem.getBoundingClientRect();
|
|
193
|
+
const { top: anchorTop, width: anchorWidth } = anchorElem.getBoundingClientRect();
|
|
194
|
+
const { marginTop, marginBottom } = getCollapsedMargins(targetBlockElem);
|
|
195
|
+
let lineTop = targetBlockElemTop;
|
|
196
|
+
if (mouseY >= targetBlockElemTop) {
|
|
197
|
+
lineTop += targetBlockElemHeight + marginBottom / 2;
|
|
198
|
+
} else {
|
|
199
|
+
lineTop -= marginTop / 2;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const top = lineTop - anchorTop - TARGET_LINE_HALF_HEIGHT + anchorElem.scrollTop;
|
|
203
|
+
const left = TEXT_BOX_HORIZONTAL_PADDING - SPACE;
|
|
204
|
+
|
|
205
|
+
targetLineElem.style.transform = `translate(${left}px, ${top}px)`;
|
|
206
|
+
targetLineElem.style.width = `${anchorWidth - (TEXT_BOX_HORIZONTAL_PADDING - SPACE) * 2}px`;
|
|
207
|
+
targetLineElem.style.opacity = '.4';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function hideTargetLine(targetLineElem) {
|
|
211
|
+
if (targetLineElem) {
|
|
212
|
+
targetLineElem.style.opacity = '0';
|
|
213
|
+
targetLineElem.style.transform = 'translate(-10000px, -10000px)';
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function DraggableContainer(props) @{
|
|
218
|
+
<>
|
|
219
|
+
<div draggable={true} onDragStart={props.onDragStart} onDragEnd={props.onDragEnd}>
|
|
220
|
+
{props.isEditable ? props.menuComponent : null}
|
|
221
|
+
</div>
|
|
222
|
+
{props.targetLineComponent}
|
|
223
|
+
</>
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function useDraggableBlockMenu(
|
|
227
|
+
editor,
|
|
228
|
+
anchorElem,
|
|
229
|
+
menuRef,
|
|
230
|
+
targetLineRef,
|
|
231
|
+
isEditable,
|
|
232
|
+
menuComponent,
|
|
233
|
+
targetLineComponent,
|
|
234
|
+
isOnMenu,
|
|
235
|
+
onElementChanged,
|
|
236
|
+
) {
|
|
237
|
+
const scrollerElem = getParentElement(anchorElem);
|
|
238
|
+
|
|
239
|
+
const isDraggingBlockRef = useRef(false);
|
|
240
|
+
const [draggableBlockElem, setDraggableBlockElemState] = useState(null);
|
|
241
|
+
|
|
242
|
+
const setDraggableBlockElem = useCallback((elem) => {
|
|
243
|
+
setDraggableBlockElemState(elem);
|
|
244
|
+
if (onElementChanged) {
|
|
245
|
+
onElementChanged(elem);
|
|
246
|
+
}
|
|
247
|
+
}, [onElementChanged]);
|
|
248
|
+
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
function onMouseMove(event) {
|
|
251
|
+
const target = getComposedEventTarget(event);
|
|
252
|
+
if (!isHTMLElement(target)) {
|
|
253
|
+
setDraggableBlockElem(null);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (isOnMenu(target)) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const _draggableBlockElem = getBlockElement(anchorElem, editor, event);
|
|
262
|
+
setDraggableBlockElem(_draggableBlockElem);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function onMouseLeave() {
|
|
266
|
+
setDraggableBlockElem(null);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (scrollerElem != null) {
|
|
270
|
+
scrollerElem.addEventListener('mousemove', onMouseMove);
|
|
271
|
+
scrollerElem.addEventListener('mouseleave', onMouseLeave);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return () => {
|
|
275
|
+
if (scrollerElem != null) {
|
|
276
|
+
scrollerElem.removeEventListener('mousemove', onMouseMove);
|
|
277
|
+
scrollerElem.removeEventListener('mouseleave', onMouseLeave);
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
}, [scrollerElem, anchorElem, editor, isOnMenu, setDraggableBlockElem]);
|
|
281
|
+
|
|
282
|
+
useEffect(() => {
|
|
283
|
+
const zoomLevel = calculateZoomLevel(editor.getRootElement(), true);
|
|
284
|
+
if (menuRef.current) {
|
|
285
|
+
setMenuPosition(draggableBlockElem, menuRef.current, anchorElem, zoomLevel);
|
|
286
|
+
}
|
|
287
|
+
}, [editor, anchorElem, draggableBlockElem, menuRef]);
|
|
288
|
+
|
|
289
|
+
useEffect(() => {
|
|
290
|
+
function onDragover(event) {
|
|
291
|
+
if (!isDraggingBlockRef.current) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
const [isFileTransfer] = eventFiles(event);
|
|
295
|
+
if (isFileTransfer) {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
const pageY = event.pageY;
|
|
299
|
+
const target = getComposedEventTarget(event);
|
|
300
|
+
if (!isHTMLElement(target)) {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
const targetBlockElem = getBlockElement(anchorElem, editor, event, true);
|
|
304
|
+
const targetLineElem = targetLineRef.current;
|
|
305
|
+
if (targetBlockElem === null || targetLineElem === null) {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
setTargetLine(
|
|
309
|
+
targetLineElem,
|
|
310
|
+
targetBlockElem,
|
|
311
|
+
pageY / calculateZoomLevel(target),
|
|
312
|
+
anchorElem,
|
|
313
|
+
);
|
|
314
|
+
event.preventDefault();
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function $onDrop(event) {
|
|
319
|
+
if (!isDraggingBlockRef.current) {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
const [isFileTransfer] = eventFiles(event);
|
|
323
|
+
if (isFileTransfer) {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
const { dataTransfer, pageY } = event;
|
|
327
|
+
const target = getComposedEventTarget(event);
|
|
328
|
+
const dragData =
|
|
329
|
+
dataTransfer != null ? dataTransfer.getData(DRAG_DATA_FORMAT) : '';
|
|
330
|
+
const draggedNode = $getNodeByKey(dragData);
|
|
331
|
+
if (!draggedNode) {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
if (!isHTMLElement(target)) {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
const targetBlockElem = getBlockElement(anchorElem, editor, event, true);
|
|
338
|
+
if (!targetBlockElem) {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
const targetNode = $getNearestNodeFromDOMNode(targetBlockElem);
|
|
342
|
+
if (!targetNode) {
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
if (targetNode === draggedNode) {
|
|
346
|
+
if (IS_FIREFOX) {
|
|
347
|
+
editor.focus();
|
|
348
|
+
}
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
const targetBlockElemTop = targetBlockElem.getBoundingClientRect().top;
|
|
352
|
+
if (pageY / calculateZoomLevel(target) >= targetBlockElemTop) {
|
|
353
|
+
targetNode.insertAfter(draggedNode);
|
|
354
|
+
} else {
|
|
355
|
+
targetNode.insertBefore(draggedNode);
|
|
356
|
+
}
|
|
357
|
+
setDraggableBlockElem(null);
|
|
358
|
+
|
|
359
|
+
if (IS_FIREFOX) {
|
|
360
|
+
$onUpdate(() => {
|
|
361
|
+
editor.focus();
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return mergeRegister(
|
|
369
|
+
editor.registerCommand(
|
|
370
|
+
DRAGOVER_COMMAND,
|
|
371
|
+
(event) => {
|
|
372
|
+
return onDragover(event);
|
|
373
|
+
},
|
|
374
|
+
COMMAND_PRIORITY_LOW,
|
|
375
|
+
),
|
|
376
|
+
editor.registerCommand(
|
|
377
|
+
DROP_COMMAND,
|
|
378
|
+
(event) => {
|
|
379
|
+
return $onDrop(event);
|
|
380
|
+
},
|
|
381
|
+
COMMAND_PRIORITY_HIGH,
|
|
382
|
+
),
|
|
383
|
+
);
|
|
384
|
+
}, [anchorElem, editor, targetLineRef, setDraggableBlockElem]);
|
|
385
|
+
|
|
386
|
+
useEffect(() => {
|
|
387
|
+
if (!IS_FIREFOX || !isEditable) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return mergeRegister(
|
|
392
|
+
editor.registerRootListener((rootElement) => {
|
|
393
|
+
function onBlur(event) {
|
|
394
|
+
const relatedTarget = event.relatedTarget;
|
|
395
|
+
if (isHTMLElement(relatedTarget) && isOnMenu(relatedTarget)) {
|
|
396
|
+
if (rootElement) {
|
|
397
|
+
rootElement.focus({ preventScroll: true });
|
|
398
|
+
editor.update(() => {
|
|
399
|
+
const selection = $getSelection();
|
|
400
|
+
if (selection !== null && !selection.dirty) {
|
|
401
|
+
selection.dirty = true;
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
event.stopImmediatePropagation();
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (rootElement) {
|
|
410
|
+
rootElement.addEventListener('blur', onBlur, true);
|
|
411
|
+
return () => rootElement.removeEventListener('blur', onBlur, true);
|
|
412
|
+
}
|
|
413
|
+
}),
|
|
414
|
+
editor.registerCommand(
|
|
415
|
+
BLUR_COMMAND,
|
|
416
|
+
() => {
|
|
417
|
+
const rootElement = editor.getRootElement();
|
|
418
|
+
const activeElement = getActiveElementDeep(getRootOwnerDocument(rootElement));
|
|
419
|
+
if (rootElement && isHTMLElement(activeElement) && isOnMenu(activeElement)) {
|
|
420
|
+
rootElement.focus({ preventScroll: true });
|
|
421
|
+
editor.update(() => {
|
|
422
|
+
const selection = $getSelection();
|
|
423
|
+
if (selection !== null && !selection.dirty) {
|
|
424
|
+
selection.dirty = true;
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
return false;
|
|
430
|
+
},
|
|
431
|
+
COMMAND_PRIORITY_HIGH,
|
|
432
|
+
),
|
|
433
|
+
);
|
|
434
|
+
}, [editor, isEditable, isOnMenu]);
|
|
435
|
+
|
|
436
|
+
function onDragStart(event) {
|
|
437
|
+
const dataTransfer = event.dataTransfer;
|
|
438
|
+
if (!dataTransfer || !draggableBlockElem) {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
setDragImage(dataTransfer, draggableBlockElem);
|
|
443
|
+
let nodeKey = '';
|
|
444
|
+
editor.update(() => {
|
|
445
|
+
const node = $getNearestNodeFromDOMNode(draggableBlockElem);
|
|
446
|
+
if (node) {
|
|
447
|
+
nodeKey = node.getKey();
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
isDraggingBlockRef.current = true;
|
|
451
|
+
dataTransfer.setData(DRAG_DATA_FORMAT, nodeKey);
|
|
452
|
+
|
|
453
|
+
if (IS_FIREFOX) {
|
|
454
|
+
const rootElement = editor.getRootElement();
|
|
455
|
+
if (rootElement !== null && getActiveElement(rootElement) !== rootElement) {
|
|
456
|
+
rootElement.focus({ preventScroll: true });
|
|
457
|
+
editor.update(() => {
|
|
458
|
+
const selection = $getSelection();
|
|
459
|
+
if (selection !== null && !selection.dirty) {
|
|
460
|
+
selection.dirty = true;
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function onDragEnd() {
|
|
468
|
+
isDraggingBlockRef.current = false;
|
|
469
|
+
hideTargetLine(targetLineRef.current);
|
|
470
|
+
|
|
471
|
+
if (IS_FIREFOX) {
|
|
472
|
+
editor.focus();
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return createPortal(DraggableContainer, anchorElem, {
|
|
477
|
+
onDragStart,
|
|
478
|
+
onDragEnd,
|
|
479
|
+
isEditable,
|
|
480
|
+
menuComponent,
|
|
481
|
+
targetLineComponent,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export function DraggableBlockPlugin_EXPERIMENTAL(props) {
|
|
486
|
+
const anchorElem = props.anchorElem ?? document.body;
|
|
487
|
+
const [editor] = useLexicalComposerContext();
|
|
488
|
+
return useDraggableBlockMenu(
|
|
489
|
+
editor,
|
|
490
|
+
anchorElem,
|
|
491
|
+
props.menuRef,
|
|
492
|
+
props.targetLineRef,
|
|
493
|
+
editor._editable,
|
|
494
|
+
props.menuComponent,
|
|
495
|
+
props.targetLineComponent,
|
|
496
|
+
props.isOnMenu,
|
|
497
|
+
props.onElementChanged,
|
|
498
|
+
);
|
|
499
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Ported from @lexical/react/src/LexicalEditorRefPlugin.tsx. Hands the editor to
|
|
2
|
+
// a callback ref or object ref so UI outside the composer can reach it.
|
|
3
|
+
import { useLexicalComposerContext } from './LexicalComposerContext';
|
|
4
|
+
import { useEffect } from 'octane';
|
|
5
|
+
|
|
6
|
+
export function EditorRefPlugin(props) {
|
|
7
|
+
const [editor] = useLexicalComposerContext();
|
|
8
|
+
const editorRef = props.editorRef;
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (typeof editorRef === 'function') {
|
|
12
|
+
editorRef(editor);
|
|
13
|
+
} else if (typeof editorRef === 'object' && editorRef !== null) {
|
|
14
|
+
editorRef.current = editor;
|
|
15
|
+
}
|
|
16
|
+
}, [editor]);
|
|
17
|
+
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Ported from @lexical/react/src/LexicalErrorBoundary.tsx. React's class-based
|
|
2
|
+
// boundary becomes octane's <ErrorBoundary> primitive. octane's ErrorBoundary has
|
|
3
|
+
// no `onError` prop, so we route it through the fallback render-prop (invoked with
|
|
4
|
+
// the caught error) — preserving the (error, info) callback Rich/PlainTextPlugin
|
|
5
|
+
// pass for decorator-render failures.
|
|
6
|
+
import { ErrorBoundary } from 'octane';
|
|
7
|
+
|
|
8
|
+
export function LexicalErrorBoundary(props) @{
|
|
9
|
+
<ErrorBoundary
|
|
10
|
+
fallback={(error, _reset) => {
|
|
11
|
+
if (props.onError) {
|
|
12
|
+
props.onError(error instanceof Error ? error : new Error(String(error), { cause: error }), {
|
|
13
|
+
componentStack: '',
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return props.fallback === undefined
|
|
17
|
+
? <div style={{ border: '1px solid #f00', color: '#f00', padding: '8px' }}>
|
|
18
|
+
An error was thrown.
|
|
19
|
+
</div>
|
|
20
|
+
: (props.fallback);
|
|
21
|
+
}}
|
|
22
|
+
>{props.children}</ErrorBoundary>
|
|
23
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Ported from @lexical/react/src/LexicalHashtagPlugin.ts.
|
|
2
|
+
import { HashtagNode, registerLexicalHashtag } from '@lexical/hashtag';
|
|
3
|
+
import { useLexicalComposerContext } from './LexicalComposerContext';
|
|
4
|
+
import { useEffect } from 'octane';
|
|
5
|
+
|
|
6
|
+
export function HashtagPlugin() {
|
|
7
|
+
const [editor] = useLexicalComposerContext();
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (!editor.hasNodes([HashtagNode])) {
|
|
11
|
+
throw new Error('HashtagPlugin: HashtagNode not registered on editor');
|
|
12
|
+
}
|
|
13
|
+
return registerLexicalHashtag(editor);
|
|
14
|
+
}, [editor]);
|
|
15
|
+
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Ported from @lexical/react/src/LexicalHistoryPlugin.ts (+ shared/useHistory.ts
|
|
2
|
+
// inlined — it was a single-use internal hook, so the component's auto-slotted
|
|
3
|
+
// useMemo/useEffect cover it directly).
|
|
4
|
+
import { useLexicalComposerContext } from './LexicalComposerContext';
|
|
5
|
+
import { createEmptyHistoryState, registerHistory } from '@lexical/history';
|
|
6
|
+
import { useEffect, useMemo } from 'octane';
|
|
7
|
+
|
|
8
|
+
export { createEmptyHistoryState } from '@lexical/history';
|
|
9
|
+
|
|
10
|
+
export function HistoryPlugin(props) {
|
|
11
|
+
const [editor] = useLexicalComposerContext();
|
|
12
|
+
const externalHistoryState = props.externalHistoryState;
|
|
13
|
+
const delay =
|
|
14
|
+
props.delay === undefined ? 1000 : props.delay;
|
|
15
|
+
|
|
16
|
+
const historyState = useMemo(() => externalHistoryState || createEmptyHistoryState(), [
|
|
17
|
+
externalHistoryState,
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
useEffect(() => registerHistory(editor, historyState, delay), [delay, editor, historyState]);
|
|
21
|
+
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Ported from @lexical/react/src/LexicalHorizontalRuleNode.tsx. The node's
|
|
2
|
+
// decorate() returns a `<HorizontalRuleComponent/>` — at octane value position
|
|
3
|
+
// that lowers to an element descriptor, which the Decorators portal renders into
|
|
4
|
+
// the <hr> element Lexical created. This is the first decorator node, so it
|
|
5
|
+
// exercises the full decorator portal-map.
|
|
6
|
+
import {
|
|
7
|
+
$isHorizontalRuleNode,
|
|
8
|
+
HorizontalRuleNode as BaseHorizontalRuleNode,
|
|
9
|
+
INSERT_HORIZONTAL_RULE_COMMAND,
|
|
10
|
+
} from '@lexical/extension';
|
|
11
|
+
import { useLexicalComposerContext } from './LexicalComposerContext';
|
|
12
|
+
import { useLexicalNodeSelection } from './useLexicalNodeSelection';
|
|
13
|
+
import {
|
|
14
|
+
$applyNodeReplacement,
|
|
15
|
+
addClassNamesToElement,
|
|
16
|
+
CLICK_COMMAND,
|
|
17
|
+
COMMAND_PRIORITY_LOW,
|
|
18
|
+
getComposedEventTarget,
|
|
19
|
+
mergeRegister,
|
|
20
|
+
removeClassNamesFromElement,
|
|
21
|
+
} from 'lexical';
|
|
22
|
+
import { useEffect } from 'octane';
|
|
23
|
+
|
|
24
|
+
export { $isHorizontalRuleNode, INSERT_HORIZONTAL_RULE_COMMAND } from '@lexical/extension';
|
|
25
|
+
|
|
26
|
+
function HorizontalRuleComponent(props) {
|
|
27
|
+
const nodeKey = props.nodeKey;
|
|
28
|
+
const [editor] = useLexicalComposerContext();
|
|
29
|
+
const [isSelected, setSelected, clearSelection] = useLexicalNodeSelection(nodeKey);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
return mergeRegister(
|
|
33
|
+
editor.registerCommand(
|
|
34
|
+
CLICK_COMMAND,
|
|
35
|
+
(event) => {
|
|
36
|
+
const hrElem = editor.getElementByKey(nodeKey);
|
|
37
|
+
if (getComposedEventTarget(event) === hrElem) {
|
|
38
|
+
if (!event.shiftKey) {
|
|
39
|
+
clearSelection();
|
|
40
|
+
}
|
|
41
|
+
setSelected(!isSelected);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
},
|
|
46
|
+
COMMAND_PRIORITY_LOW,
|
|
47
|
+
),
|
|
48
|
+
);
|
|
49
|
+
}, [clearSelection, editor, isSelected, nodeKey, setSelected]);
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const hrElem = editor.getElementByKey(nodeKey);
|
|
53
|
+
const isSelectedClassName = editor._config.theme.hrSelected ?? 'selected';
|
|
54
|
+
if (hrElem !== null) {
|
|
55
|
+
if (isSelected) {
|
|
56
|
+
addClassNamesToElement(hrElem, isSelectedClassName);
|
|
57
|
+
} else {
|
|
58
|
+
removeClassNamesFromElement(hrElem, isSelectedClassName);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}, [editor, isSelected, nodeKey]);
|
|
62
|
+
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export class HorizontalRuleNode extends BaseHorizontalRuleNode {
|
|
67
|
+
static getType() {
|
|
68
|
+
return 'horizontalrule';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
static clone(node) {
|
|
72
|
+
return new HorizontalRuleNode(node.__key);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static importJSON(serializedNode) {
|
|
76
|
+
return $createHorizontalRuleNode().updateFromJSON(serializedNode);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
static importDOM() {
|
|
80
|
+
return {
|
|
81
|
+
hr: () => ({
|
|
82
|
+
conversion: $convertHorizontalRuleElement,
|
|
83
|
+
priority: 0,
|
|
84
|
+
}),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
decorate() {
|
|
89
|
+
return <HorizontalRuleComponent nodeKey={this.__key} />;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function $convertHorizontalRuleElement() {
|
|
94
|
+
return { node: $createHorizontalRuleNode() };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function $createHorizontalRuleNode() {
|
|
98
|
+
return $applyNodeReplacement(new HorizontalRuleNode());
|
|
99
|
+
}
|