payload icon indicating copy to clipboard operation
payload copied to clipboard

feat(richtext-lexical): text classes

Open GermanJablo opened this issue 2 months ago • 0 comments

This PR allows adding arbitrary classes to TextNodes, without the need to extend the node. My idea is to do the same with the rest of the nodes.

Extending a node is quite complex and has a lot of footguns. Also, we should make extending nodes the last resort to customize the editor, since 2 plugins extending the same node would not work.

Most of the time when someone extends a node, what they want is only to add a class to the node under certain conditions, so this property would allow them to achieve their goals in a simpler way.

As an example, this PR shows how a TextColorFeature could be developed in fewer lines of code and in a safer way since it would not involve breaking the code if used with another plugin that also extends TextNode (run pnpm dev _community to try it).

The TextColorFeature is a work in progress. It still has bugs and room for improvement. Let's keep the discussion of this PR only about the new TextNode with classes. The TextColorFeature will be removed from here and incorporated in a later PR.


How does this work?

The __classes property is an object where the key-value has the form prefix-suffix if the suffix is a string, or prefix if the value is of type boolean.

For example, this is how the following object would be rendered:

{
  bg-color: 'red', // the node is rendered with class `bg-color-red`
  color: 'green', // node is rendered with class `color-green`,
  collapsed: true, // node is rendered with class `collapsed`,
  hidden: false, // this does not add any class to the node
}

Also, in order to make the serialization as lightweight as possible, I filter out values that are undefined or false, or ignore the classes property if it has no entries.


Now, the million dollar question is: What if a user is already extending and replacing TextNode? Or what if they are importing TextNode to do other things?

Well, to solve this problem we have 2 options:

Option 1: we implement this feature in the Lexical repository. I know the Lexical team has often expressed interest in reducing the complexity of customizing nodes, and I think they might be interested in this API. If this is the case, it would be the simplest option.

Option 2: we do not re-export Lexical's TextNode, but our own TextNode with the same name. We are currently using a wildcard export, and as far as I know you can't exclude a module, so we would have to explicitly mention all modules except the nodes, something complex to maintain. An important point is that if there is a user who (a) is not using our re-export from Lexical as he/she should and (b) is extending the nodes he imports directly from Lexical, then there would be a breaking change. Actually, this is acceptable because the error is theirs, and we are re-exporting Lexical for a reason. But still to take into account. If we want typescript not to complain if someone tries to use our TextNode in a Lexical function (example $isTextNode), we would have to make the node extension structurally equivalent (__classes should be optional, and the getClasses and mutateClasses methods should be functions external to the class). By way of clarification, I have made sure to test that a node replacement API string works well. See this discussion.

GermanJablo avatar Nov 27 '24 20:11 GermanJablo