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​