Skip to content

Commit

Permalink
feat(core): support node with changeable children
Browse files Browse the repository at this point in the history
  • Loading branch information
yf-yang committed Apr 29, 2024
1 parent 1616562 commit 5c832a1
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 2 deletions.
2 changes: 2 additions & 0 deletions packages/lexical-playground/src/nodes/PlaygroundNodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {LayoutItemNode} from './LayoutItemNode';
import {MentionNode} from './MentionNode';
import {PageBreakNode} from './PageBreakNode';
import {PollNode} from './PollNode';
import {ReactListNode} from './ReactListNode';
import {StickyNode} from './StickyNode';
import {TweetNode} from './TweetNode';
import {YouTubeNode} from './YouTubeNode';
Expand Down Expand Up @@ -74,6 +75,7 @@ const PlaygroundNodes: Array<Klass<LexicalNode>> = [
LayoutContainerNode,
LayoutItemNode,
CardNode,
ReactListNode,
];

export default PlaygroundNodes;
143 changes: 143 additions & 0 deletions packages/lexical-playground/src/nodes/ReactListNode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {
$applyNodeReplacement,
$createNestedRootNode,
$createParagraphNode,
$createTextNode,
$getNodeByKey,
$isNestedRootNode,
EXPERIMENTAL_DecoratorElementNode,
LexicalNode,
NodeKey,
} from 'lexical';
import * as React from 'react';

export class ReactListNode extends EXPERIMENTAL_DecoratorElementNode<JSX.Element> {
static getType(): string {
return 'reactlist';
}

static clone(node: ReactListNode): ReactListNode {
return new ReactListNode(node.__key);
}

constructor(key?: NodeKey) {
super(key);
}

createDOM(): HTMLElement {
return document.createElement('div');
}

updateDOM(): false {
return false;
}

decorate(): JSX.Element {
return <ReactListComponent nodeKey={this.__key} />;
}
}

export function $createReactListNode(): ReactListNode {
return $applyNodeReplacement(new ReactListNode());
}

export function $isReactListNode(
node: LexicalNode | null | undefined,
): node is ReactListNode {
return node instanceof ReactListNode;
}

function ReactListComponent({nodeKey}: {nodeKey: NodeKey}) {
const [editor] = useLexicalComposerContext();
const addAtBeginning = React.useCallback(() => {
editor.update(() => {
const node = $getNodeByKey(nodeKey);
if ($isReactListNode(node)) {
const paragraph = $createParagraphNode();
paragraph.append($createTextNode('New text'));
const nestedRoot = $createNestedRootNode();
nestedRoot.append(paragraph);
node.splice(0, 0, [nestedRoot]);
}
});
}, [editor, nodeKey]);
const childKeys = editor.getEditorState().read(() => {
const node = $getNodeByKey(nodeKey);
return $isReactListNode(node) ? node.getChildrenKeys() : [];
});
return (
<div
style={{
borderColor: 'black',
borderStyle: 'solid',
borderWidth: 1,
}}>
<button onClick={addAtBeginning}>Add one item at beginning</button>
{childKeys.map((childKey, index) => (
<ReactListItem key={childKey} index={index} nodeKey={childKey} />
))}
</div>
);
}

function ReactListItem({
index,
nodeKey,
}: {
index: number;
nodeKey: NodeKey;
}): JSX.Element {
const [editor] = useLexicalComposerContext();
const onRef = React.useCallback(
(element: HTMLElement | null) => {
editor.setNestedRootElement(nodeKey, element);
},
[editor, nodeKey],
);
const removeSelf = React.useCallback(() => {
editor.update(() => {
const node = $getNodeByKey(nodeKey);
if ($isNestedRootNode(node)) {
node.remove();
}
});
}, [editor, nodeKey]);
const addOneAfter = React.useCallback(() => {
editor.update(() => {
const node = $getNodeByKey(nodeKey);
if ($isNestedRootNode(node)) {
const paragraph = $createParagraphNode();
paragraph.append($createTextNode('New text'));
const nestedRoot = $createNestedRootNode();
nestedRoot.append(paragraph);
node.insertAfter(nestedRoot);
}
});
}, [editor, nodeKey]);
return (
<div>
Item No.{index} Key:{nodeKey}
<div
style={{
borderColor: 'red',
borderStyle: 'solid',
borderWidth: 1,
minHeight: 30,
minWidth: 100,
}}
ref={onRef}
/>
<button onClick={removeSelf}>Remove</button>
<button onClick={addOneAfter}>Add one after</button>
</div>
);
}
11 changes: 11 additions & 0 deletions packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ import {IS_APPLE} from 'shared/environment';
import useModal from '../../hooks/useModal';
import catTypingGif from '../../images/cat-typing.gif';
import {$createCardNode} from '../../nodes/CardNode';
import {$createReactListNode} from '../../nodes/ReactListNode';
import {$createStickyNode} from '../../nodes/StickyNode';
import DropDown, {DropDownItem} from '../../ui/DropDown';
import DropdownColorPicker from '../../ui/DropdownColorPicker';
Expand Down Expand Up @@ -1040,6 +1041,16 @@ export default function ToolbarPlugin({
buttonLabel="Insert"
buttonAriaLabel="Insert specialized editor node"
buttonIconClassName="icon plus">
<DropDownItem
onClick={() => {
activeEditor.update(() => {
const reactListNode = $createReactListNode();
$insertNodeToNearestRoot(reactListNode);
});
}}
className="item">
<span className="text">React List Node</span>
</DropDownItem>
<DropDownItem
onClick={() => {
activeEditor.update(() => {
Expand Down
10 changes: 8 additions & 2 deletions packages/lexical/src/LexicalEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1043,12 +1043,18 @@ export class LexicalEditor {
style.whiteSpace = 'pre-wrap';
style.wordBreak = 'break-word';
this._keyToDOMMap.set(key, nextNestedRootElement);
this._pendingNestedRootNodeKeys.add(key);
} else {
this._keyToDOMMap.delete(key);
// There are two cases that nextNestedRootElement is null:
// 1. The nested root node is removed from Lexical
// 2. The nested root node remains unchanged, but the HTML Element no longer exists
// Only in the second case, we need to further process Lexical node children.
if (this._editorState._nodeMap.get(key)) {
this._pendingNestedRootNodeKeys.add(key);
}
}

this._pendingNestedRootNodeKeys.add(key);

// Defer to next microtask, so multiple simultaneous root element update
// can trigger only one reconciliation.
queueMicrotask(() => {
Expand Down

0 comments on commit 5c832a1

Please sign in to comment.