Skip to content

Commit fc83823

Browse files
authored
feat(richtext-lexical): add TextStateFeature (allows applying styles such as color and background color to text) (#9667)
Originally this PR was going to introduce a `TextColorFeature`, but it ended up becoming a more general-purpose `TextStateFeature`. ## Example of use: ```ts import { defaultColors, TextStateFeature } from '@payloadcms/richtext-lexical' TextStateFeature({ // prettier-ignore state: { color: { ...defaultColors, // fancy gradients! galaxy: { label: 'Galaxy', css: { background: 'linear-gradient(to right, #0000ff, #ff0000)', color: 'white' } }, sunset: { label: 'Sunset', css: { background: 'linear-gradient(to top, #ff5f6d, #6a3093)' } }, }, // You can have both colored and underlined text at the same time. // If you don't want that, you should group them within the same key. // (just like I did with defaultColors and my fancy gradients) underline: { 'solid': { label: 'Solid', css: { 'text-decoration': 'underline', 'text-underline-offset': '4px' } }, // You'll probably want to use the CSS light-dark() utility. 'yellow-dashed': { label: 'Yellow Dashed', css: { 'text-decoration': 'underline dashed', 'text-decoration-color': 'light-dark(#EAB308,yellow)', 'text-underline-offset': '4px' } }, }, }, }), ``` Which will result in the following: ![image](https://github.com/user-attachments/assets/ed29b30b-8efd-4265-a1b9-125c97ac5fce) ## Challenges & Considerations Adding colors or styles in general to the Lexical editor is not as simple as it seems. 1. **Extending TextNode isn't ideal** - While possible, it's verbose, error-prone, and not composable. If multiple features extend the same node, conflicts arise. - That’s why we collaborated with the Lexical team to introduce [the new State API](https://lexical.dev/docs/concepts/node-replacement) ([PR](facebook/lexical#7117)). 2. **Issues with patchStyles** - Some community plugins use `patchStyles`, but storing CSS in the editor’s JSON has drawbacks: - Style adaptability: Users may want different styles per scenario (dark/light mode, mobile/web, etc.). - Migration challenges: Hardcoded colors (e.g., #FF0000) make updates difficult. Using tokens (e.g., "red") allows flexibility. - Larger JSON footprint increases DB size. 3. **Managing overlapping styles** - Some users may want both text and background colors on the same node, while others may prefer mutual exclusivity. - This approach allows either: - Using a single "color" state (e.g., "bg-red" + "text-red"). - Defining separate "bg-color" and "text-color" states for independent styling. 4. **Good light and dark modes by default** - Many major editors (Google Docs, OneNote, Word) treat dark mode as an afterthought, leading to poor UX. - We provide a well-balanced default palette that looks great in both themes, serving as a strong foundation for customization. 5. **Feature name. Why TextState?** - Other names considered were `TextFormatFeature` and `TextStylesFeature`. The term `format` in Lexical and Payload is already used to refer to something else (italic, bold, etc.). The term `style` could be misleading since it is never attached to the editorState. - State seems appropriate because: - Lexical's new state API is used under the hood. - Perhaps in the future we'll want to make state features for other nodes, such as `ElementStateFeature` or `RootStateFeature`. Note: There's a bug in Lexical's `forEachSelectedTextNode`. When the selection includes a textNode partially on the left, all state for that node is removed instead of splitting it along the selection edge.
1 parent 2a41d3f commit fc83823

File tree

12 files changed

+633
-28
lines changed

12 files changed

+633
-28
lines changed

docs/rich-text/overview.mdx

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -142,32 +142,33 @@ import { CallToAction } from '../blocks/CallToAction'
142142

143143
Here's an overview of all the included features:
144144

145-
| Feature Name | Included by default | Description |
146-
| ------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
147-
| **`BoldFeature`** | Yes | Handles the bold text format |
148-
| **`ItalicFeature`** | Yes | Handles the italic text format |
149-
| **`UnderlineFeature`** | Yes | Handles the underline text format |
150-
| **`StrikethroughFeature`** | Yes | Handles the strikethrough text format |
151-
| **`SubscriptFeature`** | Yes | Handles the subscript text format |
152-
| **`SuperscriptFeature`** | Yes | Handles the superscript text format |
153-
| **`InlineCodeFeature`** | Yes | Handles the inline-code text format |
154-
| **`ParagraphFeature`** | Yes | Handles paragraphs. Since they are already a key feature of lexical itself, this Feature mainly handles the Slash and Add-Block menu entries for paragraphs |
155-
| **`HeadingFeature`** | Yes | Adds Heading Nodes (by default, H1 - H6, but that can be customized) |
156-
| **`AlignFeature`** | Yes | Allows you to align text left, centered and right |
157-
| **`IndentFeature`** | Yes | Allows you to indent text with the tab key |
158-
| **`UnorderedListFeature`** | Yes | Adds unordered lists (ul) |
159-
| **`OrderedListFeature`** | Yes | Adds ordered lists (ol) |
160-
| **`ChecklistFeature`** | Yes | Adds checklists |
161-
| **`LinkFeature`** | Yes | Allows you to create internal and external links |
162-
| **`RelationshipFeature`** | Yes | Allows you to create block-level (not inline) relationships to other documents |
163-
| **`BlockquoteFeature`** | Yes | Allows you to create block-level quotes |
164-
| **`UploadFeature`** | Yes | Allows you to create block-level upload nodes - this supports all kinds of uploads, not just images |
165-
| **`HorizontalRuleFeature`** | Yes | Horizontal rules / separators. Basically displays an `<hr>` element |
166-
| **`InlineToolbarFeature`** | Yes | The inline toolbar is the floating toolbar which appears when you select text. This toolbar only contains actions relevant for selected text |
167-
| **`FixedToolbarFeature`** | No | This classic toolbar is pinned to the top and always visible. Both inline and fixed toolbars can be enabled at the same time. |
168-
| **`BlocksFeature`** | No | Allows you to use Payload's [Blocks Field](../fields/blocks) directly inside your editor. In the feature props, you can specify the allowed blocks - just like in the Blocks field. |
169-
| **`TreeViewFeature`** | No | Adds a debug box under the editor, which allows you to see the current editor state live, the dom, as well as time travel. Very useful for debugging |
170-
| **`EXPERIMENTAL_TableFeature`** | No | Adds support for tables. This feature may be removed or receive breaking changes in the future - even within a stable lexical release, without needing a major release. |
145+
| Feature Name | Included by default | Description |
146+
| ----------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
147+
| **`BoldFeature`** | Yes | Handles the bold text format |
148+
| **`ItalicFeature`** | Yes | Handles the italic text format |
149+
| **`UnderlineFeature`** | Yes | Handles the underline text format |
150+
| **`StrikethroughFeature`** | Yes | Handles the strikethrough text format |
151+
| **`SubscriptFeature`** | Yes | Handles the subscript text format |
152+
| **`SuperscriptFeature`** | Yes | Handles the superscript text format |
153+
| **`InlineCodeFeature`** | Yes | Handles the inline-code text format |
154+
| **`ParagraphFeature`** | Yes | Handles paragraphs. Since they are already a key feature of lexical itself, this Feature mainly handles the Slash and Add-Block menu entries for paragraphs |
155+
| **`HeadingFeature`** | Yes | Adds Heading Nodes (by default, H1 - H6, but that can be customized) |
156+
| **`AlignFeature`** | Yes | Allows you to align text left, centered and right |
157+
| **`IndentFeature`** | Yes | Allows you to indent text with the tab key |
158+
| **`UnorderedListFeature`** | Yes | Adds unordered lists (ul) |
159+
| **`OrderedListFeature`** | Yes | Adds ordered lists (ol) |
160+
| **`ChecklistFeature`** | Yes | Adds checklists |
161+
| **`LinkFeature`** | Yes | Allows you to create internal and external links |
162+
| **`RelationshipFeature`** | Yes | Allows you to create block-level (not inline) relationships to other documents |
163+
| **`BlockquoteFeature`** | Yes | Allows you to create block-level quotes |
164+
| **`UploadFeature`** | Yes | Allows you to create block-level upload nodes - this supports all kinds of uploads, not just images |
165+
| **`HorizontalRuleFeature`** | Yes | Horizontal rules / separators. Basically displays an `<hr>` element |
166+
| **`InlineToolbarFeature`** | Yes | The inline toolbar is the floating toolbar which appears when you select text. This toolbar only contains actions relevant for selected text |
167+
| **`FixedToolbarFeature`** | No | This classic toolbar is pinned to the top and always visible. Both inline and fixed toolbars can be enabled at the same time. |
168+
| **`BlocksFeature`** | No | Allows you to use Payload's [Blocks Field](../fields/blocks) directly inside your editor. In the feature props, you can specify the allowed blocks - just like in the Blocks field. |
169+
| **`TreeViewFeature`** | No | Adds a debug box under the editor, which allows you to see the current editor state live, the dom, as well as time travel. Very useful for debugging |
170+
| **`EXPERIMENTAL_TableFeature`** | No | Adds support for tables. This feature may be removed or receive breaking changes in the future - even within a stable lexical release, without needing a major release. |
171+
| **`EXPERIMENTAL_TextStateFeature`** | No | Allows you to store key-value attributes within TextNodes and assign them inline styles. |
171172

172173
Notice how even the toolbars are features? That's how extensible our lexical editor is - you could theoretically create your own toolbar if you wanted to!
173174

packages/richtext-lexical/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,7 @@
370370
"@types/uuid": "10.0.0",
371371
"acorn": "8.12.1",
372372
"bson-objectid": "2.0.4",
373+
"csstype": "3.1.3",
373374
"dequal": "2.0.3",
374375
"escape-html": "1.0.3",
375376
"jsox": "1.2.121",

packages/richtext-lexical/src/exports/client/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export { StrikethroughFeatureClient } from '../../features/format/strikethrough/
2020
export { SubscriptFeatureClient } from '../../features/format/subscript/feature.client.js'
2121
export { SuperscriptFeatureClient } from '../../features/format/superscript/feature.client.js'
2222
export { UnderlineFeatureClient } from '../../features/format/underline/feature.client.js'
23+
export { TextStateFeatureClient } from '../../features/textState/feature.client.js'
2324
export { HeadingFeatureClient } from '../../features/heading/client/index.js'
2425
export { HorizontalRuleFeatureClient } from '../../features/horizontalRule/client/index.js'
2526
export { IndentFeatureClient } from '../../features/indent/client/index.js'

0 commit comments

Comments
 (0)