lexical-beautiful-mentions
Version:
A mentions plugin for the lexical text editor.
416 lines (338 loc) • 12.7 kB
Markdown
[![CI status][github-ci-action-image]][github-ci-action-url]
[![CodeQL status][github-codeql-analysis-action-image]][github-codeql-analysis-action-url]
[]: https://github.com/sodenn/lexical-beautiful-mentions/actions/workflows/ci.yml/badge.svg
[]: https://github.com/sodenn/lexical-beautiful-mentions/actions/workflows/ci.yml
[]: https://github.com/sodenn/lexical-beautiful-mentions/actions/workflows/codeql-analysis.yml/badge.svg
[]: https://github.com/sodenn/lexical-beautiful-mentions/actions/workflows/codeql-analysis.yml
[](https://lexical-beautiful-mentions-docs.vercel.app/)
A mentions plugin for the [lexical editor](https://lexical.dev/). lexical is an extendable text editor for the web build by Meta. While the lexical playground offers a basic mentions plugin for demo purposes, this plugin is more advanced and offers more features.
- **Customizable triggers**: Use characters, words or regular expressions as triggers for mentions.
- **Multiple triggers**: You can define multiple triggers (e.g. `@` and `#`).
- [Editing mentions outside the editor](#programmatically-insert-delete-or-rename-mentions): Programmatically insert, delete, or rename mentions via the `useBeautifulMentions` hook.
- **Automatic spacing**: The plugin automatically adds spaces around the mentions, which makes it easier for the user to continue typing.
- [Adding new mentions](#creating-new-mentions): You can allow users to create new mentions that are not in the suggestion list.
- [Async query function](#async-query-function): You can use an async query function to provide mentions for the suggestion list.
- [Additional metadata](#additional-metadata): You can add additional metadata to the mention items, which will be included in the mention nodes when serializing the editor content.
- [Customizable mention style](#customize-mention-style): You can change the look of the mentions via the editor theme to match the style of your application.
- [Custom menu and menu item](#custom-menu-and-menu-item-component): You can customize the look and behavior of the menu that displays the mention suggestions.
- [Custom mention component](#custom-mention-node-and-component): You can replace the default mention component with a custom component of your choice.
## Installation
To install the plugin, run the following command:
```bash
// with npm
npm install lexical-beautiful-mentions
// with yarn
yarn add lexical-beautiful-mentions
```
You also need to install the `lexical` and `@lexical/react`, which is a peer dependency of this plugin.
## Usage
Import the `BeautifulMentionsPlugin` plugin:
```tsx
import { BeautifulMentionsPlugin, BeautifulMentionNode } from "lexical-beautiful-mentions";
```
Add the plugin to the lexical editor:
```tsx
import { LexicalComposer } from "@lexical/react/LexicalComposer";
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
const mentionItems = {
"@": ["Anton", "Boris", "Catherine", "Dmitri", "Elena", "Felix", "Gina"],
"#": ["Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig", "Grape"],
"due:": ["Today", "Tomorrow", "01-01-2023"],
};
const editorConfig = {
// ...
nodes: [BeautifulMentionNode] // 👈 register the mention node
};
return (
<LexicalComposer initialConfig={editorConfig}>
<RichTextPlugin // 👈 use the RichTextPlugin to get clipboard support for mentions
contentEditable={/* ... */}
placeholder={/* ... */}
ErrorBoundary={/* ... */}
/>
<BeautifulMentionsPlugin // 👈 add the mentions plugin
items={mentionItems}
/>
{/** ... */}
</LexicalComposer>
);
```
<img src="https://raw.githubusercontent.com/sodenn/lexical-beautiful-mentions/main/resources/screenshot1.png" width="200"/><br>
```tsx
import { BeautifulMentionsTheme } from "lexical-beautiful-mentions";
// ...
const beautifulMentionsTheme: BeautifulMentionsTheme = {
// 👇 use the trigger name as the key
"@": "px-1 mx-px ...",
// 👇 add the "Focused" suffix to style the focused mention
"@Focused": "outline-none shadow-md ...",
// 👇 use a class configuration object for advanced styling
"due:": {
trigger: "text-blue-400 ...",
value: "text-orange-400 ...",
container: "px-1 mx-px ...",
containerFocused: "outline-none shadow-md ...",
},
}
// 👇 add the mention theme to the editor theme
const editorConfig = {
// ...
theme: {
// ...
beautifulMentions: beautifulMentionsTheme,
},
};
// ...
return (
<LexicalComposer initialConfig={editorConfig}>
{/** ... */}
</LexicalComposer>
);
```
If applying styles via the theme is not enough, you can replace the BeautifulMentionNode by using the lexical [Node Overrides](https://lexical.dev/docs/concepts/node-replacement) API. This allows you to change the default behavior of the mention node:
```tsx
export class CustomMentionsNode extends BeautifulMentionNode {
static getType() {
return "custom-beautifulMention";
}
static clone(node: CustomBeautifulMentionNode) {
// TODO: implement
}
static importJSON(serializedNode: SerializedBeautifulMentionNode) {
// TODO: implement
}
exportJSON(): SerializedBeautifulMentionNode {
// TODO: implement
}
component(): ElementType<BeautifulMentionComponentProps> | null {
// the component that renders the mention in the editor
// return null to use the default component
// 💡 if you only want to replace the component use the `createBeautifulMentionNode` helper method. See below for more details 👇
}
decorate(editor: LexicalEditor, config: EditorConfig): React.JSX.Element {
// TODO: implement
}
}
const editorConfig = {
// ...
nodes: [
// Don't forget to register your custom node separately!
CustomMentionsNode,
{
replace: BeautifulMentionNode,
with: (node: BeautifulMentionNode) => {
return new CustomMentionsNode(
node.getTrigger(),
node.getValue(),
node.getData(),
);
}
}
]
}
```
The plugin also provides a helper method that overrides the default `BeautifulMentionNode` and uses a customized version with a component of your choice:
```tsx
const CustomMentionComponent = forwardRef<
HTMLDivElement,
BeautifulMentionComponentProps<MyData>
>(({ trigger, value, data: myData, children, ...other }, ref) => {
return (
<div {...other} ref={ref} title={trigger + value}>
{value}
</div>
);
});
const editorConfig = {
// ...
nodes: [...createBeautifulMentionNode(CustomMentionComponent)],
};
```
<img src="https://raw.githubusercontent.com/sodenn/lexical-beautiful-mentions/main/resources/screenshot2.png" width="500"/><br>
```tsx
function CustomMenu({ loading, ...props }: BeautifulMentionsMenuProps) {
<ul
className="m-0 mt-6 ..."
{...props}
/>
}
const CustomMenuItem = forwardRef<
HTMLLIElement,
BeautifulMentionsMenuItemProps
>(({ selected, item, ...props }, ref) => (
<li
className={`m-0 flex ... ${selected ? "bg-gray-100 ..." : "bg-white ..."}`}
{...props}
ref={ref}
/>
));
// ...
<BeautifulMentionsPlugin
items={mentionItems}
menuComponent={CustomMenu}
menuItemComponent={CustomMenuItem}
/>
```
Additional metadata can be used to uniquely identify mentions by adding an `id` or any other unique property to the mention items. When serializing the editor content, the metadata will be included in the mention nodes:
```tsx
const mentionItems = {
"@": [
{ value: "Catherine", id: "1", email: "catherine.a@example.com" },
{ value: "Catherine", id: "2", email: "catherine.b@example.com" },
// ...
],
};
```
Serializes to the following lexical nodes:
```json
[
{
"trigger": "@",
"value": "Catherine",
"data": {
"id": "1",
"email": "catherine.a@example.com"
},
"type": "beautifulMention",
"version": 1
},
{
"trigger": "@",
"value": "Catherine",
"data": {
"id": "2",
"email": "catherine.b@example.com"
},
"type": "beautifulMention",
"version": 1
}
]
```
All additional metadata are available as props of the `BeautifulMentionsMenuItem` component:
```tsx
const CustomMenuItem = forwardRef<
HTMLLIElement,
BeautifulMentionsMenuItemProps
>(({ item: { data: { id, email }}, ...props }, ref) => (
<li
// ...
/>
));
```
The plugin allows you to display a custom component when the mention menu is empty and no search results are found:
```tsx
const Empty = () => (
<div className="top-[2px] m-0 min-w-[10rem] overflow-hidden ...">
No results found.
</div>
);
// ...
<BeautifulMentionsPlugin
// ...
menuComponent={CustomMenu}
menuItemComponent={CustomMenuItem}
emptyComponent={Empty} // 👈
/>
```
```tsx
import {
BeautifulMentionsPlugin,
useBeautifulMentions,
} from "lexical-beautiful-mentions";
// ...
function MentionsToolbar() {
const { removeMentions, insertMention } = useBeautifulMentions();
return (
<div className="grid gap-2 grid-cols-2">
<Button onClick={() => removeMentions({ trigger: "#", value: "urgent" })}>
Remove Mention
</Button>
<Button onClick={() => insertMention({ trigger: "#", value: "work" })}>
Insert Mention
</Button>
</div>
);
}
// ...
return (
<LexicalComposer>
{/** ... */}
<BeautifulMentionsPlugin
items={mentionItems}
/>
<MentionsToolbar />
{/** ... */}
</LexicalComposer>
);
```
By default, the plugin allows users to create new mentions that are not in the suggestion list.
Customize the text of the menu item that allows users to create new mentions:
```tsx
<BeautifulMentionsPlugin
items={mentionItems}
creatable={`Add user "{{name}}"`} // 👈 the `{{name}}` placeholder contains the current search query
/>
```
Hide the menu item that allows users to create new mentions:
```tsx
<BeautifulMentionsPlugin
items={mentionItems}
creatable={false} // 👈 hide the menu item that allows users to create new mentions
/>
```
```tsx
const queryMentions = async (trigger: string, query: string) => {
const response = await fetch(
`https://example.com/api/mentions?trigger=${trigger}&query=${query}`
);
const data = await response.json();
return data as string[];
};
// ...
return (
<LexicalComposer>
{/** ... */}
<BeautifulMentionsPlugin
triggers={["@", "#"]} // needed to tell the plugin when to call the query function
onSearch={queryMentions}
/>
{/** ... */}
</LexicalComposer>
);
```
You can customize the regular expression that looks for mentions in your text by using the `punctuation` and `preTriggerChars` properties.
The `punctuation` property allows you to specify the punctuation characters that can appear directly after a mention. So that you can type for example `@Alice.` **without the dot** being part of the mention. The default value contains a common set of [punctuation characters](https://github.com/sodenn/lexical-beautiful-mentions/blob/eb6b1a3e1c642c77634d586ebceaf44d00cbbdfc/plugin/src/mention-utils.ts#L48).
The `preTriggerChars` property allows you to specify a set of characters that can appear directly before the trigger character. By default, only the open bracket is allowed (e.g. `(@Alice)`).
### Utility Functions for Mention Conversion
The plugin provides two utility functions to help with converting text to mention nodes:
#### `$convertToMentionNodes`
This function converts a string or a text node into a list of mention and text nodes.
Usage example:
```typescript
import { $convertToMentionNodes } from 'lexical-beautiful-mentions';
const text = "Hello @Alice and #world";
const nodes = $convertToMentionNodes(text, ['@', '#']);
// nodes will be an array of TextNodes and BeautifulMentionNodes
```
This function transforms text nodes in the editor that contain mention strings into mention nodes.
Usage example:
```typescript
import { $transformTextToMentionNodes } from 'lexical-beautiful-mentions';
editor.update(() => {
$transformTextToMentionNodes(['@', '#']);
});
```
**Note:** Both functions only work for mentions without spaces. Ensure spaces are disabled via the `allowSpaces` prop.