Ver Fonte

Merge branch 'master' into t/4

# Conflicts:
#	src/mentionui.js
Maciej Gołaszewski há 6 anos atrás
pai
commit
7e7d8ccf3f
34 ficheiros alterados com 843 adições e 126 exclusões
  1. 5 1
      packages/ckeditor5-mention/README.md
  2. 159 0
      packages/ckeditor5-mention/docs/_snippets/examples/chat-with-mentions.html
  3. 180 0
      packages/ckeditor5-mention/docs/_snippets/examples/chat-with-mentions.js
  4. 3 3
      packages/ckeditor5-mention/docs/_snippets/features/custom-mention-colors-variables.html
  5. 6 6
      packages/ckeditor5-mention/docs/_snippets/features/mention-customization.js
  6. 1 1
      packages/ckeditor5-mention/docs/api/mention.md
  7. BIN
      packages/ckeditor5-mention/docs/assets/img/m_0.jpg
  8. BIN
      packages/ckeditor5-mention/docs/assets/img/m_1.jpg
  9. BIN
      packages/ckeditor5-mention/docs/assets/img/m_10.jpg
  10. BIN
      packages/ckeditor5-mention/docs/assets/img/m_2.jpg
  11. BIN
      packages/ckeditor5-mention/docs/assets/img/m_3.jpg
  12. BIN
      packages/ckeditor5-mention/docs/assets/img/m_4.jpg
  13. BIN
      packages/ckeditor5-mention/docs/assets/img/m_5.jpg
  14. BIN
      packages/ckeditor5-mention/docs/assets/img/m_6.jpg
  15. BIN
      packages/ckeditor5-mention/docs/assets/img/m_7.jpg
  16. BIN
      packages/ckeditor5-mention/docs/assets/img/m_8.jpg
  17. BIN
      packages/ckeditor5-mention/docs/assets/img/m_9.jpg
  18. BIN
      packages/ckeditor5-mention/docs/assets/img/w_1.jpg
  19. BIN
      packages/ckeditor5-mention/docs/assets/img/w_10.jpg
  20. BIN
      packages/ckeditor5-mention/docs/assets/img/w_2.jpg
  21. BIN
      packages/ckeditor5-mention/docs/assets/img/w_3.jpg
  22. BIN
      packages/ckeditor5-mention/docs/assets/img/w_4.jpg
  23. BIN
      packages/ckeditor5-mention/docs/assets/img/w_5.jpg
  24. BIN
      packages/ckeditor5-mention/docs/assets/img/w_6.jpg
  25. BIN
      packages/ckeditor5-mention/docs/assets/img/w_7.jpg
  26. BIN
      packages/ckeditor5-mention/docs/assets/img/w_8.jpg
  27. BIN
      packages/ckeditor5-mention/docs/assets/img/w_9.jpg
  28. 357 0
      packages/ckeditor5-mention/docs/examples/chat-with-mentions.md
  29. 33 29
      packages/ckeditor5-mention/docs/features/mentions.md
  30. 13 12
      packages/ckeditor5-mention/src/mention.js
  31. 15 14
      packages/ckeditor5-mention/src/mentioncommand.js
  32. 20 19
      packages/ckeditor5-mention/src/mentionediting.js
  33. 38 28
      packages/ckeditor5-mention/src/mentionui.js
  34. 13 13
      packages/ckeditor5-mention/src/textwatcher.js

+ 5 - 1
packages/ckeditor5-mention/README.md

@@ -10,7 +10,11 @@ CKEditor 5 mention feature
 [![Dependency Status](https://david-dm.org/ckeditor/ckeditor5-mention/status.svg)](https://david-dm.org/ckeditor/ckeditor5-mention)
 [![devDependency Status](https://david-dm.org/ckeditor/ckeditor5-mention/dev-status.svg)](https://david-dm.org/ckeditor/ckeditor5-mention?type=dev)
 
-This package implements mention support for CKEditor 5.
+This package implements mention support for CKEditor 5 and brings smart autocompletion based on user input.
+
+## Demo
+
+Check out the demo in the [Mentions (autocomplete) feature](https://ckeditor.com/docs/ckeditor5/latest/features/mentions.html) guide.
 
 ## Documentation
 

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

@@ -0,0 +1,159 @@
+<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="mailto: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 .mention__item {
+		display: block;
+	}
+
+	.ck-mentions .mention__item img {
+		border-radius: 100%;
+		height: 30px;
+	}
+
+	.ck-mentions .mention__item span {
+		margin-left: .5em;
+	}
+
+	.ck-mentions .mention__item.ck-on span {
+		color: var(--ck-color-base-background);
+	}
+
+	.ck-mentions .mention__item .mention__item__full-name {
+		color: hsl(0, 0%, 45%);
+	}
+
+	.ck-mentions .mention__item:hover:not(.ck-on) .mention__item__full-name {
+		color: hsl(0, 0%, 40%);
+	}
+
+	/* ---- 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 the editor model and data.
+ * Instead of a 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 the 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( 'mention__item' );
+
+	avatar.src = `../../assets/img/${ item.avatar }.jpg`;
+
+	userNameElement.classList.add( 'mention__item__user-name' );
+	userNameElement.textContent = item.id;
+
+	fullNameElement.classList.add( 'mention__item__full-name' );
+	fullNameElement.textContent = item.name;
+
+	itemElement.appendChild( avatar );
+	itemElement.appendChild( userNameElement );
+	itemElement.appendChild( fullNameElement );
+
+	return itemElement;
+}

+ 3 - 3
packages/ckeditor5-mention/docs/_snippets/features/custom-mention-colors-variables.html

@@ -1,10 +1,10 @@
 <style>
-	/* We are not using here :root because of many instances of editor */
+	/* We are not using :root here because of many instances of the editor. */
 	#snippet-mention-custom-colors + .ck-editor {
-		/* Make mention background blue. */
+		/* Make the mention background blue. */
 		--ck-color-mention-background: hsla(220, 100%, 54%, 0.4);
 
-		/* Make mention text dark grey. */
+		/* Make the mention text dark grey. */
 		--ck-color-mention-text: hsl(0, 0%, 15%);
 	}
 </style>

+ 6 - 6
packages/ckeditor5-mention/docs/_snippets/features/mention-customization.js

@@ -52,7 +52,7 @@ function MentionCustomization( editor ) {
 			value: viewItem => {
 				// The mention feature expects that the mention attribute value
 				// in the model is a plain object with a set of additional attributes.
-				// In order to create a proper object use the toMentionAttribute() helper method:
+				// In order to create a proper object, use the toMentionAttribute() helper method:
 				const mentionAttribute = editor.plugins.get( 'Mention' ).toMentionAttribute( viewItem, {
 					// Add any other properties that you need.
 					link: viewItem.getAttribute( 'href' ),
@@ -94,13 +94,13 @@ const items = [
 ];
 
 function getFeedItems( queryText ) {
-	// As an example of an asynchronous action, let's return a promise
+	// As an example of an asynchronous action, 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 out the full list of all items to only those matching the query text.
 				.filter( isItemMatching )
 				// Return 10 items max - needed for generic queries when the list may contain hundreds of elements.
 				.slice( 0, 10 );
@@ -109,12 +109,12 @@ function getFeedItems( queryText ) {
 		}, 100 );
 	} );
 
-	// Filtering function - it uses `name` and `username` properties of an item to find a match.
+	// Filtering function - it uses the `name` and `username` properties of an item to find a match.
 	function isItemMatching( item ) {
-		// Make search case-insensitive.
+		// Make the search case-insensitive.
 		const searchString = queryText.toLowerCase();
 
-		// Include an item in the search results if name or username includes the current user input.
+		// Include an item in the search results if the name or username includes the current user input.
 		return (
 			item.name.toLowerCase().includes( searchString ) ||
 			item.id.toLowerCase().includes( searchString )

+ 1 - 1
packages/ckeditor5-mention/docs/api/mention.md

@@ -6,7 +6,7 @@ category: api-reference
 
 [![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-mention.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-mention)
 
-This package implements the mention feature for CKEditor 5. This features provides smart completion functionality for custom text matches based on user input.
+This package implements the mention feature for CKEditor 5. It provides smart completion functionality for custom text matches based on user input.
 
 ## Demo
 

BIN
packages/ckeditor5-mention/docs/assets/img/m_0.jpg


BIN
packages/ckeditor5-mention/docs/assets/img/m_1.jpg


BIN
packages/ckeditor5-mention/docs/assets/img/m_10.jpg


BIN
packages/ckeditor5-mention/docs/assets/img/m_2.jpg


BIN
packages/ckeditor5-mention/docs/assets/img/m_3.jpg


BIN
packages/ckeditor5-mention/docs/assets/img/m_4.jpg


BIN
packages/ckeditor5-mention/docs/assets/img/m_5.jpg


BIN
packages/ckeditor5-mention/docs/assets/img/m_6.jpg


BIN
packages/ckeditor5-mention/docs/assets/img/m_7.jpg


BIN
packages/ckeditor5-mention/docs/assets/img/m_8.jpg


BIN
packages/ckeditor5-mention/docs/assets/img/m_9.jpg


BIN
packages/ckeditor5-mention/docs/assets/img/w_1.jpg


BIN
packages/ckeditor5-mention/docs/assets/img/w_10.jpg


BIN
packages/ckeditor5-mention/docs/assets/img/w_2.jpg


BIN
packages/ckeditor5-mention/docs/assets/img/w_3.jpg


BIN
packages/ckeditor5-mention/docs/assets/img/w_4.jpg


BIN
packages/ckeditor5-mention/docs/assets/img/w_5.jpg


BIN
packages/ckeditor5-mention/docs/assets/img/w_6.jpg


BIN
packages/ckeditor5-mention/docs/assets/img/w_7.jpg


BIN
packages/ckeditor5-mention/docs/assets/img/w_8.jpg


BIN
packages/ckeditor5-mention/docs/assets/img/w_9.jpg


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

@@ -0,0 +1,357 @@
+---
+category: examples-framework
+order: 10
+---
+
+# Chat with mentions
+
+The {@link features/mentions mention} feature allows developing rich–text applications (e.g. chats) with autocomplete suggestions displayed in a 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 using CKEditor 5 WYSIWYG editor.
+
+## 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 code 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="mailto: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 .mention__item {
+		display: block;
+	}
+
+	.ck-mentions .mention__item img {
+		border-radius: 100%;
+		height: 30px;
+	}
+
+	.ck-mentions .mention__item span {
+		margin-left: .5em;
+	}
+
+	.ck-mentions .mention__item.ck-on span {
+		color: var(--ck-color-base-background);
+	}
+
+	.ck-mentions .mention__item .mention__item__full-name {
+		color: hsl(0, 0%, 45%);
+	}
+
+	.ck-mentions .mention__item:hover:not(.ck-on) .mention__item__full-name {
+		color: hsl(0, 0%, 40%);
+	}
+
+	/* ---- 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 code 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 the editor model and data.
+ * Instead of a 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 the 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( 'mention__item' );
+
+	avatar.src = `../../assets/img/${ item.avatar }.jpg`;
+
+	userNameElement.classList.add( 'mention__item__user-name' );
+	userNameElement.textContent = item.id;
+
+	fullNameElement.classList.add( 'mention__item__full-name' );
+	fullNameElement.textContent = item.name;
+
+	itemElement.appendChild( avatar );
+	itemElement.appendChild( userNameElement );
+	itemElement.appendChild( fullNameElement );
+
+	return itemElement;
+}
+```

+ 33 - 29
packages/ckeditor5-mention/docs/features/mentions.md

@@ -11,10 +11,14 @@ The {@link module:mention/mention~Mention} feature brings support for smart auto
 
 ## Demo
 
-You can type the "@" character to invoke mention autocomplete UI. The demo below is configured to suggest a static list of names ("Barney", "Lily", "Marshall", "Robin", and "Ted").
+You can type the "@" character to invoke the mention autocomplete UI. The demo below is configured to suggest a static list of names ("Barney", "Lily", "Marshall", "Robin", and "Ted").
 
 {@snippet features/mention}
 
+<info-box>
+	Check out the {@link examples/chat-with-mentions more advanced example} of the mention feature used in a chat application.
+</info-box>
+
 ## 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 show up.
@@ -46,24 +50,24 @@ 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 {@link framework/guides/architecture/editing-engine#conversion conversion}. See ["Customizing the output"](#customizing-the-output).
-* Multiple feeds &mdash; 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.
+* Multiple feeds. Te demo above uses 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 &mdash; good for scenarios with a relatively small set of autocomplete items.
-* a callback &mdash; which provides more control over the returned list of items.
+* A static array &ndash; Good for scenarios with a relatively small set of autocomplete items.
+* A callback &ndash; It 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 strings or 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 a callback you can return a `Promise` that resolves with the list of {@link module:mention/mention~MentionFeedItem matching feed items}. These can be simple strings or 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).
 
 <info-box>
 	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 minimum characters typed instead of action on marker alone.
+	You can also consider adding the `minimumCharacters` option to the feed configuration so the editor will call the feed callback after some minimum number of characters typed instead of an action on a marker alone.
 </info-box>
 
-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.
+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 the feed text.
 
 ```js
 ClassicEditor
@@ -79,7 +83,7 @@ ClassicEditor
 					feed: getFeedItems
 				}
 			}
-		}
+		]
 	} )
 	.then( ... )
 	.catch( ... );
@@ -93,13 +97,13 @@ const items = [
 ];
 
 function getFeedItems( queryText ) {
-	// As an example of an asynchronous action, let's return a promise
+	// As an example of an asynchronous action, 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 out the full list of all items to only those matching the query text.
 				.filter( isItemMatching )
 				// Return 10 items max - needed for generic queries when the list may contain hundreds of elements.
 				.slice( 0, 10 );
@@ -108,12 +112,12 @@ function getFeedItems( queryText ) {
 		}, 100 );
 	} );
 
-	// Filtering function - it uses `name` and `username` properties of an item to find a match.
+	// Filtering function - it uses the `name` and `username` properties of an item to find a match.
 	function isItemMatching( item ) {
-		// Make search case-insensitive.
+		// Make the search case-insensitive.
 		const searchString = queryText.toLowerCase();
 
-		// Include an item in the search results if name or username includes the current user input.
+		// Include an item in the search results if the name or username includes the current user input.
 		return (
 			item.name.toLowerCase().includes( searchString ) ||
 			item.id.toLowerCase().includes( searchString )
@@ -138,7 +142,7 @@ ClassicEditor
 			feeds: [
 				{
 					feed: [ ... ],
-					// Define the custom item renderer:
+					// Define the custom item renderer.
 					itemRenderer: customItemRenderer
 				}
 			]
@@ -171,7 +175,7 @@ A full, working demo with all possible customizations and its source code is ava
 
 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:
+The example below defines a plugin that overrides the default output:
 
 ```html
 <span data-mention="@Ted" class="mention">@Ted</span>
@@ -183,12 +187,12 @@ To a link:
 <a class="mention" data-mention="@Ted" data-user-id="5" href="https://www.imdb.com/title/tt0460649/characters/nm1102140">@tdog</a>
 ```
 
-The converters must be defined with a `'high'` priority to be executed before the {@link features/link link} feature's converter and before the default converter of the mention feature. A mention is stored in the model as a {@link framework/guides/architecture/editing-engine#text-attributes text attribute} which stores an object (see {@link module:mention/mention~MentionFeedItem}).
+The converters must be defined with a `'high'` priority to be executed before the {@link features/link link} feature's converter and before the default converter of the mention feature. A mention is stored in the model as a {@link framework/guides/architecture/editing-engine#text-attributes text attribute} that stores an object (see {@link module:mention/mention~MentionFeedItem}).
 
 ```js
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
-		plugins: [ Mention, MentionCustomization, ... ], // Add custom mention plugin function.
+		plugins: [ Mention, MentionCustomization, ... ], // Add the custom mention plugin function.
 		mention: {
 			// Configuration...
 		}
@@ -248,7 +252,7 @@ function MentionCustomization( editor ) {
 }
 ```
 
-The full working demo with all customization possible is {@link features/mentions#fully-customized-mention-feed  at the end of this section}.
+A full, working demo with all possible customizations and its source code is available {@link features/mentions#fully-customized-mention-feed at the end of this section}.
 
 ### Fully customized mention feed
 
@@ -256,7 +260,7 @@ 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 `<a>` element instead of a `<span>`.
+* Converts a mention to an `<a>` element instead of a `<span>`.
 
 {@snippet features/mention-customization}
 
@@ -300,8 +304,8 @@ function MentionCustomization( editor ) {
 			key: 'mention',
 			value: viewItem => {
 				// The mention feature expects that the mention attribute value
-				// in the model is a plain object with set of additional attributes.
-				// In order to create proper object use `toMentionAttribute` helper method:
+				// in the model is a plain object with a set of additional attributes.
+				// In order to create a proper object, use the toMentionAttribute helper method:
 				const mentionAttribute = editor.plugins.get( 'Mention' ).toMentionAttribute( viewItem, {
 					// Add any other properties that you need.
 					link: viewItem.getAttribute( 'href' ),
@@ -314,7 +318,7 @@ function MentionCustomization( editor ) {
 		converterPriority: 'high'
 	} );
 
-	// Do not forget to define a downcast converter as well:
+	// Downcast the model 'mention' text attribute to a view <a> element.
 	editor.conversion.for( 'downcast' ).attributeToElement( {
 		model: 'mention',
 		view: ( modelAttributeValue, viewWriter ) => {
@@ -343,13 +347,13 @@ const items = [
 ];
 
 function getFeedItems( queryText ) {
-	// As an example of an asynchronous action, let's return a promise
+	// As an example of an asynchronous action, 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 out the full list of all items to only those matching the query text.
 				.filter( isItemMatching )
 				// Return 10 items max - needed for generic queries when the list may contain hundreds of elements.
 				.slice( 0, 10 );
@@ -360,7 +364,7 @@ function getFeedItems( queryText ) {
 
 	// Filtering function - it uses `name` and `username` properties of an item to find a match.
 	function isItemMatching( item ) {
-		// Make search case-insensitive.
+		// Make the search case-insensitive.
 		const searchString = queryText.toLowerCase();
 
 		// Include an item in the search results if name or username includes the current user input.
@@ -393,14 +397,14 @@ function customItemRenderer( item ) {
 
 #### 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 {@link framework/guides/theme-customization easily customized}:
+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 [Lark theme stylesheet](https://github.com/ckeditor/ckeditor5-theme-lark/blob/master/theme/ckeditor5-mention/mentionediting.css). Thanks to that, mention styles can be {@link framework/guides/theme-customization easily customized}:
 
 ```css
 :root {
-	/* Make mention background blue. */
+	/* Make the mention background blue. */
 	--ck-color-mention-background: hsla(220, 100%, 54%, 0.4);
 
-	/* Make mention text dark grey. */
+	/* Make the mention text dark grey. */
 	--ck-color-mention-text: hsl(0, 0%, 15%);
 }
 ```
@@ -438,7 +442,7 @@ ClassicEditor
 ## Common API
 
 The {@link module:mention/mention~Mention} plugin registers:
-* the `'mention'` command implemented by {@link module:mention/mentioncommand~MentionCommand}.
+* The `'mention'` command implemented by {@link module:mention/mentioncommand~MentionCommand}.
 
 	You can insert a mention element by executing the following code:
 

+ 13 - 12
packages/ckeditor5-mention/src/mention.js

@@ -27,7 +27,7 @@ export default class Mention extends Plugin {
 	 *
 	 *		editor.plugins.get( 'Mention' ).toMentionAttribute( viewElement, { userId: '1234' } );
 	 *
-	 *		// for a viewElement: <span data-mention="@joe">@John Doe</span>
+	 *		// For a view element: <span data-mention="@joe">@John Doe</span>
 	 *		// it will return:
 	 *		// { id: '@joe', userId: '1234', _uid: '7a7bc7...', _text: '@John Doe' }
 	 *
@@ -70,7 +70,7 @@ export default class Mention extends Plugin {
  *
  *		ClassicEditor
  *			.create( editorElement, {
- *				mention: ... // Media embed feature options.
+ *				mention: ... // Mention feature options.
  *			} )
  *			.then( ... )
  *			.catch( ... );
@@ -99,7 +99,7 @@ export default class Mention extends Plugin {
  *			.then( ... )
  *			.catch( ... );
  *
- * You can provide as many mention feeds but they must use different `marker`s.
+ * You can provide many mention feeds but they must use different `marker`s.
  * For example, you can use `'@'` to autocomplete people and `'#'` to autocomplete tags.
  *
  * @member {Array.<module:mention/mention~MentionFeed>} module:mention/mention~MentionConfig#feeds
@@ -117,7 +117,7 @@ export default class Mention extends Plugin {
  *			minimumCharacters: 2
  *		};
  *
- *		// Simple, synchronous callback.
+ *		// Simple synchronous callback.
  *		const mentionFeedTags = {
  *			marker: '#',
  *			feed: searchString => {
@@ -154,18 +154,18 @@ export default class Mention extends Plugin {
  *
  * @typedef {Object} module:mention/mention~MentionFeed
  * @property {String} [marker] The character which triggers autocompletion for mention. It must be a single character.
- * @property {Array.<module:mention/mention~MentionFeedItem>|Function} feed The autocomplete items. Provide an array for
+ * @property {Array.<module:mention/mention~MentionFeedItem>|Function} feed Autocomplete items. Provide an array for
  * a static configuration (the mention feature will show matching items automatically) or a function which returns an array of
  * matching items (directly, or via a promise).
  * @property {Number} [minimumCharacters=0] Specifies after how many characters the autocomplete panel should be shown.
- * @property {Function} [itemRenderer] Function that renders a {@link module:mention/mention~MentionFeedItem}
+ * @property {Function} [itemRenderer] A function that renders a {@link module:mention/mention~MentionFeedItem}
  * to the autocomplete panel.
  */
 
 /**
  * The mention feed item. It may be defined as a string or a plain object.
  *
- * When defining a feed item as a plain object, the `id` property is obligatory. The additional properties
+ * When defining a feed item as a plain object, the `id` property is obligatory. Additional properties
  * can be used when customizing the mention feature bahavior
  * (see {@glink features/mentions#customizing-the-autocomplete-list "Customizing the autocomplete list"}
  * and {@glink features/mentions#customizing-the-output "Customizing the output"} sections).
@@ -215,18 +215,19 @@ export default class Mention extends Plugin {
  *			.catch( ... );
  *
  * @typedef {Object|String} module:mention/mention~MentionFeedItem
- * @property {String} id Unique id of the mention. It must start with the marker character.
+ * @property {String} id A unique ID of the mention. It must start with the marker character.
  * @property {String} [text] Text inserted into the editor when creating a mention.
  */
 
 /**
- * Represents mention in the model.
+ * Represents a mention in the model.
  *
  * See {@link module:mention/mention~Mention#toMentionAttribute `Mention#toMentionAttribute()`}.
  *
  * @interface module:mention/mention~MentionAttribute
- * @property {String} id Id of a mention – identifies the mention item in mention feed.
- * @property {String} _uid Internal mention view item id. Should be passed as an `option.id` when using
+ * @property {String} id The ID of a mention. It identifies the mention item in the mention feed.
+ * @property {String} _uid An internal mention view item ID. Should be passed as an `option.id` when using
  * {@link module:engine/view/downcastwriter~DowncastWriter#createAttributeElement writer.createAttributeElement()}.
- * @property {String} _text Helper property that holds text of inserted mention. Used for detecting broken mention in the editing area.
+ * @property {String} _text Helper property that stores the text of the inserted mention. Used for detecting a broken mention
+ * in the editing area.
  */

+ 15 - 14
packages/ckeditor5-mention/src/mentioncommand.js

@@ -15,14 +15,14 @@ import { _addMentionAttributes } from './mentionediting';
 /**
  * The mention command.
  *
- * The command is registered by the {@link module:mention/mentionediting~MentionEditing} as `'mention'`.
+ * The command is registered by {@link module:mention/mentionediting~MentionEditing} as `'mention'`.
  *
- * To insert a mention on a range, execute the command and specify a mention object and a range to replace:
+ * To insert a mention onto a range, execute the command and specify a mention object with a range to replace:
  *
  *		const focus = editor.model.document.selection.focus;
  *
- *		// It will replace one character before selection focus with '#1234' text
- *		// with mention attribute filled with passed attributes.
+ *		// It will replace one character before the selection focus with the '#1234' text
+ *		// with the mention attribute filled with passed attributes.
  *		editor.execute( 'mention', {
  *			marker: '#',
  *			mention: {
@@ -33,8 +33,8 @@ import { _addMentionAttributes } from './mentionediting';
  *			range: model.createRange( focus, focus.getShiftedBy( -1 ) )
  *		} );
  *
- *		// It will replace one character before selection focus with 'Teh "Big Foo"' text
- *		// with attribute filled with passed attributes.
+ *		// It will replace one character before the selection focus with the 'The "Big Foo"' text
+ *		// with the mention attribute filled with passed attributes.
  *		editor.execute( 'mention', {
  *			marker: '#',
  *			mention: {
@@ -63,12 +63,13 @@ export default class MentionCommand extends Command {
 	 * Executes the command.
 	 *
 	 * @param {Object} [options] Options for the executed command.
-	 * @param {Object|String} options.mention Mention object to insert. If passed a string it will be used to create a plain object with
-	 * name attribute equal to passed string.
+	 * @param {Object|String} options.mention The mention object to insert. When a string is passed, it will be used to create a plain
+	 * object with the name attribute that equals the passed string.
 	 * @param {String} options.marker The marker character (e.g. `'@'`).
-	 * @param {String} [options.text] The text of inserted mention. Defaults to full mention string composed from `marker` and
-	 * `mention` string or `mention.id` if object is passed.
-	 * @param {String} [options.range] Range to replace. Note that replace range might be shorter then inserted text with mention attribute.
+	 * @param {String} [options.text] The text of the inserted mention. Defaults to the full mention string composed from `marker` and
+	 * `mention` string or `mention.id` if an object is passed.
+	 * @param {String} [options.range] The range to replace. Note that the replaced range might be shorter than the inserted text with the
+	 * mention attribute.
 	 * @fires execute
 	 */
 	execute( options ) {
@@ -102,7 +103,7 @@ export default class MentionCommand extends Command {
 
 		if ( mentionID.charAt( 0 ) != options.marker ) {
 			/**
-			 * The feed item id must start with the marker character.
+			 * The feed item ID must start with the marker character.
 			 *
 			 * Correct mention feed setting:
 			 *
@@ -135,11 +136,11 @@ export default class MentionCommand extends Command {
 
 			attributesWithMention.set( 'mention', mention );
 
-			// Replace range with a text with mention.
+			// Replace a range with the text with a mention.
 			writer.remove( range );
 			writer.insertText( mentionText, attributesWithMention, range.start );
 
-			// Insert space after a mention.
+			// Insert a space after the mention.
 			writer.insertText( ' ', currentAttributes, model.document.selection.focus );
 		} );
 	}

+ 20 - 19
packages/ckeditor5-mention/src/mentionediting.js

@@ -37,7 +37,7 @@ export default class MentionEditing extends Plugin {
 		const model = editor.model;
 		const doc = model.document;
 
-		// Allow mention attribute on all text nodes.
+		// Allow the mention attribute on all text nodes.
 		model.schema.extend( '$text', { allowAttributes: 'mention' } );
 
 		editor.conversion.for( 'upcast' ).elementToAttribute( {
@@ -70,7 +70,7 @@ export function _addMentionAttributes( baseMentionData, data ) {
 }
 
 /**
- * Creates mention attribute value from provided view element and optional data.
+ * Creates a mention attribute value from the provided view element and optional data.
  *
  * This function is exposed as
  * {@link module:mention/mention~Mention#toMentionAttribute `editor.plugins.get( 'Mention' ).toMentionAttribute()`}.
@@ -98,7 +98,7 @@ export function _toMentionAttribute( viewElementOrMention, data ) {
 	return _addMentionAttributes( baseMentionData, data );
 }
 
-// Creates mention element from mention data.
+// Creates a mention element from the mention data.
 //
 // @param {Object} mention
 // @param {module:engine/view/downcastwriter~DowncastWriter} viewWriter
@@ -121,11 +121,11 @@ function createViewMentionElement( mention, viewWriter ) {
 	return viewWriter.createAttributeElement( 'span', attributes, options );
 }
 
-// Model post-fixer that disallows typing with selection when selection is placed after the text node with mention attribute.
+// Model post-fixer that disallows typing with selection when the selection is placed after the text node with the mention attribute.
 //
 // @param {module:engine/model/writer~Writer} writer
 // @param {module:engine/model/document~Document} doc
-// @returns {Boolean} Returns true if selection was fixed.
+// @returns {Boolean} Returns `true` if the selection was fixed.
 function selectionMentionAttributePostFixer( writer, doc ) {
 	const selection = doc.selection;
 	const focus = selection.focus;
@@ -141,33 +141,33 @@ function selectionMentionAttributePostFixer( writer, doc ) {
 	}
 }
 
-// Model post-fixer that removes mention attribute from modified text node.
+// Model post-fixer that removes the mention attribute from the modified text node.
 //
 // @param {module:engine/model/writer~Writer} writer
 // @param {module:engine/model/document~Document} doc
-// @returns {Boolean} Returns true if selection was fixed.
+// @returns {Boolean} Returns `true` if the selection was fixed.
 function removePartialMentionPostFixer( writer, doc, schema ) {
 	const changes = doc.differ.getChanges();
 
 	let wasChanged = false;
 
 	for ( const change of changes ) {
-		// Check text node on current position;
+		// Checks the text node on the current position.
 		const position = change.position;
 
 		if ( change.name == '$text' ) {
 			const nodeAfterInsertedTextNode = position.textNode && position.textNode.nextSibling;
 
-			// Check textNode where the change occurred.
+			// Checks the text node where the change occurred.
 			wasChanged = checkAndFix( position.textNode, writer ) || wasChanged;
 
-			// Occurs on paste occurs inside a text node with mention.
+			// Occurs on paste inside a text node with mention.
 			wasChanged = checkAndFix( nodeAfterInsertedTextNode, writer ) || wasChanged;
 			wasChanged = checkAndFix( position.nodeBefore, writer ) || wasChanged;
 			wasChanged = checkAndFix( position.nodeAfter, writer ) || wasChanged;
 		}
 
-		// Check text nodes in inserted elements (might occur when splitting paragraph or pasting content inside text with mention).
+		// Checks text nodes in inserted elements (might occur when splitting a paragraph or pasting content inside text with mention).
 		if ( change.name != '$text' && change.type == 'insert' ) {
 			const insertedNode = position.nodeAfter;
 
@@ -188,11 +188,12 @@ function removePartialMentionPostFixer( writer, doc, schema ) {
 	return wasChanged;
 }
 
-// This post-fixer will extend attribute applied on part of a mention so a whole text node of a mention will have added attribute.
+// This post-fixer will extend the attribute applied on the part of the mention so the whole text node of the mention will have
+// the added attribute.
 //
 // @param {module:engine/model/writer~Writer} writer
 // @param {module:engine/model/document~Document} doc
-// @returns {Boolean} Returns true if selection was fixed.
+// @returns {Boolean} Returns `true` if the selection was fixed.
 function extendAttributeOnMentionPostFixer( writer, doc ) {
 	const changes = doc.differ.getChanges();
 
@@ -200,9 +201,9 @@ function extendAttributeOnMentionPostFixer( writer, doc ) {
 
 	for ( const change of changes ) {
 		if ( change.type === 'attribute' && change.attributeKey != 'mention' ) {
-			// Check node at the left side of a range...
+			// Checks the node on the left side of the range...
 			const nodeBefore = change.range.start.nodeBefore;
-			// ... and on right side of range.
+			// ... and on the right side of the range.
 			const nodeAfter = change.range.end.nodeAfter;
 
 			for ( const node of [ nodeBefore, nodeAfter ] ) {
@@ -218,10 +219,10 @@ function extendAttributeOnMentionPostFixer( writer, doc ) {
 	return wasChanged;
 }
 
-// Checks if node has correct mention attribute if present.
-// Returns true if node is text and has a mention attribute which text does not match expected mention text.
+// Checks if a node has a correct mention attribute if present.
+// Returns `true` if the node is text and has a mention attribute whose text does not match the expected mention text.
 //
-// @param {module:engine/model/node~Node} node a node to check
+// @param {module:engine/model/node~Node} node The node to check.
 // @returns {Boolean}
 function isBrokenMentionNode( node ) {
 	if ( !node || !( node.is( 'text' ) || node.is( 'textProxy' ) ) || !node.hasAttribute( 'mention' ) ) {
@@ -236,7 +237,7 @@ function isBrokenMentionNode( node ) {
 	return text != expectedText;
 }
 
-// Fixes mention on text node it needs a fix.
+// Fixes a mention on a text node if it needs a fix.
 //
 // @param {module:engine/model/text~Text} textNode
 // @param {module:engine/model/writer~Writer} writer

+ 38 - 28
packages/ckeditor5-mention/src/mentionui.js

@@ -44,7 +44,7 @@ export default class MentionUI extends Plugin {
 		super( editor );
 
 		/**
-		 * The mentions view.
+		 * The mention view.
 		 *
 		 * @type {module:mention/ui/mentionsview~MentionsView}
 		 * @private
@@ -52,7 +52,7 @@ export default class MentionUI extends Plugin {
 		this._mentionsView = this._createMentionView();
 
 		/**
-		 * Stores mentions feeds configurations.
+		 * Stores mention feeds configurations.
 		 *
 		 * @type {Map<String, Object>}
 		 * @private
@@ -80,7 +80,7 @@ export default class MentionUI extends Plugin {
 		editor.editing.view.document.on( 'keydown', ( evt, data ) => {
 			if ( isHandledKey( data.keyCode ) && this._isUIVisible ) {
 				data.preventDefault();
-				evt.stop(); // Required for enter overriding.
+				evt.stop(); // Required for Enter key overriding.
 
 				if ( data.keyCode == keyCodes.arrowdown ) {
 					this._mentionsView.selectNext();
@@ -98,9 +98,9 @@ export default class MentionUI extends Plugin {
 					this._hideUIAndRemoveMarker();
 				}
 			}
-		}, { priority: 'highest' } ); // Required to override enter.
+		}, { priority: 'highest' } ); // Required to override the Enter key.
 
-		// Close the #panelView upon clicking outside of the plugin UI.
+		// Close the dropdown upon clicking outside of the plugin UI.
 		clickOutsideHandler( {
 			emitter: this._mentionsView,
 			activator: () => this._isUIVisible,
@@ -127,7 +127,7 @@ export default class MentionUI extends Plugin {
 				 *
 				 * @error mentionconfig-incorrect-marker
 				 */
-				throw new CKEditorError( 'mentionconfig-incorrect-marker: The marker must be provided and be a single character.' );
+				throw new CKEditorError( 'mentionconfig-incorrect-marker: The marker must be provided and it must be a single character.' );
 			}
 
 			const minimumCharacters = mentionDescription.minimumCharacters || 0;
@@ -158,6 +158,14 @@ export default class MentionUI extends Plugin {
 		return [ ContextualBalloon ];
 	}
 
+	/**
+	 * Returns true when {@link #_mentionsView} is in the {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon} and it is
+	 * currently visible.
+	 *
+	 * @readonly
+	 * @protected
+	 * @type {Boolean}
+	 */
 	get _isUIVisible() {
 		return this._balloon.visibleView === this._mentionsView;
 	}
@@ -233,7 +241,7 @@ export default class MentionUI extends Plugin {
 	}
 
 	/**
-	 * Returns item renderer for marker.
+	 * Returns item renderer for the marker.
 	 *
 	 * @private
 	 * @param {String} marker
@@ -246,7 +254,7 @@ export default class MentionUI extends Plugin {
 	}
 
 	/**
-	 * Returns a promise that resolves with autocomplete items for given text.
+	 * Returns a promise that resolves with autocomplete items for a given text.
 	 *
 	 * @param {String} marker
 	 * @param {String} feedText
@@ -278,7 +286,7 @@ export default class MentionUI extends Plugin {
 	}
 
 	/**
-	 * Registers a text watcher for marker.
+	 * Registers a text watcher for the marker.
 	 *
 	 * @private
 	 * @param {String} marker
@@ -312,7 +320,7 @@ export default class MentionUI extends Plugin {
 
 			const matchedTextLength = marker.length + feedText.length;
 
-			// create marker range
+			// Create a marker range.
 			const start = focus.getShiftedBy( -matchedTextLength );
 			const end = focus.getShiftedBy( -feedText.length );
 
@@ -367,7 +375,7 @@ export default class MentionUI extends Plugin {
 	}
 
 	/**
-	 * Returns registered text watcher for marker.
+	 * Returns the registered text watcher for the marker.
 	 *
 	 * @private
 	 * @param {String} marker
@@ -380,7 +388,7 @@ export default class MentionUI extends Plugin {
 	}
 
 	/**
-	 * Shows the mentions balloon.
+	 * Shows the mentions balloon. If the panel is already visible, it will reposition it.
 	 *
 	 * @private
 	 */
@@ -402,7 +410,7 @@ export default class MentionUI extends Plugin {
 	}
 
 	/**
-	 * Hides the mentions balloon and remove 'mention' marker from markers collection.
+	 * Hides the mentions balloon and removes the 'mention' marker from the markers collection.
 	 *
 	 * @private
 	 */
@@ -415,6 +423,8 @@ export default class MentionUI extends Plugin {
 			this._balloon.remove( this._mentionsView );
 		}
 
+		// Make the last matched position on panel view undefined so the #_getBalloonPanelPositionData() method will return all positions
+		// on the next call.
 		this._mentionsView.position = undefined;
 	}
 
@@ -457,10 +467,10 @@ export default class MentionUI extends Plugin {
 	}
 
 	/**
-	 * Creates position options object used to position the balloon panel.
+	 * Creates a position options object used to position the balloon panel.
 	 *
 	 * @param {module:engine/model/markercollection~Marker} mentionMarker
-	 * @param {String|undefined} preferredPosition Name of last matched position name.
+	 * @param {String|undefined} preferredPosition The name of the last matched position name.
 	 * @returns {module:utils/dom/position~Options}
 	 * @private
 	 */
@@ -493,13 +503,13 @@ export default class MentionUI extends Plugin {
 	}
 }
 
-// Returns balloon positions data callbacks.
+// Returns the balloon positions data callbacks.
 //
 // @param {String} preferredPosition
 // @returns {Array.<module:utils/dom/position~Position>}
 function getBalloonPanelPositions( preferredPosition ) {
 	const positions = {
-		// Positions panel to the south of caret rect.
+		// Positions the panel to the southeast of the caret rectangle.
 		'caret_se': targetRect => {
 			return {
 				top: targetRect.bottom + VERTICAL_SPACING,
@@ -508,7 +518,7 @@ function getBalloonPanelPositions( preferredPosition ) {
 			};
 		},
 
-		// Positions panel to the north of caret rect.
+		// Positions the panel to the northeast of the caret rectangle.
 		'caret_ne': ( targetRect, balloonRect ) => {
 			return {
 				top: targetRect.top - balloonRect.height - VERTICAL_SPACING,
@@ -517,7 +527,7 @@ function getBalloonPanelPositions( preferredPosition ) {
 			};
 		},
 
-		// Positions panel to the south of caret rect.
+		// Positions the panel to the southwest of the caret rectangle.
 		'caret_sw': ( targetRect, balloonRect ) => {
 			return {
 				top: targetRect.bottom + VERTICAL_SPACING,
@@ -526,7 +536,7 @@ function getBalloonPanelPositions( preferredPosition ) {
 			};
 		},
 
-		// Positions panel to the north of caret rect.
+		// Positions the panel to the northwest of the caret rect.
 		'caret_nw': ( targetRect, balloonRect ) => {
 			return {
 				top: targetRect.top - balloonRect.height - VERTICAL_SPACING,
@@ -536,14 +546,14 @@ function getBalloonPanelPositions( preferredPosition ) {
 		}
 	};
 
-	// Return only last position if it was matched to prevent panel from jumping after first match.
+	// Returns only the last position if it was matched to prevent the panel from jumping after the first match.
 	if ( positions.hasOwnProperty( preferredPosition ) ) {
 		return [
 			positions[ preferredPosition ]
 		];
 	}
 
-	// As default return all positions callbacks.
+	// By default return all position callbacks.
 	return [
 		positions.caret_se,
 		positions.caret_sw,
@@ -552,7 +562,7 @@ function getBalloonPanelPositions( preferredPosition ) {
 	];
 }
 
-// Creates a regex pattern for marker.
+// Creates a regex pattern for the marker.
 //
 // @param {String} marker
 // @param {Number} minimumCharacters
@@ -563,7 +573,7 @@ function createPattern( marker, minimumCharacters ) {
 	return `(^| )(\\${ marker })([_a-zA-Z0-9À-ž]${ numberOfCharacters }?)$`;
 }
 
-// Creates a test callback for marker to be used in text watcher instance.
+// Creates a test callback for the marker to be used in the text watcher instance.
 //
 // @param {String} marker
 // @param {Number} minimumCharacters
@@ -574,7 +584,7 @@ function createTestCallback( marker, minimumCharacters ) {
 	return text => regExp.test( text );
 }
 
-// Creates a text watcher matcher for marker.
+// Creates a text matcher from the marker.
 //
 // @param {String} marker
 // @returns {Function}
@@ -591,11 +601,11 @@ function createTextMatcher( marker ) {
 	};
 }
 
-// Default feed callback.
+// The default feed callback.
 function createFeedCallback( feedItems ) {
 	return feedText => {
 		const filteredItems = feedItems
-		// Make default mention feed case-insensitive.
+		// Make the default mention feed case-insensitive.
 			.filter( item => {
 				// Item might be defined as object.
 				const itemId = typeof item == 'string' ? item : String( item.id );
@@ -610,7 +620,7 @@ function createFeedCallback( feedItems ) {
 	};
 }
 
-// Checks if given key code is handled by the mention ui.
+// Checks if a given key code is handled by the mention UI.
 //
 // @param {Number}
 // @returns {Boolean}

+ 13 - 13
packages/ckeditor5-mention/src/textwatcher.js

@@ -11,10 +11,10 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix';
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
 
 /**
- * Text watcher feature.
+ * The text watcher feature.
  *
- * Fires {@link module:mention/textwatcher~TextWatcher#event:matched matched} and
- * {@link module:mention/textwatcher~TextWatcher#event:unmatched unmatched} events on typing or selection changes.
+ * Fires {@link module:mention/textwatcher~TextWatcher#event:matched `matched`} and
+ * {@link module:mention/textwatcher~TextWatcher#event:unmatched `unmatched`} events on typing or selection changes.
  *
  * @private
  */
@@ -22,8 +22,8 @@ export default class TextWatcher {
 	/**
 	 * Creates a text watcher instance.
 	 * @param {module:core/editor/editor~Editor} editor
-	 * @param {Function} testCallback Function used to match the text.
-	 * @param {Function} textMatcherCallback Function used to process matched text.
+	 * @param {Function} testCallback The function used to match the text.
+	 * @param {Function} textMatcherCallback The function used to process matched text.
 	 */
 	constructor( editor, testCallback, textMatcherCallback ) {
 		this.editor = editor;
@@ -36,7 +36,7 @@ export default class TextWatcher {
 	}
 
 	/**
-	 * Last matched text.
+	 * The last matched text.
 	 *
 	 * @property {String}
 	 */
@@ -45,7 +45,7 @@ export default class TextWatcher {
 	}
 
 	/**
-	 * Starts listening the editor for typing & selection events.
+	 * Starts listening to the editor for typing and selection events.
 	 *
 	 * @private
 	 */
@@ -53,7 +53,7 @@ export default class TextWatcher {
 		const editor = this.editor;
 
 		editor.model.document.selection.on( 'change:range', ( evt, { directChange } ) => {
-			// The indirect changes (ie when user types or external changes are applied) are handled in document's change event.
+			// Indirect changes (i.e. when the user types or external changes are applied) are handled in the document's change event.
 			if ( !directChange ) {
 				return;
 			}
@@ -85,7 +85,7 @@ export default class TextWatcher {
 
 		if ( !textHasMatch && this.hasMatch ) {
 			/**
-			 * Fired whenever text doesn't match anymore. Fired only when text matcher was matched.
+			 * Fired whenever the text does not match anymore. Fired only when the text watcher found a match.
 			 *
 			 * @event unmatched
 			 */
@@ -98,7 +98,7 @@ export default class TextWatcher {
 			const matched = this.textMatcher( text );
 
 			/**
-			 * Fired whenever text matcher was matched.
+			 * Fired whenever the text watcher found a match.
 			 *
 			 * @event matched
 			 */
@@ -109,14 +109,14 @@ export default class TextWatcher {
 	/**
 	 * Returns the text before the caret from the current selection block.
 	 *
-	 * @returns {String|undefined} Text from block or undefined if selection is not collapsed.
+	 * @returns {String|undefined} The text from the block or undefined if the selection is not collapsed.
 	 * @private
 	 */
 	_getText() {
 		const editor = this.editor;
 		const selection = editor.model.document.selection;
 
-		// Do nothing if selection is not collapsed.
+		// Do nothing if the selection is not collapsed.
 		if ( !selection.isCollapsed ) {
 			return;
 		}
@@ -128,7 +128,7 @@ export default class TextWatcher {
 }
 
 /**
- * Returns whole text from given range by adding all data from text nodes together.
+ * Returns the whole text from a given range by adding all data from the text nodes together.
  *
  * @protected
  * @param {module:engine/model/range~Range} range