--- category: features --- {@snippet features/build-mention-source} # Mention The {@link module:mention/mention~Mention} feature brings support for smart autocompletion based on user input. When user types a pre-configured marker, such as `@` or `#`, they get an autocomplete suggestions in a panel displayed next to the caret. The selected suggestion is then inserted into the content. ## Demo You can type `'@'` character to invoke mention autocomplete UI. The below demo is configured as static list of names. {@snippet features/mention} ## Configuration The minimal configuration of the mention feature requires defining a {@link module:mention/mention~MentionFeed `feed`} and a {@link module:mention/mention~MentionFeed `marker`}. You can also define `minimumCharacters` after which the autocomplete panel will be shown. The code snippet below was used to configure the demo above. It defines the list of names that will be autocompleted after the user types the `'@'` character. ```js ClassicEditor .create( document.querySelector( '#editor' ), { // This feature is not available in any of the builds. // See the "Installation" section. plugins: [ Mention, ... ], mention: { feeds: [ { marker: '@', feed: [ 'Barney', 'Lily', 'Marshall', 'Robin', 'Ted' ], minimumCharacters: 1 } } } } ) .then( ... ) .catch( ... ); ``` Additionally, you can configure: * How the item is rendered in the autocomplete panel (via setting {@link module:mention/mention~MentionFeed `itemRenderer`}). See ["Customizing the autocomplete list"](#customizing-the-autocomplete-list). * How the item is converted during the conversion. See ["Customizing the output"](#customizing-the-output). * Multiple feeds — in the demo above we used only one feed, which is triggered by the `'@'` character. You can define multiple feeds but they must use different markers. For example, you can use `'@'` for people and `#` for tags. ### Providing the feed The {@link module:mention/mention~MentionFeed `feed`} can be provided as: * a static array — good for scenarios with relatively small set of autocomplete items. * a callback — which provides more control over the returned list of items. When using a callback you can return a `Promise` that resolves with the list of {@link module:mention/mention~MentionFeedItem matching feed items}. Those can be simple stringsor plain objects with at least the `name` property. The other properties of this object can later be used e.g. when [customizing the autocomplete list](#customizing-the-autocomplete-list) or [customizing the output](#customizing-the-output). When using external resources to obtain the feed it is recommended to add some caching mechanism so subsequent calls for the same suggestion would load faster. You can also consider adding the `minimumCharacters` option to the feed config so the editor will call the feed callback after a minimum characters typed instead of action on marker alone. The callback receives the query text which should be used to filter item suggestions. It should return a `Promise` and resolve it with an array of items that match to the feed text. ```js const items = [ { id: '1', name: 'Barney Stinson', username: 'swarley', link: 'https://www.imdb.com/title/tt0460649/characters/nm0000439' }, { id: '2', name: 'Lily Aldrin', username: 'lilypad', link: 'https://www.imdb.com/title/tt0460649/characters/nm0004989' }, { id: '3', name: 'Marshall Eriksen', username: 'marshmallow', link: 'https://www.imdb.com/title/tt0460649/characters/nm0781981' }, { id: '4', name: 'Robin Scherbatsky', username: 'rsparkles', link: 'https://www.imdb.com/title/tt0460649/characters/nm1130627' }, { id: '5', name: 'Ted Mosby', username: 'tdog', link: 'https://www.imdb.com/title/tt0460649/characters/nm1102140' } ]; function getFeedItems( queryText ) { // As an example of an asynchronous action, let's return a promise // that resolves after a 100ms timeout. // This can be a server request, or any sort of delayed action. return new Promise( resolve => { setTimeout( () => { const itemsToDisplay = items // Filter out the full list of all items to only those matching queryText. .filter( isItemMatching ) // Return 10 items max - needed for generic queries when the list may contain hundreds of elements. .slice( 0, 10 ); resolve( itemsToDisplay ); }, 100 ); } ); // Filtering function - it uses `name` and `username` properties of an item to find a match. function isItemMatching( item ) { // Make search case-insensitive. const searchString = queryText.toLowerCase(); // Include an item in the search results if name or username includes the current user input. return ( item.name.toLowerCase().includes( searchString ) || item.username.toLowerCase().includes( searchString ) ); } } ``` A full, working demo with all possible customizations and its source code is available {@link features/mention#fully-customized-mention-feed at the end of this section}. ### Customizing the autocomplete list The items displayed in the autocomplete list can be customized by defining the {@link module:mention/mention~MentionFeed `itemRenderer`} callback. This callback takes a feed item (it contains at least the `name` property). The item renderer function must return a new DOM element. ```js ClassicEditor .create( document.querySelector( '#editor' ), { plugins: [ Mention, ... ], mention: { feeds: [ { feed: [ ... ], // Define the custom item renderer: itemRenderer: customItemRenderer } ] } } ) .then( ... ) .catch( ... ); function customItemRenderer( item ) { const itemElement = document.createElement( 'span' ); itemElement.classList.add( 'custom-item' ); itemElement.id = `mention-list-item-id-${ item.id }`; itemElement.textContent = `${ item.name } `; const usernameElement = document.createElement( 'span' ); usernameElement.classList.add( 'custom-item-username' ); usernameElement.textContent = `@${ item.username }`; itemElement.appendChild( usernameElement ); return itemElement; } ``` A full, working demo with all possible customizations and its source code is available {@link features/mention#fully-customized-mention-feed at the end of this section}. ### Customizing the output In order to change the markup generated by the editor for mentions you can overwrite the default converter of the mention feature. To do that, you must specify both {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher upcast} and {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher downcast} converters. The example below defined a plugin which overrides the default output: ```html @Ted ``` To a link: ```html @Ted Mosby ``` The converters must be defined with a `'high'` priority to be executed before the link feature's converter and before the default converter of the mention feature. A mention is stored in the model as a text attribute which stores an object (see {@link module:mention/mention~MentionFeedItem}). ```js ClassicEditor .create( document.querySelector( '#editor' ), { plugins: [ Mention, MentionCustomization, ... ], // Add custom mention plugin function. mention: { // Configuration... } } ) .then( ... ) .catch( ... ); function MentionCustomization( editor ) { // The upcast converter will convert view // elements to the model 'mention' text attribute. editor.conversion.for( 'upcast' ).elementToAttribute( { view: { name: 'a', key: 'data-mention', classes: 'mention', attributes: { href: true, 'data-user-id': true } }, model: { key: 'mention', value: viewItem => { // The mention feature expects that the mention attribute value // in the model is a plain object: const mentionValue = { // The name attribute is required. name: viewItem.getAttribute( 'data-mention' ), // Add any other properties that you need. link: viewItem.getAttribute( 'href' ), id: viewItem.getAttribute( 'data-user-id' ) }; return mentionValue; } }, converterPriority: 'high' } ); // Downcast the model 'mention' text attribute to a view element. editor.conversion.for( 'downcast' ).attributeToElement( { model: 'mention', view: ( modelAttributeValue, viewWriter ) => { // Do not convert empty attributes (lack of value means no mention). if ( !modelAttributeValue ) { return; } return viewWriter.createAttributeElement( 'a', { class: 'mention', 'data-mention': modelAttributeValue.name, 'data-user-id': modelAttributeValue.id, 'href': modelAttributeValue.link } ); }, converterPriority: 'high' } ); } ``` The full working demo with all customization possible is {@link features/mention#fully-customized-mention-feed at the end of this section}. ### Fully customized mention feed Below is an example of a customized mention feature that: * Uses a feed of items with additional properties (`id`, `username`, `link`). * Renders custom item views in the autocomplete panel. * Converts mention to an `` element instead of ``. {@snippet features/mention-customization} #### Source code ```js ClassicEditor .create( document.querySelector( '#snippet-mention-customization' ), { plugins: [ Mention, MentionCustomization, ... ], mention: { feeds: [ { marker: '@', feed: getFeedItems, itemRenderer: customItemRenderer, minimumCharacters: 1 } ] } } ) .then( editor => { window.editor = editor; } ) .catch( err => { console.error( err.stack ); } ); function MentionCustomization( editor ) { // The upcast converter will convert // elements to the model 'mention' attribute. editor.conversion.for( 'upcast' ).elementToAttribute( { view: { name: 'a', key: 'data-mention', classes: 'mention', attributes: { href: true, 'data-user-id': true } }, model: { key: 'mention', value: viewItem => { // The mention feature expects that the mention attribute value // in the model is a plain object: const mentionValue = { // The name attribute is required. name: viewItem.getAttribute( 'data-mention' ), // Add any other properties that you need. link: viewItem.getAttribute( 'href' ), id: viewItem.getAttribute( 'data-user-id' ) }; return mentionValue; } }, converterPriority: 'high' } ); // Do not forget to define a downcast converter as well: editor.conversion.for( 'downcast' ).attributeToElement( { model: 'mention', view: ( modelAttributeValue, viewWriter ) => { // Do not convert empty attributes (lack of value means no mention). if ( !modelAttributeValue ) { return; } return viewWriter.createAttributeElement( 'a', { class: 'mention', 'data-mention': modelAttributeValue.name, 'data-user-id': modelAttributeValue.id, 'href': modelAttributeValue.link } ); }, converterPriority: 'high' } ); } const items = [ { id: '1', name: 'Barney Stinson', username: 'swarley', link: 'https://www.imdb.com/title/tt0460649/characters/nm0000439' }, { id: '2', name: 'Lily Aldrin', username: 'lilypad', link: 'https://www.imdb.com/title/tt0460649/characters/nm0004989' }, { id: '3', name: 'Marshall Eriksen', username: 'marshmallow', link: 'https://www.imdb.com/title/tt0460649/characters/nm0781981' }, { id: '4', name: 'Robin Scherbatsky', username: 'rsparkles', link: 'https://www.imdb.com/title/tt0460649/characters/nm1130627' }, { id: '5', name: 'Ted Mosby', username: 'tdog', link: 'https://www.imdb.com/title/tt0460649/characters/nm1102140' } ]; function getFeedItems( queryText ) { // As an example of an asynchronous action, let's return a promise // that resolves after a 100ms timeout. // This can be a server request, or any sort of delayed action. return new Promise( resolve => { setTimeout( () => { resolve( items.filter( isItemMatching ) ); }, 100 ); } ); // Filtering function - it uses `name` and `username` properties of an item to find a match. function isItemMatching( item ) { // Make search case-insensitive. const searchString = queryText.toLowerCase(); // Include an item in the search results if name or username includes the current user input. return ( item.name.toLowerCase().includes( searchString ) || item.username.toLowerCase().includes( searchString ) ); } } function customItemRenderer( item ) { const itemElement = document.createElement( 'span' ); itemElement.classList.add( 'custom-item' ); itemElement.id = `mention-list-item-id-${ item.id }`; itemElement.textContent = `${ item.name } `; const usernameElement = document.createElement( 'span' ); usernameElement.classList.add( 'custom-item-username' ); usernameElement.textContent = `@${ item.username }`; itemElement.appendChild( usernameElement ); return itemElement; } ``` ### Colors and styles #### Using CSS variables The mention feature is using the power of [CSS variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_variables) which are defined in the [theme lark stylesheet](https://github.com/ckeditor/ckeditor5-theme-lark/blob/master/theme/ckeditor5-mention/mentionediting.css). Thanks to that mention styles can be easily customized: ```css :root { /* Make mention background blue. */ --ck-color-mention-background: hsla(220, 100%, 54%, 0.4); /* Make mention text dark grey. */ --ck-color-mention-text: hsl(0, 0%, 15%); } ``` {@snippet features/custom-mention-colors-variables} ## Installation To add this feature to your editor, install the [`@ckeditor/ckeditor5-mention`](https://www.npmjs.com/package/@ckeditor/ckeditor5-mention) package: ```bash npm install --save @ckeditor/ckeditor5-mention ``` Then add `Mention` to your plugin list and {@link module:mention/mention~MentionConfig configure} the feature: ```js import Mention from '@ckeditor/ckeditor5-mention/src/mention'; ClassicEditor .create( document.querySelector( '#editor' ), { plugins: [ Mention, ... ], mention: { // Configuration... } } ) .then( ... ) .catch( ... ); ``` Read more about {@link builds/guides/integration/installing-plugins installing plugins}. ## Common API The {@link module:mention/mention~Mention} plugin registers: * the `'mention'` command implemented by {@link module:mention/mentioncommand~MentionCommand}. You can insert a mention element by executing the following code: ```js editor.execute( 'mention', { marker: '@', mention: 'John' } ); ``` ## Contribute The source code of the feature is available on GitHub in https://github.com/ckeditor/ckeditor5-mention.