Skip to main content

MentionAtomExtension

Summary

This is the atom version of the MentionExtension.

It provides mentions as atom nodes which don't support editing once being inserted into the document.

Usage

Installation

This extension is installed for you when you install the main remirror package. The extension is provided by the @remirror/extension-mention-atom package.

Using the setup from Getting started, you have:

import 'remirror/styles/all.css';

import { EditorComponent, Remirror, useRemirror } from '@remirror/react';

const MyEditor: React.FC = () => {
const { manager, state } = useRemirror({
content: '<p>I love Remirror</p>',
selection: 'start',
stringHandler: 'html',
});

return (
<div className='remirror-theme'>
<Remirror manager={manager} initialContent={state}>
<EditorComponent />
</Remirror>
</div>
);
};

Add the mentionAtomExtension and create a component to use for the suggestion popup:

import 'remirror/styles/all.css';

import { useEffect, useState } from 'react';
import { MentionAtomExtension } from 'remirror/extensions';
import { cx } from '@remirror/core';
import {
EditorComponent,
FloatingWrapper,
MentionAtomNodeAttributes,
Remirror,
useMentionAtom,
useRemirror,
} from '@remirror/react';

const ALL_USERS = [
{ id: 'joe', label: 'Joe' },
{ id: 'sue', label: 'Sue' },
{ id: 'pat', label: 'Pat' },
{ id: 'tom', label: 'Tom' },
{ id: 'jim', label: 'Jim' },
];

const MentionSuggestor: React.FC = () => {
const [options, setOptions] = useState<MentionAtomNodeAttributes[]>([]);
const { state, getMenuProps, getItemProps, indexIsHovered, indexIsSelected } = useMentionAtom({
items: options,
});

useEffect(() => {
if (!state) {
return;
}

const searchTerm = state.query.full.toLowerCase();

const filteredOptions = ALL_USERS.filter((user) =>
user.label.toLowerCase().includes(searchTerm),
)
.sort()
.slice(0, 5);

setOptions(filteredOptions);
}, [state]);

const enabled = Boolean(state);

return (
<FloatingWrapper positioner='cursor' enabled={enabled} placement='bottom-start'>
<div {...getMenuProps()} className='suggestions'>
{enabled &&
options.map((user, index) => {
const isHighlighted = indexIsSelected(index);
const isHovered = indexIsHovered(index);

return (
<div
key={user.id}
className={cx('suggestion', isHighlighted && 'highlighted', isHovered && 'hovered')}
{...getItemProps({
item: user,
index,
})}
>
{user.label}
</div>
);
})}
</div>
</FloatingWrapper>
);
};

const MyEditor: React.FC = () => {
const { manager, state } = useRemirror({
extensions: () => [
new MentionAtomExtension({
matchers: [{ name: 'at', char: '@', appendText: ' ' }],
}),
],
content: '<p>I love Remirror</p>',
selection: 'start',
stringHandler: 'html',
});

return (
<div className='remirror-theme'>
<Remirror manager={manager} initialContent={state}>
<EditorComponent />
<MentionSuggestor />
</Remirror>
</div>
);
};

You can add additional machers, for example hashtags:

const ALL_TAGS = [
{ id: 'cel', label: 'Celebrity' },
{ id: 'ed', label: 'Education' },
{ id: 'tech', label: 'Tech' },
];
new MentionAtomExtension({
matchers: [
{ name: 'at', char: '@', appendText: ' ' },
{ name: 'tag', char: '#', appendText: ' ' },
],
});

Update useEffect in MentionSuggestor component:

useEffect(() => {
if (!state) {
return;
}

const searchTerm = state.query.full.toLowerCase();
let filteredOptions: MentionAtomNodeAttributes[] = [];

if (state.name === 'tag') {
filteredOptions = ALL_TAGS.filter((tag) => tag.label.toLowerCase().includes(searchTerm));
} else if (state.name === 'at') {
filteredOptions = ALL_USERS.filter((user) => user.label.toLowerCase().includes(searchTerm));
}

filteredOptions = filteredOptions.sort().slice(0, 5);
setOptions(filteredOptions);
}, [state]);

Examples

Source code
import React, { useCallback } from 'react';
import { useHelpers, useRemirrorContext } from '@remirror/react';
import { SocialEditor } from '@remirror/react-editors/social';

const ALL_USERS = [
{ id: 'joe', label: 'Joe' },
{ id: 'sue', label: 'Sue' },
{ id: 'pat', label: 'Pat' },
{ id: 'tom', label: 'Tom' },
{ id: 'jim', label: 'Jim' },
];

const TAGS = ['editor', 'remirror', 'opensource', 'prosemirror'];

const SAMPLE_DOC = {
type: 'doc',
content: [
{
type: 'paragraph',
attrs: { dir: null, ignoreBidiAutoUpdate: null },
content: [{ type: 'text', text: 'Loaded content' }],
},
],
};

function LoadButton() {
const { setContent } = useRemirrorContext();
const handleClick = useCallback(() => setContent(SAMPLE_DOC), [setContent]);

return (
<button onMouseDown={(event) => event.preventDefault()} onClick={handleClick}>
Load
</button>
);
}

function SaveButton() {
const { getJSON } = useHelpers();
const handleClick = useCallback(() => alert(JSON.stringify(getJSON())), [getJSON]);

return (
<button onMouseDown={(event) => event.preventDefault()} onClick={handleClick}>
Save
</button>
);
}

const Basic: React.FC = () => (
<SocialEditor placeholder='Mention @joe or add #remirror' users={ALL_USERS} tags={TAGS}>
<LoadButton />
<SaveButton />
</SocialEditor>
);

export default Basic;

See storybook for examples.

API