Mentions for everyone! This plugin allows the user to choose an entry from a list. After selection an entry the search text will be replace with the selected entity. The list of suggestions mentions needs to contain at least a name to display. If desired a link and/or an avatar image can be provided.
While the suggestion popover is open, the user can close it by pressing ESC. This will be stored for as long as the the selection stays inside the word that triggered the search. After the selection left this word once the escape behaviour will be reset. The suggestions will appear again once the user selects the word that that triggered the selection.
npm install [email protected] --savenpm install [email protected] --savePlease checkout the 'Simple Example' further down the page.The plugin ships with a default styling available at this location in the installed package: node_modules/draft-js-mention-plugin/lib/plugin.css
npm i style-loader css-loader --save-devmodule.exports = {
module: {
loaders: [
{
test: /plugin\.css$/,
loaders: [
'style', 'css',
],
},
],
},
};
import 'draft-js-mention-plugin/lib/plugin.css'; // eslint-disable-line import/no-unresolved
Please help, by submiting a Pull Request to the documentation.
popoverComponentimport { defaultSuggestionsFilter } from 'draft-js-mention-plugin';import React, { Component } from 'react';
import { EditorState } from 'draft-js';
import Editor from 'draft-js-plugins-editor'; // eslint-disable-line import/no-unresolved
import createMentionPlugin, { defaultSuggestionsFilter } from 'draft-js-mention-plugin'; // eslint-disable-line import/no-unresolved
import editorStyles from './editorStyles.css';
import mentions from './mentions';
const mentionPlugin = createMentionPlugin();
const { MentionSuggestions } = mentionPlugin;
const plugins = [mentionPlugin];
export default class SimpleMentionEditor extends Component {
state = {
editorState: EditorState.createEmpty(),
suggestions: mentions,
};
onChange = (editorState) => {
this.setState({
editorState,
});
};
onSearchChange = ({ value }) => {
this.setState({
suggestions: defaultSuggestionsFilter(value, mentions),
});
};
onAddMention = () => {
// get the mention object selected
}
focus = () => {
this.editor.focus();
};
render() {
return (
<div className={editorStyles.editor} onClick={this.focus}>
<Editor
editorState={this.state.editorState}
onChange={this.onChange}
plugins={plugins}
ref={(element) => { this.editor = element; }}
/>
<MentionSuggestions
onSearchChange={this.onSearchChange}
suggestions={this.state.suggestions}
onAddMention={this.onAddMention}
/>
</div>
);
}
}
import { fromJS } from 'immutable';
const mentions = fromJS([
{
name: 'Matthew Russell',
link: 'https://twitter.com/mrussell247',
avatar: 'https://pbs.twimg.com/profile_images/517863945/mattsailing_400x400.jpg',
},
{
name: 'Julian Krispel-Samsel',
link: 'https://twitter.com/juliandoesstuff',
avatar: 'https://avatars2.githubusercontent.com/u/1188186?v=3&s=400',
},
{
name: 'Jyoti Puri',
link: 'https://twitter.com/jyopur',
avatar: 'https://avatars0.githubusercontent.com/u/2182307?v=3&s=400',
},
{
name: 'Max Stoiber',
link: 'https://twitter.com/mxstbr',
avatar: 'https://pbs.twimg.com/profile_images/763033229993574400/6frGyDyA_400x400.jpg',
},
{
name: 'Nik Graf',
link: 'https://twitter.com/nikgraf',
avatar: 'https://avatars0.githubusercontent.com/u/223045?v=3&s=400',
},
{
name: 'Pascal Brandt',
link: 'https://twitter.com/psbrandt',
avatar: 'https://pbs.twimg.com/profile_images/688487813025640448/E6O6I011_400x400.png',
},
]);
export default mentions;
.editor {
box-sizing: border-box;
border: 1px solid #ddd;
cursor: text;
padding: 16px;
border-radius: 2px;
margin-bottom: 2em;
box-shadow: inset 0px 1px 8px -3px #ABABAB;
background: #fefefe;
}
.editor :global(.public-DraftEditor-content) {
min-height: 140px;
}
import React, { Component } from 'react';
import { EditorState } from 'draft-js';
import Editor from 'draft-js-plugins-editor'; // eslint-disable-line import/no-unresolved
import createMentionPlugin, { defaultSuggestionsFilter } from 'draft-js-mention-plugin'; // eslint-disable-line import/no-unresolved
import editorStyles from './editorStyles.css';
import mentionsStyles from './mentionsStyles.css';
import mentions from './mentions';
const positionSuggestions = ({ state, props }) => {
let transform;
let transition;
if (state.isActive && props.suggestions.size > 0) {
transform = 'scaleY(1)';
transition = 'all 0.25s cubic-bezier(.3,1.2,.2,1)';
} else if (state.isActive) {
transform = 'scaleY(0)';
transition = 'all 0.25s cubic-bezier(.3,1,.2,1)';
}
return {
transform,
transition,
};
};
const mentionPlugin = createMentionPlugin({
mentions,
entityMutability: 'IMMUTABLE',
theme: mentionsStyles,
positionSuggestions,
mentionPrefix: '@',
});
const { MentionSuggestions } = mentionPlugin;
const plugins = [mentionPlugin];
const Entry = (props) => {
const {
mention,
theme,
searchValue, // eslint-disable-line no-unused-vars
...parentProps
} = props;
return (
<div {...parentProps}>
<div className={theme.mentionSuggestionsEntryContainer}>
<div className={theme.mentionSuggestionsEntryContainerLeft}>
<img
src={mention.get('avatar')}
className={theme.mentionSuggestionsEntryAvatar}
role="presentation"
/>
</div>
<div className={theme.mentionSuggestionsEntryContainerRight}>
<div className={theme.mentionSuggestionsEntryText}>
{mention.get('name')}
</div>
<div className={theme.mentionSuggestionsEntryTitle}>
{mention.get('title')}
</div>
</div>
</div>
</div>
);
};
export default class CustomMentionEditor extends Component {
state = {
editorState: EditorState.createEmpty(),
suggestions: mentions,
};
onChange = (editorState) => {
this.setState({
editorState,
});
};
onSearchChange = ({ value }) => {
this.setState({
suggestions: defaultSuggestionsFilter(value, mentions),
});
};
focus = () => {
this.editor.focus();
};
render() {
return (
<div className={editorStyles.editor} onClick={this.focus}>
<Editor
editorState={this.state.editorState}
onChange={this.onChange}
plugins={plugins}
ref={(element) => { this.editor = element; }}
/>
<MentionSuggestions
onSearchChange={this.onSearchChange}
suggestions={this.state.suggestions}
entryComponent={Entry}
/>
</div>
);
}
}
.mention {
color: #4a85bb;
text-decoration: none;
}
.mentionSuggestions {
border-top: 1px solid #eee;
background: #fff;
border-radius: 2px;
cursor: pointer;
padding-top: 8px;
padding-bottom: 8px;
display: flex;
flex-direction: column;
box-sizing: border-box;
transform-origin: 50% 0%;
transform: scaleY(0);
margin: -16px;
}
.mentionSuggestionsEntryContainer {
display: table;
width: 100%;
}
.mentionSuggestionsEntryContainerLeft,
.mentionSuggestionsEntryContainerRight {
display: table-cell;
vertical-align: middle;
}
.mentionSuggestionsEntryContainerRight {
width: 100%;
padding-left: 8px;
}
.mentionSuggestionsEntry {
padding: 7px 10px 3px 10px;
transition: background-color 0.4s cubic-bezier(.27,1.27,.48,.56);
}
.mentionSuggestionsEntry:active {
background-color: #cce7ff;
}
.mentionSuggestionsEntryFocused {
composes: mentionSuggestionsEntry;
background-color: #e6f3ff;
}
.mentionSuggestionsEntryText,
.mentionSuggestionsEntryTitle {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mentionSuggestionsEntryText {
}
.mentionSuggestionsEntryTitle {
font-size: 80%;
color: #a7a7a7;
}
.mentionSuggestionsEntryAvatar {
display: block;
width: 30px;
height: 30px;
border-radius: 50%;
}
import { fromJS } from 'immutable';
const mentions = fromJS([
{
name: 'matthew',
title: 'Senior Software Engineer',
avatar: 'https://pbs.twimg.com/profile_images/517863945/mattsailing_400x400.jpg',
},
{
name: 'julian',
title: 'United Kingdom',
avatar: 'https://avatars2.githubusercontent.com/u/1188186?v=3&s=400',
},
{
name: 'jyoti',
title: 'New Delhi, India',
avatar: 'https://avatars0.githubusercontent.com/u/2182307?v=3&s=400',
},
{
name: 'max',
title: 'Travels around the world, brews coffee, skis mountains and makes stuff on the web.',
avatar: 'https://pbs.twimg.com/profile_images/763033229993574400/6frGyDyA_400x400.jpg',
},
{
name: 'nik',
title: 'Passionate about Software Architecture, UX, Skiing & Triathlons',
avatar: 'https://avatars0.githubusercontent.com/u/223045?v=3&s=400',
},
{
name: 'pascal',
title: 'HeathIT hacker and researcher',
avatar: 'https://pbs.twimg.com/profile_images/688487813025640448/E6O6I011_400x400.png',
},
]);
export default mentions;
.editor {
box-sizing: border-box;
border: 1px solid #ddd;
cursor: text;
padding: 16px;
border-radius: 2px;
margin-bottom: 2em;
box-shadow: inset 0px 1px 8px -3px #ABABAB;
background: #fefefe;
}
.editor :global(.public-DraftEditor-content) {
min-height: 140px;
}
import React, { Component } from 'react';
import { EditorState } from 'draft-js';
// eslint-disable-next-line import/no-unresolved
import Editor from 'draft-js-plugins-editor';
// eslint-disable-next-line import/no-unresolved
import createMentionPlugin from 'draft-js-mention-plugin';
import { fromJS } from 'immutable';
import editorStyles from './editorStyles.css';
const mentionPlugin = createMentionPlugin();
const { MentionSuggestions } = mentionPlugin;
const plugins = [mentionPlugin];
export default class SimpleMentionEditor extends Component {
state = {
editorState: EditorState.createEmpty(),
suggestions: fromJS([]),
};
onChange = (editorState) => {
this.setState({
editorState,
});
};
onSearchChange = ({ value }) => {
// An import statment would break server-side rendering.
require('whatwg-fetch'); // eslint-disable-line global-require
// while you normally would have a dynamic server that takes the value as
// a workaround we use this workaround to show different results
let url = '/data/mentionsA.json';
if (value.length === 1) {
url = '/data/mentionsB.json';
} else if (value.length > 1) {
url = '/data/mentionsC.json';
}
fetch(url)
.then((response) => response.json())
.then((data) => {
this.setState({
suggestions: fromJS(data),
});
});
};
focus = () => {
this.editor.focus();
};
render() {
return (
<div className={editorStyles.editor} onClick={this.focus}>
<Editor
editorState={this.state.editorState}
onChange={this.onChange}
plugins={plugins}
ref={(element) => { this.editor = element; }}
/>
<MentionSuggestions
onSearchChange={this.onSearchChange}
suggestions={this.state.suggestions}
/>
</div>
);
}
}
.editor {
box-sizing: border-box;
border: 1px solid #ddd;
cursor: text;
padding: 16px;
border-radius: 2px;
margin-bottom: 2em;
box-shadow: inset 0px 1px 8px -3px #ABABAB;
background: #fefefe;
}
.editor :global(.public-DraftEditor-content) {
min-height: 140px;
}
import React, { Component } from 'react';
import { EditorState } from 'draft-js';
import Editor from 'draft-js-plugins-editor'; // eslint-disable-line import/no-unresolved
import createMentionPlugin, { defaultSuggestionsFilter } from 'draft-js-mention-plugin'; // eslint-disable-line import/no-unresolved
import editorStyles from './editorStyles.css';
import mentions from './mentions';
const mentionPlugin = createMentionPlugin({
mentions,
mentionComponent: (props) => (
<span
className={props.className}
// eslint-disable-next-line no-alert
onClick={() => alert('Clicked on the Mention!')}
>
{props.children}
</span>
),
});
const { MentionSuggestions } = mentionPlugin;
const plugins = [mentionPlugin];
export default class CustomMentionEditor extends Component {
state = {
editorState: EditorState.createEmpty(),
suggestions: mentions,
};
onChange = (editorState) => {
this.setState({
editorState,
});
};
onSearchChange = ({ value }) => {
this.setState({
suggestions: defaultSuggestionsFilter(value, mentions),
});
};
focus = () => {
this.editor.focus();
};
render() {
return (
<div className={editorStyles.editor} onClick={this.focus}>
<Editor
editorState={this.state.editorState}
onChange={this.onChange}
plugins={plugins}
ref={(element) => { this.editor = element; }}
/>
<MentionSuggestions
onSearchChange={this.onSearchChange}
suggestions={this.state.suggestions}
/>
</div>
);
}
}
.editor {
box-sizing: border-box;
border: 1px solid #ddd;
cursor: text;
padding: 16px;
border-radius: 2px;
margin-bottom: 2em;
box-shadow: inset 0px 1px 8px -3px #ABABAB;
background: #fefefe;
}
.editor :global(.public-DraftEditor-content) {
min-height: 140px;
}