Browse Source

Docs: Created the 'Chat with mentions' example.

Aleksander Nowodzinski 6 years ago
parent
commit
5fa167220e

+ 155 - 0
packages/ckeditor5-mention/docs/_snippets/examples/chat-with-mentions.html

@@ -0,0 +1,155 @@
+<div class="chat">
+	<ul class="chat__posts">
+		<li>
+			<img src="../../assets/img/m_1.jpg" alt="avatar" class="photo">
+			<div class="chat__posts__post__message">
+				<strong>Charles Flores</strong>
+				<a class="chat__posts__post__mailto-user" href="mailto:cflores@example.com">@cflores</a>
+				<span class="chat__posts__post__time">2 hours ago</span>
+				<p class="chat__posts__post__content">
+					Thanks for another <a class="mention" data-mention="#yummy" href="https://example.com/social/yummy">#yummy</a> recipe,
+					<a class="mention" data-mention="@dwilliams" href="mailto:dwilliams@example.com">@dwilliams</a>!
+					Makes me <a class="mention" data-mention="#hungry" href="https://example.com/social/hungry">#hungry</a>
+					just looking at the photos 😋. Definitely adding it to my TODO list for our next
+					<a class="mention" data-mention="#mediterranean" href="https://example.com/social/mediterranean">#mediterranean</a> potluck.
+				</p>
+			</div>
+		</li>
+		<li>
+			<img src="../../assets/img/w_1.jpg" alt="avatar" class="photo">
+			<div class="chat__posts__post__message">
+				<strong>Mildred Wilson</strong>
+				<a class="chat__posts__post__mailto-user" href="mailto:mwilson@example.com">@mwilson</a>
+				<span class="chat__posts__post__time">4 hours ago</span>
+				<p class="chat__posts__post__content">
+					Really appreciate the <a class="mention" data-mention="#vegetarian" href="https://example.com/social/vegetarian">#vegetarian</a>
+					and <a class="mention" data-mention="#vegan" href="https://example.com/social/vegan">#vegan</a> variations of your recipes.
+					So thoughtful of you! 🌱
+				</p>
+			</div>
+		</li>
+	</ul>
+	<div class="chat__editor">
+		<p>
+			I agree with <a href="mwilson@example.com" class="mention" data-mention="@mwilson">@mwilson</a> 👍.
+			It’s so nice of you to always be providing a few options to try! I love
+			<a href="https://example.com/social/greek" class="mention" data-mention="#greek">#greek</a> cuisine with a modern twist,
+			this one will be perfect to try.
+		</p>
+	</div>
+
+	<button class="chat-send" type="button">Send</button>
+</div>
+
+<style>
+	/* ---- General layout ---------------------------------------------------------------------- */
+
+	.chat {
+		margin-bottom: 1em
+	}
+
+	/* ---- Chat posts -------------------------------------------------------------------------- */
+
+	.chat ul.chat__posts {
+		border: 1px solid var(--ck-color-base-border);
+		border-top-left-radius: var(--ck-border-radius);
+		border-top-right-radius: var(--ck-border-radius);
+		border-bottom: none;
+		margin: 1em 0 0;
+		padding: 1em;
+		list-style-position: inside;
+	}
+
+	.chat ul.chat__posts li {
+		display: flex;
+	}
+
+	.chat ul.chat__posts li.new-post {
+		/* Highlight a new post in the chat */
+		animation: highlight 600ms ease-out;
+	}
+
+	.chat ul.chat__posts li + li {
+		margin-top: 1em;
+	}
+
+	.chat .chat__posts li .photo {
+		border-radius: 100%;
+		height: 40px;
+		margin-right: 1.5em;
+	}
+
+	.chat .chat__posts li .time {
+		color: hsl(0, 0%, 72%);
+		font-size: .9em;
+	}
+
+	.chat .chat__posts .chat__posts__post__message > strong::after,
+	.chat__posts__post__mailto-user::after {
+		content: "•";
+		padding-left: 5px;
+		padding-right: 5px;
+		color: hsl(0, 0%, 72%);
+	}
+
+	@keyframes highlight {
+		0% {
+			background-color: yellow;
+		}
+
+		100% {
+			background-color: white;
+		}
+	}
+
+	/* ---- Chat editor ------------------------------------------------------------------------- */
+
+	.chat .chat__editor {
+		/* Anti–FOUC (flash of unstyled content). */
+		padding: 1em;
+		border: 1px solid var(--ck-color-base-border);
+	}
+
+	.chat .chat__editor + .ck.ck-editor {
+		margin-top: 0;
+	}
+
+	.chat .chat__editor + .ck.ck-editor .ck.ck-toolbar {
+		border-top-left-radius: 0;
+		border-top-right-radius: 0;
+	}
+
+	/* ---- In–editor mention list --------------------------------------------------------------- */
+
+	.ck-mentions .chat__item {
+		display: block;
+	}
+
+	.ck-mentions .chat__item img {
+		border-radius: 100%;
+		height: 30px;
+	}
+
+	.ck-mentions .chat__item span {
+		margin-left: .5em;
+	}
+
+	.ck-mentions .chat__item.ck-on .chat__item__user-name {
+		color: var(--ck-color-base-background);
+	}
+
+	.ck-mentions .chat__item .chat__item__full-name {
+		color: hsl(0, 0%, 67%);
+	}
+
+	/* ---- Chat editor content styles ----------------------------------------------------------- */
+
+	.chat .ck-content .mention {
+		background: unset;
+	}
+
+	.chat .ck.ck-content a,
+	.chat .chat__posts a {
+		color: hsl(231, 89%, 53%);
+	}
+</style>

+ 180 - 0
packages/ckeditor5-mention/docs/_snippets/examples/chat-with-mentions.js

@@ -0,0 +1,180 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals console, window, document */
+
+import ClassicEditor from '@ckeditor/ckeditor5-build-classic/src/ckeditor';
+import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline';
+import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough';
+import Mention from '@ckeditor/ckeditor5-mention/src/mention';
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
+
+ClassicEditor
+	.create( document.querySelector( '.chat__editor' ), {
+		cloudServices: CS_CONFIG,
+		extraPlugins: [ Mention, MentionLinks, Underline, Strikethrough ],
+		toolbar: {
+			items: [
+				'bold', 'italic', 'underline', 'strikethrough', '|', 'link', '|', 'undo', 'redo'
+			],
+			viewportTopOffset: window.getViewportTopOffsetConfig()
+		},
+		mention: {
+			feeds: [
+				{
+					marker: '@',
+					feed: [
+						{ id: '@cflores', avatar: 'm_1', name: 'Charles Flores' },
+						{ id: '@gjackson', avatar: 'm_2', name: 'Gerald Jackson' },
+						{ id: '@wreed', avatar: 'm_3', name: 'Wayne Reed' },
+						{ id: '@lgarcia', avatar: 'm_4', name: 'Louis Garcia' },
+						{ id: '@rwilson', avatar: 'm_5', name: 'Roy Wilson' },
+						{ id: '@mnelson', avatar: 'm_6', name: 'Matthew Nelson' },
+						{ id: '@rwilliams', avatar: 'm_7', name: 'Randy Williams' },
+						{ id: '@ajohnson', avatar: 'm_8', name: 'Albert Johnson' },
+						{ id: '@sroberts', avatar: 'm_9', name: 'Steve Roberts' },
+						{ id: '@kevans', avatar: 'm_10', name: 'Kevin Evans' },
+						{ id: '@mwilson', avatar: 'w_1', name: 'Mildred Wilson' },
+						{ id: '@mnelson', avatar: 'w_2', name: 'Melissa Nelson' },
+						{ id: '@kallen', avatar: 'w_3', name: 'Kathleen Allen' },
+						{ id: '@myoung', avatar: 'w_4', name: 'Mary Young' },
+						{ id: '@arogers', avatar: 'w_5', name: 'Ashley Rogers' },
+						{ id: '@dgriffin', avatar: 'w_6', name: 'Debra Griffin' },
+						{ id: '@dwilliams', avatar: 'w_7', name: 'Denise Williams' },
+						{ id: '@ajames', avatar: 'w_8', name: 'Amy James' },
+						{ id: '@randerson', avatar: 'w_9', name: 'Ruby Anderson' },
+						{ id: '@wlee', avatar: 'w_10', name: 'Wanda Lee' }
+					],
+					itemRenderer: customItemRenderer
+				},
+				{
+					marker: '#',
+					feed: [
+						'#american', '#asian', '#baking', '#breakfast', '#cake', '#caribbean',
+						'#chinese', '#chocolate', '#cooking', '#dairy', '#delicious', '#delish',
+						'#dessert', '#desserts', '#dinner', '#eat', '#eating', '#eggs', '#fish',
+						'#food', '#foodgasm', '#foodie', '#foodporn', '#foods', '#french', '#fresh',
+						'#fusion', '#glutenfree', '#greek', '#grilling', '#halal', '#homemade',
+						'#hot', '#hungry', '#icecream', '#indian', '#italian', '#japanese', '#keto',
+						'#korean', '#lactosefree', '#lunch', '#meat', '#mediterranean', '#mexican',
+						'#moroccan', '#nom', '#nomnom', '#paleo', '#poultry', '#snack', '#spanish',
+						'#sugarfree', '#sweet', '#sweettooth', '#tasty', '#thai', '#vegan',
+						'#vegetarian', '#vietnamese', '#yum', '#yummy'
+					]
+				}
+			]
+		}
+	} )
+	.then( editor => {
+		window.editor = editor;
+
+		// Clone the first message in the chat when "Send" is clicked, fill it with new data
+		// and append to the chat list.
+		document.querySelector( '.chat-send' ).addEventListener( 'click', () => {
+			const clone = document.querySelector( '.chat__posts li' ).cloneNode( true );
+
+			clone.classList.add( 'new-post' );
+			clone.querySelector( 'img' ).src = '../../assets/img/m_0.jpg';
+			clone.querySelector( 'strong' ).textContent = 'CKEditor User';
+
+			const mailtoUser = clone.querySelector( '.chat__posts__post__mailto-user' );
+
+			mailtoUser.textContent = '@ckeditor';
+			mailtoUser.href = 'mailto:info@cksource.com';
+
+			clone.querySelector( '.chat__posts__post__time' ).textContent = 'just now';
+			clone.querySelector( '.chat__posts__post__content' ).innerHTML = editor.getData();
+
+			document.querySelector( '.chat__posts' ).appendChild( clone );
+		} );
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );
+
+/*
+ * This plugin customizes the way mentions are handled in editor model and data.
+ * Instead of classic <span class="mention"></span>,
+ */
+function MentionLinks( editor ) {
+	// The upcast converter will convert a view
+	//
+	//		<a href="..." class="mention" data-mention="...">...</a>
+	//
+	// element to the model "mention" text attribute.
+	editor.conversion.for( 'upcast' ).elementToAttribute( {
+		view: {
+			name: 'a',
+			key: 'data-mention',
+			classes: 'mention',
+			attributes: {
+				href: true
+			}
+		},
+		model: {
+			key: 'mention',
+			value: viewItem => editor.plugins.get( 'Mention' ).toMentionAttribute( viewItem )
+		},
+		converterPriority: 'high'
+	} );
+
+	// Downcast the model "mention" text attribute to a view
+	//
+	//		<a href="..." class="mention" data-mention="...">...</a>
+	//
+	// 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;
+			}
+
+			let href;
+
+			// User mentions are downcasted as mailto: links. Tags become normal URLs.
+			if ( modelAttributeValue.id[ 0 ] === '@' ) {
+				href = `mailto:${ modelAttributeValue.id.slice( 1 ) }@example.com`;
+			} else {
+				href = `https://example.com/social/${ modelAttributeValue.id.slice( 1 ) }`;
+			}
+
+			return viewWriter.createAttributeElement( 'a', {
+				class: 'mention',
+				'data-mention': modelAttributeValue.id,
+				href
+			} );
+		},
+		converterPriority: 'high'
+	} );
+}
+
+/*
+ * Customizes the way list of user suggestions is displayed.
+ * Each user has an @id, a name and an avatar.
+ */
+function customItemRenderer( item ) {
+	const itemElement = document.createElement( 'span' );
+	const avatar = document.createElement( 'img' );
+	const userNameElement = document.createElement( 'span' );
+	const fullNameElement = document.createElement( 'span' );
+
+	itemElement.classList.add( 'snippet-mention__item' );
+
+	avatar.src = `../assets/img/${ item.avatar }.jpg`;
+
+	userNameElement.classList.add( 'snippet-mention__item__user-name' );
+	userNameElement.textContent = item.id;
+
+	fullNameElement.classList.add( 'snippet-mention__item__full-name' );
+	fullNameElement.textContent = item.name;
+
+	itemElement.appendChild( avatar );
+	itemElement.appendChild( userNameElement );
+	itemElement.appendChild( fullNameElement );
+
+	return itemElement;
+}

+ 353 - 0
packages/ckeditor5-mention/docs/examples/chat-with-mentions.md

@@ -0,0 +1,353 @@
+---
+category: examples-framework
+order: 10
+---
+
+# Chat with mentions
+
+The {@link features/mentions Mentions} feature allows developing rich–text applications (e.g. chats) with autocomplete suggestions displayed in the dedicated panel as the user types a pre–configured marker. For instance, in the editor below, type **"@"** to mention users and **"#"** to select from the list of available tags.
+
+{@snippet examples/chat-with-mentions}
+
+Learn how to {@link features/mentions#configuration configure} mention feeds in the dedicated guide and check out the [full source code](#source–code) of this example if you want to implement your own chat.
+
+## Source code
+
+The following code will let you run the editor inside a chat application like in the example above. See the {@link builds/guides/integration/installing-plugins installing plugins} guide to learn more.
+
+**Note**: You may need to change the path to the `assets/img` both in the HTML and JavaScript to load user avatars properly.
+
+The HTML code of the application is listed below:
+
+```html
+<div class="chat">
+	<ul class="chat__posts">
+		<li>
+			<img src="../../assets/img/m_1.jpg" alt="avatar" class="photo">
+			<div class="chat__posts__post__message">
+				<strong>Charles Flores</strong>
+				<a class="chat__posts__post__mailto-user" href="mailto:cflores@example.com">@cflores</a>
+				<span class="chat__posts__post__time">2 hours ago</span>
+				<p class="chat__posts__post__content">
+					Thanks for another <a class="mention" data-mention="#yummy" href="https://example.com/social/yummy">#yummy</a> recipe,
+					<a class="mention" data-mention="@dwilliams" href="mailto:dwilliams@example.com">@dwilliams</a>!
+					Makes me <a class="mention" data-mention="#hungry" href="https://example.com/social/hungry">#hungry</a>
+					just looking at the photos 😋. Definitely adding it to my TODO list for our next
+					<a class="mention" data-mention="#mediterranean" href="https://example.com/social/mediterranean">#mediterranean</a> potluck.
+				</p>
+			</div>
+		</li>
+		<li>
+			<img src="../../assets/img/w_1.jpg" alt="avatar" class="photo">
+			<div class="chat__posts__post__message">
+				<strong>Mildred Wilson</strong>
+				<a class="chat__posts__post__mailto-user" href="mailto:mwilson@example.com">@mwilson</a>
+				<span class="chat__posts__post__time">4 hours ago</span>
+				<p class="chat__posts__post__content">
+					Really appreciate the <a class="mention" data-mention="#vegetarian" href="https://example.com/social/vegetarian">#vegetarian</a>
+					and <a class="mention" data-mention="#vegan" href="https://example.com/social/vegan">#vegan</a> variations of your recipes.
+					So thoughtful of you! 🌱
+				</p>
+			</div>
+		</li>
+	</ul>
+	<div class="chat__editor">
+		<p>
+			I agree with <a href="mwilson@example.com" class="mention" data-mention="@mwilson">@mwilson</a> 👍.
+			It’s so nice of you to always be providing a few options to try! I love
+			<a href="https://example.com/social/greek" class="mention" data-mention="#greek">#greek</a> cuisine with a modern twist,
+			this one will be perfect to try.
+		</p>
+	</div>
+
+	<button class="chat-send" type="button">Send</button>
+</div>
+
+<style>
+	/* ---- General layout ---------------------------------------------------------------------- */
+
+	.chat {
+		margin-bottom: 1em
+	}
+
+	/* ---- Chat posts -------------------------------------------------------------------------- */
+
+	.chat ul.chat__posts {
+		border: 1px solid var(--ck-color-base-border);
+		border-top-left-radius: var(--ck-border-radius);
+		border-top-right-radius: var(--ck-border-radius);
+		border-bottom: none;
+		margin: 1em 0 0;
+		padding: 1em;
+		list-style-position: inside;
+	}
+
+	.chat ul.chat__posts li {
+		display: flex;
+	}
+
+	.chat ul.chat__posts li.new-post {
+		/* Highlight a new post in the chat */
+		animation: highlight 600ms ease-out;
+	}
+
+	.chat ul.chat__posts li + li {
+		margin-top: 1em;
+	}
+
+	.chat .chat__posts li .photo {
+		border-radius: 100%;
+		height: 40px;
+		margin-right: 1.5em;
+	}
+
+	.chat .chat__posts li .time {
+		color: hsl(0, 0%, 72%);
+		font-size: .9em;
+	}
+
+	.chat .chat__posts .chat__posts__post__message > strong::after,
+	.chat__posts__post__mailto-user::after {
+		content: "•";
+		padding-left: 5px;
+		padding-right: 5px;
+		color: hsl(0, 0%, 72%);
+	}
+
+	@keyframes highlight {
+		0% {
+			background-color: yellow;
+		}
+
+		100% {
+			background-color: white;
+		}
+	}
+
+	/* ---- Chat editor ------------------------------------------------------------------------- */
+
+	.chat .chat__editor {
+		/* Anti–FOUC (flash of unstyled content). */
+		padding: 1em;
+		border: 1px solid var(--ck-color-base-border);
+	}
+
+	.chat .chat__editor + .ck.ck-editor {
+		margin-top: 0;
+	}
+
+	.chat .chat__editor + .ck.ck-editor .ck.ck-toolbar {
+		border-top-left-radius: 0;
+		border-top-right-radius: 0;
+	}
+
+	/* ---- In–editor mention list --------------------------------------------------------------- */
+
+	.ck-mentions .chat__item {
+		display: block;
+	}
+
+	.ck-mentions .chat__item img {
+		border-radius: 100%;
+		height: 30px;
+	}
+
+	.ck-mentions .chat__item span {
+		margin-left: .5em;
+	}
+
+	.ck-mentions .chat__item.ck-on .chat__item__user-name {
+		color: var(--ck-color-base-background);
+	}
+
+	.ck-mentions .chat__item .chat__item__full-name {
+		color: hsl(0, 0%, 67%);
+	}
+
+	/* ---- Chat editor content styles ----------------------------------------------------------- */
+
+	.chat .ck-content .mention {
+		background: unset;
+	}
+
+	.chat .ck.ck-content a,
+	.chat .chat__posts a {
+		color: hsl(231, 89%, 53%);
+	}
+</style>
+```
+
+JavaScript required to run the editor:
+
+```js
+import ClassicEditor from '@ckeditor/ckeditor5-build-classic/src/ckeditor';
+import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline';
+import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough';
+import Mention from '@ckeditor/ckeditor5-mention/src/mention';
+
+ClassicEditor
+	.create( document.querySelector( '.chat__editor' ), {
+		extraPlugins: [ Mention, MentionLinks, Underline, Strikethrough ],
+		toolbar: {
+			items: [
+				'bold', 'italic', 'underline', 'strikethrough', '|', 'link', '|', 'undo', 'redo'
+			]
+		},
+		mention: {
+			feeds: [
+				{
+					marker: '@',
+					feed: [
+						{ id: '@cflores', avatar: 'm_1', name: 'Charles Flores' },
+						{ id: '@gjackson', avatar: 'm_2', name: 'Gerald Jackson' },
+						{ id: '@wreed', avatar: 'm_3', name: 'Wayne Reed' },
+						{ id: '@lgarcia', avatar: 'm_4', name: 'Louis Garcia' },
+						{ id: '@rwilson', avatar: 'm_5', name: 'Roy Wilson' },
+						{ id: '@mnelson', avatar: 'm_6', name: 'Matthew Nelson' },
+						{ id: '@rwilliams', avatar: 'm_7', name: 'Randy Williams' },
+						{ id: '@ajohnson', avatar: 'm_8', name: 'Albert Johnson' },
+						{ id: '@sroberts', avatar: 'm_9', name: 'Steve Roberts' },
+						{ id: '@kevans', avatar: 'm_10', name: 'Kevin Evans' },
+						{ id: '@mwilson', avatar: 'w_1', name: 'Mildred Wilson' },
+						{ id: '@mnelson', avatar: 'w_2', name: 'Melissa Nelson' },
+						{ id: '@kallen', avatar: 'w_3', name: 'Kathleen Allen' },
+						{ id: '@myoung', avatar: 'w_4', name: 'Mary Young' },
+						{ id: '@arogers', avatar: 'w_5', name: 'Ashley Rogers' },
+						{ id: '@dgriffin', avatar: 'w_6', name: 'Debra Griffin' },
+						{ id: '@dwilliams', avatar: 'w_7', name: 'Denise Williams' },
+						{ id: '@ajames', avatar: 'w_8', name: 'Amy James' },
+						{ id: '@randerson', avatar: 'w_9', name: 'Ruby Anderson' },
+						{ id: '@wlee', avatar: 'w_10', name: 'Wanda Lee' }
+					],
+					itemRenderer: customItemRenderer
+				},
+				{
+					marker: '#',
+					feed: [
+						'#american', '#asian', '#baking', '#breakfast', '#cake', '#caribbean',
+						'#chinese', '#chocolate', '#cooking', '#dairy', '#delicious', '#delish',
+						'#dessert', '#desserts', '#dinner', '#eat', '#eating', '#eggs', '#fish',
+						'#food', '#foodgasm', '#foodie', '#foodporn', '#foods', '#french', '#fresh',
+						'#fusion', '#glutenfree', '#greek', '#grilling', '#halal', '#homemade',
+						'#hot', '#hungry', '#icecream', '#indian', '#italian', '#japanese', '#keto',
+						'#korean', '#lactosefree', '#lunch', '#meat', '#mediterranean', '#mexican',
+						'#moroccan', '#nom', '#nomnom', '#paleo', '#poultry', '#snack', '#spanish',
+						'#sugarfree', '#sweet', '#sweettooth', '#tasty', '#thai', '#vegan',
+						'#vegetarian', '#vietnamese', '#yum', '#yummy'
+					]
+				}
+			]
+		}
+	} )
+	.then( editor => {
+		window.editor = editor;
+
+		// Clone the first message in the chat when "Send" is clicked, fill it with new data
+		// and append to the chat list.
+		document.querySelector( '.chat-send' ).addEventListener( 'click', () => {
+			const clone = document.querySelector( '.chat__posts li' ).cloneNode( true );
+
+			clone.classList.add( 'new-post' );
+			clone.querySelector( 'img' ).src = '../../assets/img/m_0.jpg';
+			clone.querySelector( 'strong' ).textContent = 'CKEditor User';
+
+			const mailtoUser = clone.querySelector( '.chat__posts__post__mailto-user' );
+
+			mailtoUser.textContent = '@ckeditor';
+			mailtoUser.href = 'mailto:info@cksource.com';
+
+			clone.querySelector( '.chat__posts__post__time' ).textContent = 'just now';
+			clone.querySelector( '.chat__posts__post__content' ).innerHTML = editor.getData();
+
+			document.querySelector( '.chat__posts' ).appendChild( clone );
+		} );
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );
+
+/*
+ * This plugin customizes the way mentions are handled in editor model and data.
+ * Instead of classic <span class="mention"></span>,
+ */
+function MentionLinks( editor ) {
+	// The upcast converter will convert a view
+	//
+	//		<a href="..." class="mention" data-mention="...">...</a>
+	//
+	// element to the model "mention" text attribute.
+	editor.conversion.for( 'upcast' ).elementToAttribute( {
+		view: {
+			name: 'a',
+			key: 'data-mention',
+			classes: 'mention',
+			attributes: {
+				href: true
+			}
+		},
+		model: {
+			key: 'mention',
+			value: viewItem => editor.plugins.get( 'Mention' ).toMentionAttribute( viewItem )
+		},
+		converterPriority: 'high'
+	} );
+
+	// Downcast the model "mention" text attribute to a view
+	//
+	//		<a href="..." class="mention" data-mention="...">...</a>
+	//
+	// 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;
+			}
+
+			let href;
+
+			// User mentions are downcasted as mailto: links. Tags become normal URLs.
+			if ( modelAttributeValue.id[ 0 ] === '@' ) {
+				href = `mailto:${ modelAttributeValue.id.slice( 1 ) }@example.com`;
+			} else {
+				href = `https://example.com/social/${ modelAttributeValue.id.slice( 1 ) }`;
+			}
+
+			return viewWriter.createAttributeElement( 'a', {
+				class: 'mention',
+				'data-mention': modelAttributeValue.id,
+				href
+			} );
+		},
+		converterPriority: 'high'
+	} );
+}
+
+/*
+ * Customizes the way list of user suggestions is displayed.
+ * Each user has an @id, a name and an avatar.
+ */
+function customItemRenderer( item ) {
+	const itemElement = document.createElement( 'span' );
+	const avatar = document.createElement( 'img' );
+	const userNameElement = document.createElement( 'span' );
+	const fullNameElement = document.createElement( 'span' );
+
+	itemElement.classList.add( 'snippet-mention__item' );
+
+	avatar.src = `../assets/img/${ item.avatar }.jpg`;
+
+	userNameElement.classList.add( 'snippet-mention__item__user-name' );
+	userNameElement.textContent = item.id;
+
+	fullNameElement.classList.add( 'snippet-mention__item__full-name' );
+	fullNameElement.textContent = item.name;
+
+	itemElement.appendChild( avatar );
+	itemElement.appendChild( userNameElement );
+	itemElement.appendChild( fullNameElement );
+
+	return itemElement;
+}
+```