lexical 0.45.0 → 0.45.1-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Lexical.dev.js +213 -148
- package/dist/Lexical.dev.mjs +210 -149
- package/dist/Lexical.js.flow +9 -1
- package/dist/Lexical.mjs +4 -0
- package/dist/Lexical.node.mjs +4 -0
- package/dist/Lexical.prod.js +1 -1
- package/dist/Lexical.prod.mjs +1 -1
- package/dist/LexicalNode.d.ts +8 -0
- package/dist/LexicalUpdates.d.ts +2 -0
- package/dist/LexicalUtils.d.ts +41 -5
- package/dist/index.d.ts +3 -3
- package/dist/nodes/LexicalLineBreakNode.d.ts +18 -0
- package/package.json +2 -2
- package/src/LexicalEditor.ts +10 -9
- package/src/LexicalEvents.ts +4 -5
- package/src/LexicalMutations.ts +0 -6
- package/src/LexicalNode.ts +8 -0
- package/src/LexicalReconciler.ts +8 -0
- package/src/LexicalSelection.ts +10 -11
- package/src/LexicalUpdates.ts +12 -0
- package/src/LexicalUtils.ts +66 -15
- package/src/index.ts +5 -0
- package/src/nodes/LexicalLineBreakNode.ts +18 -2
- package/src/nodes/LexicalTextNode.ts +2 -16
package/dist/LexicalNode.d.ts
CHANGED
|
@@ -179,6 +179,14 @@ export interface LexicalPrivateDOM {
|
|
|
179
179
|
__lexicalLastChildKind?: 'line-break' | 'decorator' | 'empty' | null | undefined;
|
|
180
180
|
__lexicalDir?: 'ltr' | 'rtl' | null | undefined;
|
|
181
181
|
__lexicalUnmanaged?: boolean | undefined;
|
|
182
|
+
/**
|
|
183
|
+
* When true, the DOM subtree owns its own window selection — analogous to
|
|
184
|
+
* a DecoratorNode subtree. Resolution logic that would otherwise force the
|
|
185
|
+
* Lexical selection back onto a managed position treats the caret as
|
|
186
|
+
* intentional and leaves it alone. Set via the `captureSelection` option
|
|
187
|
+
* on {@link setDOMUnmanaged}.
|
|
188
|
+
*/
|
|
189
|
+
__lexicalCapturedSelection?: boolean | undefined;
|
|
182
190
|
}
|
|
183
191
|
export declare function $removeNode(nodeToRemove: LexicalNode, restoreSelection: boolean, preserveEmptyParent?: boolean): void;
|
|
184
192
|
export type DOMConversionProp<T extends HTMLElement> = (node: T) => DOMConversion<T> | null;
|
package/dist/LexicalUpdates.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export declare function isCurrentlyReadOnlyMode(): boolean;
|
|
|
13
13
|
export declare function errorOnReadOnly(): void;
|
|
14
14
|
export declare function errorOnInfiniteTransforms(): void;
|
|
15
15
|
export declare function getActiveEditorState(): EditorState;
|
|
16
|
+
/** @internal */
|
|
17
|
+
export declare function $assumeActiveEditor(editor: LexicalEditor): void;
|
|
16
18
|
export declare function getActiveEditor(): LexicalEditor;
|
|
17
19
|
/**
|
|
18
20
|
* Schedule a full reconcile of the active editor, so that every node is
|
package/dist/LexicalUtils.d.ts
CHANGED
|
@@ -30,7 +30,6 @@ export declare function getRegisteredNode(editor: LexicalEditor, nodeType: strin
|
|
|
30
30
|
export declare const isArray: (arg: any) => arg is any[];
|
|
31
31
|
/** @internal */
|
|
32
32
|
export declare const scheduleMicroTask: (fn: () => void) => void;
|
|
33
|
-
export declare function $isSelectionCapturedInDecorator(node: Node): boolean;
|
|
34
33
|
export declare function isSelectionCapturedInDecoratorInput(anchorDOM: Node): boolean;
|
|
35
34
|
export declare function isSelectionWithinEditor(editor: LexicalEditor, anchorDOM: null | Node, focusDOM: null | Node): boolean;
|
|
36
35
|
/**
|
|
@@ -84,6 +83,7 @@ export declare function $getCompositionKey(): null | NodeKey;
|
|
|
84
83
|
export declare function $getNodeByKey<T extends LexicalNode>(key: NodeKey, _editorState?: EditorState): T | null;
|
|
85
84
|
export declare function $getNodeFromDOMNode(dom: Node, editorState?: EditorState): LexicalNode | null;
|
|
86
85
|
export declare function setNodeKeyOnDOMNode(dom: Node, editor: LexicalEditor, key: NodeKey): void;
|
|
86
|
+
export declare function clearNodeKeyOnDOMNode(dom: Node, editor: LexicalEditor): void;
|
|
87
87
|
export declare function getNodeKeyFromDOMNode(dom: Node, editor: LexicalEditor): NodeKey | undefined;
|
|
88
88
|
export declare function $getNearestNodeFromDOMNode(startingDOM: Node, editorState?: EditorState): LexicalNode | null;
|
|
89
89
|
export declare function cloneDecorators(editor: LexicalEditor): Record<NodeKey, unknown>;
|
|
@@ -408,15 +408,37 @@ export declare function $setDirectionFromDOM<T extends ElementNode>(node: T, dom
|
|
|
408
408
|
* @returns The node, with its align format set when the source `style.textAlign` was valid.
|
|
409
409
|
*/
|
|
410
410
|
export declare function $setFormatFromDOM<T extends ElementNode>(node: T, domNode: HTMLElement): T;
|
|
411
|
+
/**
|
|
412
|
+
* Options accepted by {@link setDOMUnmanaged}.
|
|
413
|
+
*
|
|
414
|
+
* @experimental
|
|
415
|
+
*/
|
|
416
|
+
export interface SetDOMUnmanagedOptions {
|
|
417
|
+
/**
|
|
418
|
+
* When true, the marked subtree owns its own window selection — analogous
|
|
419
|
+
* to a DecoratorNode subtree. Selection resolution that would otherwise
|
|
420
|
+
* mark the selection dirty for a caret position inside unmanaged DOM
|
|
421
|
+
* leaves it alone, so the embedded interaction (custom input, focusable
|
|
422
|
+
* widget, etc.) can keep its native caret.
|
|
423
|
+
*
|
|
424
|
+
* Pass `false` to clear a previously-set marker; omit the field to leave
|
|
425
|
+
* `__lexicalCapturedSelection` untouched.
|
|
426
|
+
*/
|
|
427
|
+
captureSelection?: boolean;
|
|
428
|
+
}
|
|
411
429
|
/**
|
|
412
430
|
* Mark this DOM element as unmanaged by lexical's mutation observer (like
|
|
413
431
|
* decorator nodes are). Extensions that inject non-lexical decoration
|
|
414
432
|
* elements into a node's DOM should mark them so the mutation observer
|
|
415
433
|
* doesn't evict them as "unknown DOM children" during cleanup.
|
|
416
434
|
*
|
|
435
|
+
* Pass `{captureSelection: true}` to additionally treat the subtree's
|
|
436
|
+
* window selection as decorator-like, so resolution does not force-sync
|
|
437
|
+
* the caret out of unmanaged DOM (see {@link isDOMCapturingSelection}).
|
|
438
|
+
*
|
|
417
439
|
* @experimental
|
|
418
440
|
*/
|
|
419
|
-
export declare function setDOMUnmanaged(elementDom: HTMLElement & LexicalPrivateDOM): void;
|
|
441
|
+
export declare function setDOMUnmanaged(elementDom: HTMLElement & LexicalPrivateDOM, options?: SetDOMUnmanagedOptions): void;
|
|
420
442
|
/**
|
|
421
443
|
* True if this DOM node was marked with {@link setDOMUnmanaged}.
|
|
422
444
|
*
|
|
@@ -424,13 +446,27 @@ export declare function setDOMUnmanaged(elementDom: HTMLElement & LexicalPrivate
|
|
|
424
446
|
*/
|
|
425
447
|
export declare function isDOMUnmanaged(elementDom: Node & LexicalPrivateDOM): boolean;
|
|
426
448
|
/**
|
|
427
|
-
*
|
|
449
|
+
* True if the DOM node sits inside a subtree marked with
|
|
450
|
+
* `{captureSelection: true}` via {@link setDOMUnmanaged}. Walks ancestors
|
|
451
|
+
* so any descendant of a marked subtree (e.g. an `<input>` inside a marked
|
|
452
|
+
* `<div>`) reports as captured too.
|
|
453
|
+
*
|
|
454
|
+
* The walk aborts at the first DOM node that corresponds to a Lexical
|
|
455
|
+
* node in `editor` — that boundary is the implicit owner of the subtree's
|
|
456
|
+
* selection, so a captureSelection marker above it (in non-Lexical
|
|
457
|
+
* scaffolding around the editor) does not leak in.
|
|
458
|
+
*
|
|
459
|
+
* DecoratorNode DOM is marked with `setDOMUnmanaged({captureSelection:
|
|
460
|
+
* true})` by the reconciler, so decorator subtrees also report as
|
|
461
|
+
* captured here.
|
|
462
|
+
*
|
|
463
|
+
* @experimental
|
|
428
464
|
*/
|
|
429
|
-
export declare function
|
|
465
|
+
export declare function isDOMCapturingSelection(elementDom: Node & LexicalPrivateDOM, editor: LexicalEditor): boolean;
|
|
430
466
|
/**
|
|
431
467
|
* @internal
|
|
432
468
|
*/
|
|
433
|
-
export declare function
|
|
469
|
+
export declare function hasOwnStaticMethod(klass: Klass<LexicalNode>, k: keyof Klass<LexicalNode>): boolean;
|
|
434
470
|
/** @internal */
|
|
435
471
|
export declare function getStaticNodeConfig(klass: Klass<LexicalNode>): {
|
|
436
472
|
ownNodeType: undefined | string;
|
package/dist/index.d.ts
CHANGED
|
@@ -24,14 +24,14 @@ export { $getState, $getStateChange, $getWritableNodeState, $setState, type AnyS
|
|
|
24
24
|
export { $normalizeSelection as $normalizeSelection__EXPERIMENTAL } from './LexicalNormalization';
|
|
25
25
|
export type { BaseSelection, ElementPointType as ElementPoint, NodeSelection, Point, PointType, RangeSelection, TextPointType as TextPoint, } from './LexicalSelection';
|
|
26
26
|
export { $createNodeSelection, $createPoint, $createRangeSelection, $createRangeSelectionFromDom, $generateNodesFromRawText, $getCharacterOffsets, $getPreviousSelection, $getSelection, $getTextContent, $insertNodes, $isBlockElementNode, $isNodeSelection, $isRangeSelection, $updateDOMSelection, type RawTextVisitor, tokenizeRawText, } from './LexicalSelection';
|
|
27
|
-
export { $fullReconcile, $parseSerializedNode, isCurrentlyReadOnlyMode, } from './LexicalUpdates';
|
|
28
|
-
export { $addUpdateTag, $applyNodeReplacement, $cloneWithProperties, $cloneWithPropertiesEphemeral, $copyNode, $create, $createChildrenArray, $findMatchingParent, $getAdjacentNode, $getDOMSlot, $getDOMTextNode, $getEditor, $getEditorDOMRenderConfig, $getNearestNodeFromDOMNode, $getNearestRootOrShadowRoot, $getNodeByKey, $getNodeByKeyOrThrow, $getNodeFromDOMNode, $getRoot, $hasAncestor, $hasUpdateTag, $isElementDOMSlot, $isInlineElementOrDecoratorNode, $isLeafNode, $isRootOrShadowRoot, $isTokenOrSegmented, $isTokenOrTab, $nodesOfType, $onUpdate, $selectAll, $setCompositionKey, $setDirectionFromDOM, $setFormatFromDOM, $setSelection, $splitNode, getDOMOwnerDocument, getDOMSelection, getDOMSelectionFromTarget, getDOMTextNode, getEditorPropertyFromDOMNode, getNearestEditorFromDOMNode, getRegisteredNode, getRegisteredNodeOrThrow, getStaticNodeConfig, getTextDirection, INTERNAL_$isBlock, isBlockDomNode, isDocumentFragment, isDOMDocumentNode, isDOMNode, isDOMTextNode, isDOMUnmanaged, isExactShortcutMatch, isHTMLAnchorElement, isHTMLElement, isHTMLTableCellElement, isHTMLTableRowElement, isInlineDomNode, isLexicalEditor, isModifierMatch, isSelectionCapturedInDecoratorInput, isSelectionWithinEditor, removeFromParent, resetRandomKey, setDOMUnmanaged, setNodeIndentFromDOM, toggleTextFormatType, } from './LexicalUtils';
|
|
27
|
+
export { $assumeActiveEditor, $fullReconcile, $parseSerializedNode, isCurrentlyReadOnlyMode, } from './LexicalUpdates';
|
|
28
|
+
export { $addUpdateTag, $applyNodeReplacement, $cloneWithProperties, $cloneWithPropertiesEphemeral, $copyNode, $create, $createChildrenArray, $findMatchingParent, $getAdjacentNode, $getDOMSlot, $getDOMTextNode, $getEditor, $getEditorDOMRenderConfig, $getNearestNodeFromDOMNode, $getNearestRootOrShadowRoot, $getNodeByKey, $getNodeByKeyOrThrow, $getNodeFromDOMNode, $getRoot, $hasAncestor, $hasUpdateTag, $isElementDOMSlot, $isInlineElementOrDecoratorNode, $isLeafNode, $isRootOrShadowRoot, $isTokenOrSegmented, $isTokenOrTab, $nodesOfType, $onUpdate, $selectAll, $setCompositionKey, $setDirectionFromDOM, $setFormatFromDOM, $setSelection, $splitNode, getDOMOwnerDocument, getDOMSelection, getDOMSelectionFromTarget, getDOMTextNode, getEditorPropertyFromDOMNode, getNearestEditorFromDOMNode, getRegisteredNode, getRegisteredNodeOrThrow, getStaticNodeConfig, getTextDirection, INTERNAL_$isBlock, isBlockDomNode, isDocumentFragment, isDOMCapturingSelection, isDOMDocumentNode, isDOMNode, isDOMTextNode, isDOMUnmanaged, isExactShortcutMatch, isHTMLAnchorElement, isHTMLElement, isHTMLTableCellElement, isHTMLTableRowElement, isInlineDomNode, isLexicalEditor, isModifierMatch, isSelectionCapturedInDecoratorInput, isSelectionWithinEditor, removeFromParent, resetRandomKey, setDOMUnmanaged, type SetDOMUnmanagedOptions, setNodeIndentFromDOM, toggleTextFormatType, } from './LexicalUtils';
|
|
29
29
|
export { ArtificialNode__DO_NOT_USE } from './nodes/ArtificialNode';
|
|
30
30
|
export { $isDecoratorNode, DecoratorNode } from './nodes/LexicalDecoratorNode';
|
|
31
31
|
export type { ElementFormatType, SerializedElementNode, } from './nodes/LexicalElementNode';
|
|
32
32
|
export { $isElementNode, ElementNode } from './nodes/LexicalElementNode';
|
|
33
33
|
export type { SerializedLineBreakNode } from './nodes/LexicalLineBreakNode';
|
|
34
|
-
export { $createLineBreakNode, $isLineBreakNode, LineBreakNode, } from './nodes/LexicalLineBreakNode';
|
|
34
|
+
export { $createLineBreakNode, $isLineBreakNode, isLastChildInBlockNode, isOnlyChildInBlockNode, LineBreakNode, } from './nodes/LexicalLineBreakNode';
|
|
35
35
|
export type { SerializedParagraphNode } from './nodes/LexicalParagraphNode';
|
|
36
36
|
export { $createParagraphNode, $isParagraphNode, ParagraphNode, } from './nodes/LexicalParagraphNode';
|
|
37
37
|
export type { SerializedRootNode } from './nodes/LexicalRootNode';
|
|
@@ -25,3 +25,21 @@ export declare class LineBreakNode extends LexicalNode {
|
|
|
25
25
|
}
|
|
26
26
|
export declare function $createLineBreakNode(): LineBreakNode;
|
|
27
27
|
export declare function $isLineBreakNode(node: LexicalNode | null | undefined): node is LineBreakNode;
|
|
28
|
+
/**
|
|
29
|
+
* True when `node` is the sole non-whitespace child of a block DOM
|
|
30
|
+
* element. Used by the LineBreak importer to drop stray `<br>` elements
|
|
31
|
+
* that the legacy `$generateNodesFromDOM` also skipped (matches the
|
|
32
|
+
* behavior of `LineBreakNode.importDOM`).
|
|
33
|
+
*
|
|
34
|
+
* @experimental
|
|
35
|
+
*/
|
|
36
|
+
export declare function isOnlyChildInBlockNode(node: Node): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* True when `node` is the trailing non-whitespace child of a block DOM
|
|
39
|
+
* element (excluding the only-child case). Used by the LineBreak
|
|
40
|
+
* importer to drop trailing `<br>` elements like the Apple-interchange
|
|
41
|
+
* clipboard artifact (matches `LineBreakNode.importDOM`).
|
|
42
|
+
*
|
|
43
|
+
* @experimental
|
|
44
|
+
*/
|
|
45
|
+
export declare function isLastChildInBlockNode(node: Node): boolean;
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"rich-text"
|
|
10
10
|
],
|
|
11
11
|
"license": "MIT",
|
|
12
|
-
"version": "0.45.0",
|
|
12
|
+
"version": "0.45.1-dev.0",
|
|
13
13
|
"main": "./dist/Lexical.js",
|
|
14
14
|
"types": "./dist/index.d.ts",
|
|
15
15
|
"repository": {
|
|
@@ -51,6 +51,6 @@
|
|
|
51
51
|
"LICENSE"
|
|
52
52
|
],
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@lexical/internal": "0.45.0"
|
|
54
|
+
"@lexical/internal": "0.45.1-dev.0"
|
|
55
55
|
}
|
|
56
56
|
}
|
package/src/LexicalEditor.ts
CHANGED
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
$addUpdateTag,
|
|
55
55
|
$onUpdate,
|
|
56
56
|
$setSelection,
|
|
57
|
+
clearNodeKeyOnDOMNode,
|
|
57
58
|
createUID,
|
|
58
59
|
dispatchCommand,
|
|
59
60
|
getCachedClassNameArray,
|
|
@@ -62,9 +63,9 @@ import {
|
|
|
62
63
|
getDOMSelection,
|
|
63
64
|
getRegisteredNode,
|
|
64
65
|
getStaticNodeConfig,
|
|
65
|
-
hasOwnExportDOM,
|
|
66
66
|
hasOwnStaticMethod,
|
|
67
67
|
markNodesWithTypesAsDirty,
|
|
68
|
+
setNodeKeyOnDOMNode,
|
|
68
69
|
} from './LexicalUtils';
|
|
69
70
|
import {ArtificialNode__DO_NOT_USE} from './nodes/ArtificialNode';
|
|
70
71
|
import {LineBreakNode} from './nodes/LexicalLineBreakNode';
|
|
@@ -664,11 +665,19 @@ export function resetEditor(
|
|
|
664
665
|
// Remove all the DOM nodes from the root element
|
|
665
666
|
if (prevRootElement !== null) {
|
|
666
667
|
prevRootElement.textContent = '';
|
|
668
|
+
clearNodeKeyOnDOMNode(prevRootElement, editor);
|
|
667
669
|
}
|
|
668
670
|
|
|
669
671
|
if (nextRootElement !== null) {
|
|
670
672
|
nextRootElement.textContent = '';
|
|
671
673
|
keyNodeMap.set('root', nextRootElement);
|
|
674
|
+
// Stash __lexicalKey_${editor._key} = 'root' on the root element so it
|
|
675
|
+
// participates in the unified key lookup (selection resolution in
|
|
676
|
+
// $internalResolveSelectionPoint, mutation handling in
|
|
677
|
+
// $getNearestManagedNodePairFromDOMNode, $getNodeFromDOM, and
|
|
678
|
+
// $getNearestNodeFromDOMNode) instead of requiring a dedicated
|
|
679
|
+
// editor.getRootElement() carveout at each call site.
|
|
680
|
+
setNodeKeyOnDOMNode(nextRootElement, editor, 'root');
|
|
672
681
|
}
|
|
673
682
|
}
|
|
674
683
|
|
|
@@ -854,14 +863,6 @@ export function createEditor(editorConfig?: CreateEditorArgs): LexicalEditor {
|
|
|
854
863
|
console.warn(`${name} must implement static "${method}" method`);
|
|
855
864
|
}
|
|
856
865
|
});
|
|
857
|
-
if (
|
|
858
|
-
!hasOwnStaticMethod(klass, 'importDOM') &&
|
|
859
|
-
hasOwnExportDOM(klass)
|
|
860
|
-
) {
|
|
861
|
-
console.warn(
|
|
862
|
-
`${name} should implement "importDOM" if using a custom "exportDOM" method to ensure HTML serialization (important for copy & paste) works as expected`,
|
|
863
|
-
);
|
|
864
|
-
}
|
|
865
866
|
if (!hasOwnStaticMethod(klass, 'importJSON')) {
|
|
866
867
|
console.warn(
|
|
867
868
|
`${name} should implement "importJSON" method to ensure JSON and default HTML serialization works as expected`,
|
package/src/LexicalEvents.ts
CHANGED
|
@@ -100,7 +100,6 @@ import {
|
|
|
100
100
|
$getAdjacentNode,
|
|
101
101
|
$getDOMTextNode,
|
|
102
102
|
$getNodeByKey,
|
|
103
|
-
$isSelectionCapturedInDecorator,
|
|
104
103
|
$isTokenOrSegmented,
|
|
105
104
|
$isTokenOrTab,
|
|
106
105
|
$setSelection,
|
|
@@ -127,6 +126,7 @@ import {
|
|
|
127
126
|
isDeleteLineForward,
|
|
128
127
|
isDeleteWordBackward,
|
|
129
128
|
isDeleteWordForward,
|
|
129
|
+
isDOMCapturingSelection,
|
|
130
130
|
isDOMNode,
|
|
131
131
|
isDOMTextNode,
|
|
132
132
|
isEscape,
|
|
@@ -560,7 +560,7 @@ function onPointerDown(event: PointerEvent, editor: LexicalEditor) {
|
|
|
560
560
|
updateEditorSync(editor, () => {
|
|
561
561
|
// Drag & drop should not recompute selection until mouse up; otherwise the initially
|
|
562
562
|
// selected content is lost.
|
|
563
|
-
if (
|
|
563
|
+
if (!isDOMCapturingSelection(target, editor)) {
|
|
564
564
|
isSelectionChangeFromMouseDown = true;
|
|
565
565
|
}
|
|
566
566
|
});
|
|
@@ -1082,14 +1082,13 @@ function onInput(event: InputEvent, editor: LexicalEditor): void {
|
|
|
1082
1082
|
}
|
|
1083
1083
|
|
|
1084
1084
|
function $handleInput(event: InputEvent): boolean {
|
|
1085
|
+
const editor = getActiveEditor();
|
|
1085
1086
|
if (
|
|
1086
1087
|
isHTMLElement(event.target) &&
|
|
1087
|
-
|
|
1088
|
+
isDOMCapturingSelection(event.target, editor)
|
|
1088
1089
|
) {
|
|
1089
1090
|
return true;
|
|
1090
1091
|
}
|
|
1091
|
-
|
|
1092
|
-
const editor = getActiveEditor();
|
|
1093
1092
|
const selection = $getSelection();
|
|
1094
1093
|
const data = event.data;
|
|
1095
1094
|
const targetRange = getTargetRange(event);
|
package/src/LexicalMutations.ts
CHANGED
|
@@ -29,7 +29,6 @@ import {
|
|
|
29
29
|
getNodeKeyFromDOMNode,
|
|
30
30
|
getParentElement,
|
|
31
31
|
getWindow,
|
|
32
|
-
internalGetRoot,
|
|
33
32
|
isDOMTextNode,
|
|
34
33
|
isDOMUnmanaged,
|
|
35
34
|
isFirefoxClipboardEvents,
|
|
@@ -118,7 +117,6 @@ function $getNearestManagedNodePairFromDOMNode(
|
|
|
118
117
|
startingDOM: Node,
|
|
119
118
|
editor: LexicalEditor,
|
|
120
119
|
editorState: EditorState,
|
|
121
|
-
rootElement: HTMLElement | null,
|
|
122
120
|
): [HTMLElement, LexicalNode] | undefined {
|
|
123
121
|
for (
|
|
124
122
|
let dom: Node | null = startingDOM;
|
|
@@ -134,8 +132,6 @@ function $getNearestManagedNodePairFromDOMNode(
|
|
|
134
132
|
? undefined
|
|
135
133
|
: [dom, node];
|
|
136
134
|
}
|
|
137
|
-
} else if (dom === rootElement) {
|
|
138
|
-
return [rootElement, internalGetRoot(editorState)];
|
|
139
135
|
}
|
|
140
136
|
}
|
|
141
137
|
}
|
|
@@ -152,7 +148,6 @@ function flushMutations(
|
|
|
152
148
|
updateEditorSync(editor, () => {
|
|
153
149
|
const selection = $getSelection() || getLastSelection(editor);
|
|
154
150
|
const badDOMTargets = new Map<HTMLElement, LexicalNode>();
|
|
155
|
-
const rootElement = editor.getRootElement();
|
|
156
151
|
// We use the current editor state, as that reflects what is
|
|
157
152
|
// actually "on screen".
|
|
158
153
|
const currentEditorState = editor._editorState;
|
|
@@ -168,7 +163,6 @@ function flushMutations(
|
|
|
168
163
|
targetDOM,
|
|
169
164
|
editor,
|
|
170
165
|
currentEditorState,
|
|
171
|
-
rootElement,
|
|
172
166
|
);
|
|
173
167
|
if (!pair) {
|
|
174
168
|
continue;
|
package/src/LexicalNode.ts
CHANGED
|
@@ -272,6 +272,14 @@ export interface LexicalPrivateDOM {
|
|
|
272
272
|
| undefined;
|
|
273
273
|
__lexicalDir?: 'ltr' | 'rtl' | null | undefined;
|
|
274
274
|
__lexicalUnmanaged?: boolean | undefined;
|
|
275
|
+
/**
|
|
276
|
+
* When true, the DOM subtree owns its own window selection — analogous to
|
|
277
|
+
* a DecoratorNode subtree. Resolution logic that would otherwise force the
|
|
278
|
+
* Lexical selection back onto a managed position treats the caret as
|
|
279
|
+
* intentional and leaves it alone. Set via the `captureSelection` option
|
|
280
|
+
* on {@link setDOMUnmanaged}.
|
|
281
|
+
*/
|
|
282
|
+
__lexicalCapturedSelection?: boolean | undefined;
|
|
275
283
|
}
|
|
276
284
|
|
|
277
285
|
export function $removeNode(
|
package/src/LexicalReconciler.ts
CHANGED
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
$isRootOrShadowRoot,
|
|
52
52
|
cloneDecorators,
|
|
53
53
|
getElementByKeyOrThrow,
|
|
54
|
+
setDOMUnmanaged,
|
|
54
55
|
setMutatedNode,
|
|
55
56
|
setNodeKeyOnDOMNode,
|
|
56
57
|
} from './LexicalUtils';
|
|
@@ -458,6 +459,13 @@ function $createNode(key: NodeKey, slot: ElementDOMSlot | null): HTMLElement {
|
|
|
458
459
|
dom.setAttribute('data-lexical-text', 'true');
|
|
459
460
|
} else if ($isDecoratorNode(node)) {
|
|
460
461
|
dom.setAttribute('data-lexical-decorator', 'true');
|
|
462
|
+
// DecoratorNode DOM is selection-captured: window selection inside
|
|
463
|
+
// a decorator subtree (e.g. an embedded input) is owned by the
|
|
464
|
+
// decorator, not by Lexical's caret management. Marking it via
|
|
465
|
+
// setDOMUnmanaged unifies the decorator case with extension-owned
|
|
466
|
+
// unmanaged subtrees so callers only need isDOMCapturingSelection /
|
|
467
|
+
// isDOMUnmanaged.
|
|
468
|
+
setDOMUnmanaged(dom, {captureSelection: true});
|
|
461
469
|
}
|
|
462
470
|
|
|
463
471
|
if ($isElementNode(node)) {
|
package/src/LexicalSelection.ts
CHANGED
|
@@ -80,7 +80,6 @@ import {
|
|
|
80
80
|
$getRoot,
|
|
81
81
|
$hasAncestor,
|
|
82
82
|
$isRootOrShadowRoot,
|
|
83
|
-
$isSelectionCapturedInDecorator,
|
|
84
83
|
$isTokenOrSegmented,
|
|
85
84
|
$isTokenOrTab,
|
|
86
85
|
$setCompositionKey,
|
|
@@ -90,6 +89,7 @@ import {
|
|
|
90
89
|
getNodeKeyFromDOMNode,
|
|
91
90
|
getWindow,
|
|
92
91
|
INTERNAL_$isBlock,
|
|
92
|
+
isDOMCapturingSelection,
|
|
93
93
|
isHTMLElement,
|
|
94
94
|
isSelectionCapturedInDecoratorInput,
|
|
95
95
|
isSelectionWithinEditor,
|
|
@@ -2322,8 +2322,7 @@ function $internalResolveSelectionPoint(
|
|
|
2322
2322
|
}
|
|
2323
2323
|
if (
|
|
2324
2324
|
getNodeKeyFromDOMNode(dom, editor) === undefined &&
|
|
2325
|
-
dom
|
|
2326
|
-
!$isSelectionCapturedInDecorator(dom)
|
|
2325
|
+
!isDOMCapturingSelection(dom, editor)
|
|
2327
2326
|
) {
|
|
2328
2327
|
// The DOM caret is sitting on a node that has no Lexical key
|
|
2329
2328
|
// (e.g. <col> inside an unmanaged <colgroup>, or any unmanaged
|
|
@@ -2334,14 +2333,14 @@ function $internalResolveSelectionPoint(
|
|
|
2334
2333
|
// selection dirty so the reconciler writes a valid DOM caret back
|
|
2335
2334
|
// at the resolved Lexical position.
|
|
2336
2335
|
//
|
|
2337
|
-
//
|
|
2338
|
-
//
|
|
2339
|
-
//
|
|
2340
|
-
//
|
|
2341
|
-
// -
|
|
2342
|
-
//
|
|
2343
|
-
//
|
|
2344
|
-
//
|
|
2336
|
+
// Exclusions split across the two guard clauses:
|
|
2337
|
+
// - The first clause (`key !== undefined`) covers any DOM node
|
|
2338
|
+
// with a `__lexicalKey_*` attribute — Lexical-managed elements
|
|
2339
|
+
// and the editor root (stashed in `resetEditor`).
|
|
2340
|
+
// - `isDOMCapturingSelection` covers DecoratorNode subtrees (which
|
|
2341
|
+
// own their own DOM) and subtrees marked via
|
|
2342
|
+
// `setDOMUnmanaged(dom, {captureSelection: true})` —
|
|
2343
|
+
// extension-owned widgets that keep a native caret.
|
|
2345
2344
|
//
|
|
2346
2345
|
// Void elements that ARE Lexical nodes (LineBreakNode <br>,
|
|
2347
2346
|
// empty decorator containers, etc.) have keys, so this check
|
package/src/LexicalUpdates.ts
CHANGED
|
@@ -115,6 +115,18 @@ export function getActiveEditorState(): EditorState {
|
|
|
115
115
|
return activeEditorState;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
/** @internal */
|
|
119
|
+
export function $assumeActiveEditor(editor: LexicalEditor): void {
|
|
120
|
+
// Throw if called outside of an update
|
|
121
|
+
if (getActiveEditorState() !== null && activeEditor === null) {
|
|
122
|
+
activeEditor = editor;
|
|
123
|
+
}
|
|
124
|
+
invariant(
|
|
125
|
+
activeEditor === editor,
|
|
126
|
+
'The given editor argument does not match $getEditor() in this context. Use editor.getEditorState().read(..., {editor}) if this cross-editor call is intentional.',
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
118
130
|
export function getActiveEditor(): LexicalEditor {
|
|
119
131
|
if (activeEditor === null) {
|
|
120
132
|
invariant(
|
package/src/LexicalUtils.ts
CHANGED
|
@@ -159,10 +159,6 @@ export const scheduleMicroTask: (fn: () => void) => void =
|
|
|
159
159
|
Promise.resolve().then(fn);
|
|
160
160
|
};
|
|
161
161
|
|
|
162
|
-
export function $isSelectionCapturedInDecorator(node: Node): boolean {
|
|
163
|
-
return $isDecoratorNode($getNearestNodeFromDOMNode(node));
|
|
164
|
-
}
|
|
165
|
-
|
|
166
162
|
export function isSelectionCapturedInDecoratorInput(anchorDOM: Node): boolean {
|
|
167
163
|
const activeElement = document.activeElement;
|
|
168
164
|
|
|
@@ -571,6 +567,11 @@ export function setNodeKeyOnDOMNode(
|
|
|
571
567
|
(dom as Node & Record<typeof prop, NodeKey | undefined>)[prop] = key;
|
|
572
568
|
}
|
|
573
569
|
|
|
570
|
+
export function clearNodeKeyOnDOMNode(dom: Node, editor: LexicalEditor) {
|
|
571
|
+
const prop = `__lexicalKey_${editor._key}`;
|
|
572
|
+
delete (dom as Node & Record<typeof prop, NodeKey | undefined>)[prop];
|
|
573
|
+
}
|
|
574
|
+
|
|
574
575
|
export function getNodeKeyFromDOMNode(
|
|
575
576
|
dom: Node,
|
|
576
577
|
editor: LexicalEditor,
|
|
@@ -683,10 +684,6 @@ export function $getNodeFromDOM(dom: Node): null | LexicalNode {
|
|
|
683
684
|
const editor = getActiveEditor();
|
|
684
685
|
const nodeKey = getNodeKeyFromDOMTree(dom, editor);
|
|
685
686
|
if (nodeKey === null) {
|
|
686
|
-
const rootElement = editor.getRootElement();
|
|
687
|
-
if (dom === rootElement) {
|
|
688
|
-
return $getNodeByKey('root');
|
|
689
|
-
}
|
|
690
687
|
return null;
|
|
691
688
|
}
|
|
692
689
|
return $getNodeByKey(nodeKey);
|
|
@@ -2265,18 +2262,45 @@ export function $setFormatFromDOM<T extends ElementNode>(
|
|
|
2265
2262
|
: node;
|
|
2266
2263
|
}
|
|
2267
2264
|
|
|
2265
|
+
/**
|
|
2266
|
+
* Options accepted by {@link setDOMUnmanaged}.
|
|
2267
|
+
*
|
|
2268
|
+
* @experimental
|
|
2269
|
+
*/
|
|
2270
|
+
export interface SetDOMUnmanagedOptions {
|
|
2271
|
+
/**
|
|
2272
|
+
* When true, the marked subtree owns its own window selection — analogous
|
|
2273
|
+
* to a DecoratorNode subtree. Selection resolution that would otherwise
|
|
2274
|
+
* mark the selection dirty for a caret position inside unmanaged DOM
|
|
2275
|
+
* leaves it alone, so the embedded interaction (custom input, focusable
|
|
2276
|
+
* widget, etc.) can keep its native caret.
|
|
2277
|
+
*
|
|
2278
|
+
* Pass `false` to clear a previously-set marker; omit the field to leave
|
|
2279
|
+
* `__lexicalCapturedSelection` untouched.
|
|
2280
|
+
*/
|
|
2281
|
+
captureSelection?: boolean;
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2268
2284
|
/**
|
|
2269
2285
|
* Mark this DOM element as unmanaged by lexical's mutation observer (like
|
|
2270
2286
|
* decorator nodes are). Extensions that inject non-lexical decoration
|
|
2271
2287
|
* elements into a node's DOM should mark them so the mutation observer
|
|
2272
2288
|
* doesn't evict them as "unknown DOM children" during cleanup.
|
|
2273
2289
|
*
|
|
2290
|
+
* Pass `{captureSelection: true}` to additionally treat the subtree's
|
|
2291
|
+
* window selection as decorator-like, so resolution does not force-sync
|
|
2292
|
+
* the caret out of unmanaged DOM (see {@link isDOMCapturingSelection}).
|
|
2293
|
+
*
|
|
2274
2294
|
* @experimental
|
|
2275
2295
|
*/
|
|
2276
2296
|
export function setDOMUnmanaged(
|
|
2277
2297
|
elementDom: HTMLElement & LexicalPrivateDOM,
|
|
2298
|
+
options?: SetDOMUnmanagedOptions,
|
|
2278
2299
|
): void {
|
|
2279
2300
|
elementDom.__lexicalUnmanaged = true;
|
|
2301
|
+
if (options && options.captureSelection !== undefined) {
|
|
2302
|
+
elementDom.__lexicalCapturedSelection = options.captureSelection;
|
|
2303
|
+
}
|
|
2280
2304
|
}
|
|
2281
2305
|
|
|
2282
2306
|
/**
|
|
@@ -2288,6 +2312,40 @@ export function isDOMUnmanaged(elementDom: Node & LexicalPrivateDOM): boolean {
|
|
|
2288
2312
|
return elementDom.__lexicalUnmanaged === true;
|
|
2289
2313
|
}
|
|
2290
2314
|
|
|
2315
|
+
/**
|
|
2316
|
+
* True if the DOM node sits inside a subtree marked with
|
|
2317
|
+
* `{captureSelection: true}` via {@link setDOMUnmanaged}. Walks ancestors
|
|
2318
|
+
* so any descendant of a marked subtree (e.g. an `<input>` inside a marked
|
|
2319
|
+
* `<div>`) reports as captured too.
|
|
2320
|
+
*
|
|
2321
|
+
* The walk aborts at the first DOM node that corresponds to a Lexical
|
|
2322
|
+
* node in `editor` — that boundary is the implicit owner of the subtree's
|
|
2323
|
+
* selection, so a captureSelection marker above it (in non-Lexical
|
|
2324
|
+
* scaffolding around the editor) does not leak in.
|
|
2325
|
+
*
|
|
2326
|
+
* DecoratorNode DOM is marked with `setDOMUnmanaged({captureSelection:
|
|
2327
|
+
* true})` by the reconciler, so decorator subtrees also report as
|
|
2328
|
+
* captured here.
|
|
2329
|
+
*
|
|
2330
|
+
* @experimental
|
|
2331
|
+
*/
|
|
2332
|
+
export function isDOMCapturingSelection(
|
|
2333
|
+
elementDom: Node & LexicalPrivateDOM,
|
|
2334
|
+
editor: LexicalEditor,
|
|
2335
|
+
): boolean {
|
|
2336
|
+
let dom: (Node & LexicalPrivateDOM) | null = elementDom;
|
|
2337
|
+
while (dom != null) {
|
|
2338
|
+
if (dom.__lexicalCapturedSelection === true) {
|
|
2339
|
+
return true;
|
|
2340
|
+
}
|
|
2341
|
+
if (getNodeKeyFromDOMNode(dom, editor) !== undefined) {
|
|
2342
|
+
return false;
|
|
2343
|
+
}
|
|
2344
|
+
dom = getParentElement(dom);
|
|
2345
|
+
}
|
|
2346
|
+
return false;
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2291
2349
|
/**
|
|
2292
2350
|
* @internal
|
|
2293
2351
|
*
|
|
@@ -2307,13 +2365,6 @@ export function hasOwnStaticMethod(
|
|
|
2307
2365
|
return hasOwn(klass, k) && klass[k] !== LexicalNode[k];
|
|
2308
2366
|
}
|
|
2309
2367
|
|
|
2310
|
-
/**
|
|
2311
|
-
* @internal
|
|
2312
|
-
*/
|
|
2313
|
-
export function hasOwnExportDOM(klass: Klass<LexicalNode>) {
|
|
2314
|
-
return hasOwn(klass.prototype, 'exportDOM');
|
|
2315
|
-
}
|
|
2316
|
-
|
|
2317
2368
|
/** @internal */
|
|
2318
2369
|
function isAbstractNodeClass(klass: Klass<LexicalNode>): boolean {
|
|
2319
2370
|
if (!(klass === LexicalNode || klass.prototype instanceof LexicalNode)) {
|
package/src/index.ts
CHANGED
|
@@ -255,6 +255,7 @@ export {
|
|
|
255
255
|
tokenizeRawText,
|
|
256
256
|
} from './LexicalSelection';
|
|
257
257
|
export {
|
|
258
|
+
$assumeActiveEditor,
|
|
258
259
|
$fullReconcile,
|
|
259
260
|
$parseSerializedNode,
|
|
260
261
|
isCurrentlyReadOnlyMode,
|
|
@@ -308,6 +309,7 @@ export {
|
|
|
308
309
|
INTERNAL_$isBlock,
|
|
309
310
|
isBlockDomNode,
|
|
310
311
|
isDocumentFragment,
|
|
312
|
+
isDOMCapturingSelection,
|
|
311
313
|
isDOMDocumentNode,
|
|
312
314
|
isDOMNode,
|
|
313
315
|
isDOMTextNode,
|
|
@@ -325,6 +327,7 @@ export {
|
|
|
325
327
|
removeFromParent,
|
|
326
328
|
resetRandomKey,
|
|
327
329
|
setDOMUnmanaged,
|
|
330
|
+
type SetDOMUnmanagedOptions,
|
|
328
331
|
setNodeIndentFromDOM,
|
|
329
332
|
toggleTextFormatType,
|
|
330
333
|
} from './LexicalUtils';
|
|
@@ -339,6 +342,8 @@ export type {SerializedLineBreakNode} from './nodes/LexicalLineBreakNode';
|
|
|
339
342
|
export {
|
|
340
343
|
$createLineBreakNode,
|
|
341
344
|
$isLineBreakNode,
|
|
345
|
+
isLastChildInBlockNode,
|
|
346
|
+
isOnlyChildInBlockNode,
|
|
342
347
|
LineBreakNode,
|
|
343
348
|
} from './nodes/LexicalLineBreakNode';
|
|
344
349
|
export type {SerializedParagraphNode} from './nodes/LexicalParagraphNode';
|
|
@@ -90,7 +90,15 @@ export function $isLineBreakNode(
|
|
|
90
90
|
return node instanceof LineBreakNode;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
/**
|
|
94
|
+
* True when `node` is the sole non-whitespace child of a block DOM
|
|
95
|
+
* element. Used by the LineBreak importer to drop stray `<br>` elements
|
|
96
|
+
* that the legacy `$generateNodesFromDOM` also skipped (matches the
|
|
97
|
+
* behavior of `LineBreakNode.importDOM`).
|
|
98
|
+
*
|
|
99
|
+
* @experimental
|
|
100
|
+
*/
|
|
101
|
+
export function isOnlyChildInBlockNode(node: Node): boolean {
|
|
94
102
|
const parentElement = node.parentElement;
|
|
95
103
|
if (parentElement !== null && isBlockDomNode(parentElement)) {
|
|
96
104
|
const firstChild = parentElement.firstChild!;
|
|
@@ -111,7 +119,15 @@ function isOnlyChildInBlockNode(node: Node): boolean {
|
|
|
111
119
|
return false;
|
|
112
120
|
}
|
|
113
121
|
|
|
114
|
-
|
|
122
|
+
/**
|
|
123
|
+
* True when `node` is the trailing non-whitespace child of a block DOM
|
|
124
|
+
* element (excluding the only-child case). Used by the LineBreak
|
|
125
|
+
* importer to drop trailing `<br>` elements like the Apple-interchange
|
|
126
|
+
* clipboard artifact (matches `LineBreakNode.importDOM`).
|
|
127
|
+
*
|
|
128
|
+
* @experimental
|
|
129
|
+
*/
|
|
130
|
+
export function isLastChildInBlockNode(node: Node): boolean {
|
|
115
131
|
const parentElement = node.parentElement;
|
|
116
132
|
if (parentElement !== null && isBlockDomNode(parentElement)) {
|
|
117
133
|
// check if node is first child, because only child dont count
|
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
import {LexicalNode} from '../LexicalNode';
|
|
54
54
|
import {$cloneNodeState} from '../LexicalNodeState';
|
|
55
55
|
import {
|
|
56
|
+
$generateNodesFromRawText,
|
|
56
57
|
$getSelection,
|
|
57
58
|
$internalMakeRangeSelection,
|
|
58
59
|
$isRangeSelection,
|
|
@@ -74,8 +75,6 @@ import {
|
|
|
74
75
|
toggleTextFormatType,
|
|
75
76
|
} from '../LexicalUtils';
|
|
76
77
|
import {setDOMStyleFromCSS} from '../utils/setDOMStyle';
|
|
77
|
-
import {$createLineBreakNode} from './LexicalLineBreakNode';
|
|
78
|
-
import {$createTabNode} from './LexicalTabNode';
|
|
79
78
|
|
|
80
79
|
export type SerializedTextNode = Spread<
|
|
81
80
|
{
|
|
@@ -1262,20 +1261,7 @@ function $convertTextDOMNode(domNode: Node): DOMConversionOutput {
|
|
|
1262
1261
|
let textContent = domNode_.textContent || '';
|
|
1263
1262
|
// No collapse and preserve segment break for pre, pre-wrap and pre-line
|
|
1264
1263
|
if (findParentPreDOMNode(domNode_) !== null) {
|
|
1265
|
-
|
|
1266
|
-
const nodes: Array<LexicalNode> = [];
|
|
1267
|
-
const length = parts.length;
|
|
1268
|
-
for (let i = 0; i < length; i++) {
|
|
1269
|
-
const part = parts[i];
|
|
1270
|
-
if (part === '\n' || part === '\r\n') {
|
|
1271
|
-
nodes.push($createLineBreakNode());
|
|
1272
|
-
} else if (part === '\t') {
|
|
1273
|
-
nodes.push($createTabNode());
|
|
1274
|
-
} else if (part !== '') {
|
|
1275
|
-
nodes.push($createTextNode(part));
|
|
1276
|
-
}
|
|
1277
|
-
}
|
|
1278
|
-
return {node: nodes};
|
|
1264
|
+
return {node: $generateNodesFromRawText(textContent)};
|
|
1279
1265
|
}
|
|
1280
1266
|
textContent = textContent.replace(/\r/g, '').replace(/[ \t\n]+/g, ' ');
|
|
1281
1267
|
if (textContent === '') {
|