facebook / lexical

Lexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.
https://lexical.dev
MIT License
17.5k stars 1.45k forks source link

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

Open yf-yang opened 2 weeks ago

yf-yang commented 2 weeks ago

DecoratorElementNode

This is a VERY EARLY draft of DecoratorElementNode (#5930).

I submit this PR because I want to:

  1. Confirm the API design.
  2. Allow the maintenance team and the community to review the codes as early as possible, because I am not an expert of reconciliation, your guidance/your bug report may help me work faster to deal with the reconciliation correctly.

Naive Demo

https://github.com/facebook/lexical/assets/36890796/61a83c26-02cc-47d4-ab9e-08755512b988

https://github.com/facebook/lexical/assets/36890796/812dd021-22a5-4a8a-bdc2-155d71c0dc76

Tasks to do before this PR is submitted

Tasks I don't plan to do in this PR

Since I don't have so much spare time, I plan to deal with those issues later, maybe the community can offer some help.

API Documentation Draft

DecoratorElementNode

To combine external framework and Lexical node trees, decorator element node could be an option. It offers a decorate() method to allow external framework to render the DOM, but the external can also have child Lexical nodes handled by Lexical.

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

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

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

    // ElementNode will automatically clone children
    // So we only need to set the children if the node is new
    if (key === undefined) {
      this.append($createNestedRootNode());
      this.append($createNestedRootNode());
    }
  }

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

  updateDOM(): false {
    return false;
  }

  decorate(editor): JSX.Element {
    const onTitleRef = (element) => {
      editor.setNestedRootElement(this.getChildAtIndex(0).getKey(), element);
    }
    const onBodyRef = (element) => {
      editor.setNestedRootElement(this.getChildAtIndex(1).getKey(), element);
    }

    return (
      <div>
        <div ref={onTitleRef} />
        <div ref={onBodyRef} />
      </div>
    );
  }
}

Take the CardNode as an example:

If a DecoratorElementNode has a fixed set of children, developers are responsible to record each child's index and meaning. If it has a variable array of children, then just make sure the onRef's order in decorate function maps to each child correctly.

vercel[bot] commented 2 weeks ago

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
lexical ✅ Ready (Inspect) Visit Preview 💬 Add feedback May 9, 2024 9:10am
lexical-playground ✅ Ready (Inspect) Visit Preview 💬 Add feedback May 9, 2024 9:10am
GermanJablo commented 1 week ago

What are the problems you currently encounter with DecoratorNodes in your use case?

In my case they are:

I believe that these problems are solvable and that they should be addressed even if this PR goes ahead. Perhaps by talking about these problems and yours we can think of other alternatives.

In the past I tried to do something similar to what you do in this PR (on a local branch), but I ran into a lot of complications. The 1 LexicalNode = 1 DOM node model is something that is deeply rooted from the roots of Lexical.

yf-yang commented 1 week ago

@GermanJablo #5930

I agree I have similar troubles, but the main idea behind is decorating element nodes (instead of manipulating simple DOM node).

By the way, when realizing this is just a simplified nested editor, it is not so difficult to debug. The key is treat NestedRootNode and RootNode in the same manner in the core lib.

GermanJablo commented 3 days ago

Still, may I know how you would describe what specific problems this solves?

yf-yang commented 2 days ago

Decorate an ElementNode.

Consider things like that: https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/nodes/LayoutContainerNode.ts

You have to know vanilla JS well to make a complex element node, or you can do very little stuff. Alternatively, NestedEditor is needed and there exists multiple editor states.

LvChengbin commented 1 day ago

I'm all for adding support for the decorate method to ElementNode.

  1. We often rely on React component libraries like Material-UI to keep our element nodes, such as ParagraphNode and QuoteNode, looking consistent. But without the decorate method, we end up writing more CSS just to match the styles of components in the library.

  2. Trying to do things across nested editors comes with its fair share of headaches—think wonky history management, wonky selection handling, tangled data structures, plugin clashes, you name it.