Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): add experimental decorator element node and nested root node #5981

Closed
wants to merge 8 commits into from
179 changes: 179 additions & 0 deletions packages/lexical-playground/src/nodes/CardNode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/**
* 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,
$isElementNode,
EXPERIMENTAL_DecoratorElementNode,
LexicalNode,
NodeKey,
} from 'lexical';
import * as React from 'react';

export class CardNode extends EXPERIMENTAL_DecoratorElementNode<JSX.Element> {
__showBody: boolean;

static getType(): string {
return 'card';
}

static clone(node: CardNode): CardNode {
return new CardNode(node.__showBody, node.__key);
}

constructor(showBody = false, key?: NodeKey) {
super(key);

this.__showBody = showBody;

// ElementNode will automatically clone children
// So we only need to set the children if the node is new
if (key === undefined) {
const title = $createNestedRootNode();
const titleParagraph = $createParagraphNode();
titleParagraph.append($createTextNode('Title sample text'));
title.append(titleParagraph);
this.append(title);
const body = $createNestedRootNode();
const bodyParagraph = $createParagraphNode();
bodyParagraph.append($createTextNode('Content sample text'));
body.append(bodyParagraph);
this.append(body);
}
}

toggleBody(): void {
const writableSelf = this.getWritable();
writableSelf.__showBody = !writableSelf.__showBody;
}

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

updateDOM(): false {
return false;
}

decorate(): JSX.Element {
return (
<CardComponent
nodeKey={this.__key}
titleKey={this.getChildAtIndex(0)!.__key}
bodyKey={this.getChildAtIndex(1)!.__key}
showBody={this.__showBody}
/>
);
}
}

export function $createCardNode(): CardNode {
return $applyNodeReplacement(new CardNode());
}

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

function CardComponent({
nodeKey,
titleKey,
bodyKey,
showBody,
}: {
nodeKey: NodeKey;
titleKey: NodeKey;
bodyKey: NodeKey;
showBody: boolean;
}) {
const [editor] = useLexicalComposerContext();
const onTitleRef = React.useCallback(
(element: null | HTMLElement) => {
editor.setNestedRootElement(titleKey, element);
},
[editor, titleKey],
);
const onBodyRef = React.useCallback(
(element: null | HTMLElement) => {
editor.setNestedRootElement(bodyKey, element);
},
[editor, bodyKey],
);
const toggleBody = React.useCallback(() => {
editor.update(() => {
const node = $getNodeByKey(nodeKey);
if ($isCardNode(node)) {
node.toggleBody();
}
});
}, [editor, nodeKey]);
const addText = React.useCallback(() => {
editor.update(() => {
const node = $getNodeByKey(bodyKey);
if ($isElementNode(node)) {
const paragraph = node.getFirstChildOrThrow();
if ($isElementNode(paragraph)) {
paragraph.append($createTextNode('abcdef'));
}
}
});
}, [bodyKey, editor]);
const removeNode = React.useCallback(() => {
editor.update(() => {
const node = $getNodeByKey(nodeKey);
if ($isCardNode(node)) {
node.remove();
}
});
}, [editor, nodeKey]);
return (
<div
style={{
borderColor: 'black',
borderStyle: 'solid',
borderWidth: 1,
}}>
<div>Title</div>
<div
style={{
borderColor: 'red',
borderStyle: 'solid',
borderWidth: 1,
minHeight: 100,
minWidth: 300,
}}
ref={onTitleRef}
/>
<button onClick={toggleBody}>Toggle Body</button>
<button onClick={addText}>Add text to Body</button>
<button onClick={removeNode}>Remove current node</button>
{showBody && (
<>
<div>Body</div>
<div
style={{
borderColor: 'blue',
borderStyle: 'solid',
borderWidth: 1,
minHeight: 100,
minWidth: 300,
}}
ref={onBodyRef}
/>
</>
)}
</div>
);
}
7 changes: 5 additions & 2 deletions packages/lexical-playground/src/nodes/PlaygroundNodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
*
*/

import type {Klass, LexicalNode} from 'lexical';

import {CodeHighlightNode, CodeNode} from '@lexical/code';
import {HashtagNode} from '@lexical/hashtag';
import {AutoLinkNode, LinkNode} from '@lexical/link';
Expand All @@ -17,11 +15,13 @@ import {OverflowNode} from '@lexical/overflow';
import {HorizontalRuleNode} from '@lexical/react/LexicalHorizontalRuleNode';
import {HeadingNode, QuoteNode} from '@lexical/rich-text';
import {TableCellNode, TableNode, TableRowNode} from '@lexical/table';
import {type Klass, type LexicalNode} from 'lexical';

import {CollapsibleContainerNode} from '../plugins/CollapsiblePlugin/CollapsibleContainerNode';
import {CollapsibleContentNode} from '../plugins/CollapsiblePlugin/CollapsibleContentNode';
import {CollapsibleTitleNode} from '../plugins/CollapsiblePlugin/CollapsibleTitleNode';
import {AutocompleteNode} from './AutocompleteNode';
import {CardNode} from './CardNode';
import {EmojiNode} from './EmojiNode';
import {EquationNode} from './EquationNode';
import {ExcalidrawNode} from './ExcalidrawNode';
Expand All @@ -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 @@ -73,6 +74,8 @@ const PlaygroundNodes: Array<Klass<LexicalNode>> = [
PageBreakNode,
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>
);
}
Loading
Loading