浏览代码

Merge branch 'master' into t/38

Aleksander Nowodzinski 6 年之前
父节点
当前提交
bf4b78a49e

+ 1 - 3
packages/ckeditor5-mention/.travis.yml

@@ -28,6 +28,4 @@ after_script:
 - ckeditor5-dev-tests-notify-travis-status
 env:
   global:
-  - secure: ZQA0E73V4IrxrhGCqVMnBDZR8gP0P8MlCgdOy2qUtSB2yR+syefZBcBVoa6YYTle8UmePO4ETta4y0FMH0zZdnSETEqQ5odKGdclhkFU8I4O9+7ZXGQEfQPVriZxXHjdCX66gRShphW+6tcg5M+jGs1TlBtmgNuMn4MU7Z16lLAouO4gaicOFfDQO/FF2yO5YJURY47ThO/IVfUJB5n1a5rAkRN9WmbAubkYqtC53wOg/Ipl7I6bquxaUAcPLd+2kT1T6g+SWISoOhfOTYwwaN9Tqapzdx3wwM47DgDmjZ3tDRuhIBPLTWx7Fk70VF+8wnRi0ydhXaZYoRh6dBbjZPO9IwOArMmV6SRANWt5KAmxW3glFuvjDih/ypR1r1YzDf2kKvLijPZCQpQKzeWPZ6X+kaVGeIe8mdRz0MCh2TBvCXVRBfmh9BNFmnLhg68ympJK1zBczFtTVdoNiTJ8eV4AsKb+NED6ZRdD0Ol7ydD4LOcl2LsIpWaYj04tXuvIe+hN7AjTGiOBFqEY0VGHSu9WyGziBCHR9yboDmy8il/ChBBgWOFYJXZNGnw+VHhFrkxAXDUYbLreVIZLPUMRscB31y982R0oiU0pVsGJTSP1v6i0JMAjNJ3ZsL7V4IoFwnqk2NbKj28JxTq2BMBx5g3ry9EW9WQ1fgzxJuWOo2I=
-  - secure: dOp+yn/un5AyUPO7Zv7EY5LlEH59ASFb27DkyoIjKytcERp6iCMuHqGgbatsR1tOzSs8GF1Gqzh3K061tRjLWnIwq5iyEHlswy6o0SchHtQtsIk5Jfa/6PGwuYqsKGER9avxl4MZ4Nctdl0ljo5nZCifuRcXRB5aJFVMLDyG3KscYq38MN6RLo/YdNZ53NwaeBUwUHC6EuDEyOc7F80tCzjibpKUUonCM9P0h07BCNn8xT9vdQHRZCDlsbqHuOvIsPD//WY8q9GPzp+jQ2LrwQ1xCy2JhMAhFJ0zk1+h5O1D52nRf8G/PlqtIEWVyhr5XV6iYGocJM2jRYSvEKV0EB1kGk3rv/AVlKj2CxUrUUY/OV0JQUWo7YXPua6ivNVvEyXMsywCtHixuYUvSISVz0MgA6Y4LOcxcj7P9buwi67PsMS909iGCpyW5L+cBBvj5hdp12MZ71lhlCBwuGWNQarg9MYpI8ZFqMYBWHnCrkGC/OUQi6yg0x6cd7opydFyI94NmY673s1RddJ48Zfl35pJpqG5hD2lWPaMcnIayZ5uhrnqC+3No31g0Kb70AzoiEM09Rmn6S9vTwHJ9hChCwAyDCR2ZVQok4ypNRzUJdZjFfaL9jM6jVb8HBOrkDsMIv7GdjQPMQ45OFWj0U1ty1VxB6dBEv079WG88tQLYws=
-  - secure: II9fBZm1I7WDUkmN4402a4Fn5i44+W7DXczw49S68tAE6fqj1o7zKakWaFMbn+5ShAXmH/WVkn7bRMt2rBD9PT12osPzWXzo1+kCXnEi8u1auFGYz/Em6Rs6MOt0MWr0YPQEYyALBGVocwErJ6fw721aXUsMomoH0PsG005FOQGkqSrQL7QWjT9fIjsQhYx7RXDBQVeKX7kagqsuy9QPUSfcTHVPP9wiG+Q0oFBRXr5MZYguJ6zXB5sSFJ7uzAWL9OfrUmhH3n7jzRQq5lilTKLrLzwsxmFqQ9sU2+CGhzh9hoAVkLs1QIF8ufwV5DIQRPSQkVLVHnXAZNzrzIaMTiQ6mxpOoSpYVKXaHbKBF/dGj1IYIsLce8n3nwHATt4gNPRQUoZGlLpdMjDo+LiajDud8CaduLtZdiOB2x3ssCvPwInLRR2e80mIyAeGHew+CQ8R780my/+HaS/9Q2Vig/IfbV006dEi/AcTCX9V+JTVd9hPgP3bWUrhaOFxn/BuWgweZFnxtvEj9liczQXlCFVLDGOzjx1I/Ruy0u2IbI+0R8St9QJa6VJU8dWyxaIxvu1X/SRWop5iXEAEL3nTmBkZrdIweaT9p8ZvgCsXo7Hg+AUkx42fzXnWinXMCNWxuDhMQoXD0jf8UfgV0y4yy9Ky2pi8cconBhGV4ZKFXVQ=
+  - secure: xofGZGZnE+4xonNp2gGSpQCIwH3aZu4/EmR6YAfcJtN/STDaKeSnSlnLpQJExSAgQM/nxF/Jh/DO8AL0vjMfQut0CtIOIQaR/2g16/ZK983t0FMBSHdaFKWtfkHzDVlEJ0Mq+42x/FEHlBccuVD80WZ3uYk+vxqiQNUk5Ywluz8NJ2tYWJ8zpGsLJjzY6G0j1s4WXtAKAOmm2Fli6cNryBSGxoEJSQg8sEoFf5dy32W6jykmVafGWzvOi9uVN8nSZkAyqTu27YF8DA0mivZP/UmrLtTzvj3FAV53l06IvRQgQOMKvg4brXouR+HXGSPS72ohnzhvyWmHzesbYCnDmnsEqxIN6JKmWyFdcQdJkE7AAF0LCqxSQSR78Vc8hiD+1AD368xY345rOPBqVkWskCFAuyuu7+ri3ahBqseH7Elcdol/RXIDsaeakaYwt6/EOMk96rH4DWZw6K2l7ZP5kKfV/F11ZttY8Lltj0d/NuicCqNImwzV85/qYNpKxrB9g6ZcagutYpS61sSiT3pZp41hpuNdZLHuUiqb8Gz6dbdbIJje/J3J+4kn0+AWIWDrDUXve5nm6p24u6Y456dzIDzNn+H5M55tcXn+7U7wFBgbzJ+dtWxzH05JBrZcP4SCNMYdoAKczNqqXdSq8mPaU2+Jfs9tWNOb4OQG0R3aD8g=

+ 19 - 0
packages/ckeditor5-mention/CHANGELOG.md

@@ -1,6 +1,25 @@
 Changelog
 =========
 
+## [11.0.0](https://github.com/ckeditor/ckeditor5-mention/compare/v10.0.0...v11.0.0) (2019-06-05)
+
+### Bug fixes
+
+* A mention can now be preceded by characters such as brackets, quotes, soft break, etc. Closes [#44](https://github.com/ckeditor/ckeditor5-mention/issues/44). ([86262d1](https://github.com/ckeditor/ckeditor5-mention/commit/86262d1))
+* The mention plugin should not throw errors when another `ContextualBalloon` is already visible. Closes [#67](https://github.com/ckeditor/ckeditor5-mention/issues/67). ([de9ee71](https://github.com/ckeditor/ckeditor5-mention/commit/de9ee71))
+* The mention panel should have precedence over all other panels. Closes [#74](https://github.com/ckeditor/ckeditor5-mention/issues/74). ([3e8a84c](https://github.com/ckeditor/ckeditor5-mention/commit/3e8a84c))
+* The Mention UI will use `ContextualBalloon` plugin to display to prevent balloon collisions with other features. Closes [#27](https://github.com/ckeditor/ckeditor5-mention/issues/27). ([9ae7f30](https://github.com/ckeditor/ckeditor5-mention/commit/9ae7f30))
+
+### Other changes
+
+* Remove unknown stack option from `ContextualBalloon#add()` method call. ([b6a50cf](https://github.com/ckeditor/ckeditor5-mention/commit/b6a50cf))
+* Use `Model#insertContent()` instead of `model.Writer#insertText()`. Closes [#69](https://github.com/ckeditor/ckeditor5-mention/issues/69). ([ee973bb](https://github.com/ckeditor/ckeditor5-mention/commit/ee973bb))
+
+### BREAKING CHANGES
+
+* The `MentionUI#panelView` property is removed. The mention feature now uses the `ContextualBalloon` plugin.
+
+
 ## [10.0.0](https://github.com/ckeditor/ckeditor5-mention/tree/v10.0.0) (2019-04-10)
 
 The initial release.

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

@@ -4,7 +4,6 @@ CKEditor 5 mention feature
 [![Join the chat at https://gitter.im/ckeditor/ckeditor5](https://badges.gitter.im/ckeditor/ckeditor5.svg)](https://gitter.im/ckeditor/ckeditor5?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
 [![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-mention.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-mention)
 [![Build Status](https://travis-ci.org/ckeditor/ckeditor5-mention.svg?branch=master)](https://travis-ci.org/ckeditor/ckeditor5-mention)
-[![BrowserStack Status](https://automate.browserstack.com/automate/badge.svg?badge_key=d3hvenZqQVZERFQ5d09FWXdyT0ozVXhLaVltRFRjTTUyZGpvQWNmWVhUUT0tLUZqNlJ1YWRUd0RvdEVOaEptM1B2Q0E9PQ==--c9d3dee40b9b4471ff3fb516d9ecf8d09292c7e0)](https://automate.browserstack.com/public-build/d3hvenZqQVZERFQ5d09FWXdyT0ozVXhLaVltRFRjTTUyZGpvQWNmWVhUUT0tLUZqNlJ1YWRUd0RvdEVOaEptM1B2Q0E9PQ==--c9d3dee40b9b4471ff3fb516d9ecf8d09292c7e0)
 [![Coverage Status](https://coveralls.io/repos/github/ckeditor/ckeditor5-mention/badge.svg?branch=master)](https://coveralls.io/github/ckeditor/ckeditor5-mention?branch=master)
 <br>
 [![Dependency Status](https://david-dm.org/ckeditor/ckeditor5-mention/status.svg)](https://david-dm.org/ckeditor/ckeditor5-mention)
@@ -22,4 +21,4 @@ See the [`@ckeditor/ckeditor5-mention` package](https://ckeditor.com/docs/ckedit
 
 ## License
 
-Licensed under the terms of [GNU General Public License Version 2 or later](http://www.gnu.org/licenses/gpl.html). For full details about the license, please check the `LICENSE.md` file.
+Licensed under the terms of [GNU General Public License Version 2 or later](http://www.gnu.org/licenses/gpl.html). For full details about the license, please check the `LICENSE.md` file or [https://ckeditor.com/legal/ckeditor-oss-license](https://ckeditor.com/legal/ckeditor-oss-license).

+ 3 - 1
packages/ckeditor5-mention/docs/features/mentions.md

@@ -39,7 +39,7 @@ ClassicEditor
 					feed: [ '@Barney', '@Lily', '@Marshall', '@Robin', '@Ted' ],
 					minimumCharacters: 1
 				}
-			}
+			]
 		}
 	} )
 	.then( ... )
@@ -189,6 +189,8 @@ To a link:
 
 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}).
 
+**Note:** The feature prevents copying fragments of existing mentions. If only a part of a mention is selected, it will be copied as plain text. The internal converter with the {@link module:engine/conversion/conversion~ConverterDefinition#converterPriority `'highest'` priority} controls this behaviour; thus, we do not recommend adding mention converters with the `'highest'` priority to avoid collisions and quirky results. 
+
 ```js
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {

+ 16 - 13
packages/ckeditor5-mention/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@ckeditor/ckeditor5-mention",
-  "version": "10.0.0",
+  "version": "11.0.0",
   "description": "Mention feature for CKEditor 5.",
   "keywords": [
     "ckeditor",
@@ -10,20 +10,23 @@
     "ckeditor5-plugin"
   ],
   "dependencies": {
-    "@ckeditor/ckeditor5-core": "^12.1.0",
-    "@ckeditor/ckeditor5-ui": "^12.1.0",
-    "@ckeditor/ckeditor5-utils": "^12.1.0"
+    "@ckeditor/ckeditor5-core": "^12.1.1",
+    "@ckeditor/ckeditor5-ui": "^13.0.0",
+    "@ckeditor/ckeditor5-utils": "^12.1.1"
   },
   "devDependencies": {
-    "@ckeditor/ckeditor5-basic-styles": "^11.1.0",
-    "@ckeditor/ckeditor5-block-quote": "^11.0.1",
-    "@ckeditor/ckeditor5-clipboard": "^11.0.1",
-    "@ckeditor/ckeditor5-editor-classic": "^12.1.0",
-    "@ckeditor/ckeditor5-engine": "^13.1.0",
-    "@ckeditor/ckeditor5-font": "^11.1.0",
-    "@ckeditor/ckeditor5-paragraph": "^11.0.1",
-    "@ckeditor/ckeditor5-undo": "^11.0.1",
-    "@ckeditor/ckeditor5-widget": "^11.0.1",
+    "@ckeditor/ckeditor5-basic-styles": "^11.1.1",
+    "@ckeditor/ckeditor5-block-quote": "^11.1.0",
+    "@ckeditor/ckeditor5-clipboard": "^11.0.2",
+    "@ckeditor/ckeditor5-editor-classic": "^12.1.1",
+    "@ckeditor/ckeditor5-engine": "^13.1.1",
+    "@ckeditor/ckeditor5-font": "^11.1.1",
+    "@ckeditor/ckeditor5-link": "^11.0.2",
+    "@ckeditor/ckeditor5-paragraph": "^11.0.2",
+    "@ckeditor/ckeditor5-table": "^13.0.0",
+    "@ckeditor/ckeditor5-typing": "^12.0.2",
+    "@ckeditor/ckeditor5-undo": "^11.0.2",
+    "@ckeditor/ckeditor5-widget": "^11.0.2",
     "eslint": "^5.5.0",
     "eslint-config-ckeditor5": "^1.0.11",
     "husky": "^1.3.1",

+ 28 - 0
packages/ckeditor5-mention/src/mentionediting.js

@@ -40,6 +40,7 @@ export default class MentionEditing extends Plugin {
 		// Allow the mention attribute on all text nodes.
 		model.schema.extend( '$text', { allowAttributes: 'mention' } );
 
+		// Upcast conversion.
 		editor.conversion.for( 'upcast' ).elementToAttribute( {
 			view: {
 				name: 'span',
@@ -52,10 +53,12 @@ export default class MentionEditing extends Plugin {
 			}
 		} );
 
+		// Downcast conversion.
 		editor.conversion.for( 'downcast' ).attributeToElement( {
 			model: 'mention',
 			view: createViewMentionElement
 		} );
+		editor.conversion.for( 'downcast' ).add( preventPartialMentionDowncast );
 
 		doc.registerPostFixer( writer => removePartialMentionPostFixer( writer, doc, model.schema ) );
 		doc.registerPostFixer( writer => extendAttributeOnMentionPostFixer( writer, doc ) );
@@ -98,6 +101,31 @@ export function _toMentionAttribute( viewElementOrMention, data ) {
 	return _addMentionAttributes( baseMentionData, data );
 }
 
+// A converter that blocks partial mention from being converted.
+//
+// This converter is registered with 'highest' priority in order to consume mention attribute before it is converted by
+// any other converters. This converter only consumes partial mention - those whose `_text` attribute is not equal to text with mention
+// attribute. This may happen when copying part of mention text.
+//
+// @param {module:engine/conversion/dwoncastdispatcher~DowncastDispatcher}
+function preventPartialMentionDowncast( dispatcher ) {
+	dispatcher.on( 'attribute:mention', ( evt, data, conversionApi ) => {
+		const mention = data.attributeNewValue;
+
+		if ( !data.item.is( 'textProxy' ) || !mention ) {
+			return;
+		}
+
+		const start = data.range.start;
+		const textNode = start.textNode || start.nodeAfter;
+
+		if ( textNode.data != mention._text ) {
+			// Consume item to prevent partial mention conversion.
+			conversionApi.consumable.consume( data.item, evt.name );
+		}
+	}, { priority: 'highest' } );
+}
+
 // Creates a mention element from the mention data.
 //
 // @param {Object} mention

+ 18 - 6
packages/ckeditor5-mention/src/mentionui.js

@@ -296,6 +296,8 @@ export default class MentionUI extends Plugin {
 			const nodeBefore = focus.nodeBefore;
 
 			if ( hasMention || nodeBefore && nodeBefore.is( 'text' ) && nodeBefore.hasAttribute( 'mention' ) ) {
+				this._hideUIAndRemoveMarker();
+
 				return;
 			}
 
@@ -372,7 +374,8 @@ export default class MentionUI extends Plugin {
 			this._balloon.add( {
 				view: this._mentionsView,
 				position: this._getBalloonPanelPositionData( markerMarker, this._mentionsView.position ),
-				withArrow: false
+				withArrow: false,
+				singleViewMode: true
 			} );
 		}
 
@@ -387,12 +390,13 @@ export default class MentionUI extends Plugin {
 	 * @private
 	 */
 	_hideUIAndRemoveMarker() {
-		if ( this.editor.model.markers.has( 'mention' ) ) {
-			this.editor.model.change( writer => writer.removeMarker( 'mention' ) );
+		// Remove the mention view from balloon before removing marker - it is used by balloon position target().
+		if ( this._balloon.hasView( this._mentionsView ) ) {
+			this._balloon.remove( this._mentionsView );
 		}
 
-		if ( this._isUIVisible ) {
-			this._balloon.remove( this._mentionsView );
+		if ( this.editor.model.markers.has( 'mention' ) ) {
+			this.editor.model.change( writer => writer.removeMarker( 'mention' ) );
 		}
 
 		// Make the last matched position on panel view undefined so the #_getBalloonPanelPositionData() method will return all positions
@@ -447,14 +451,22 @@ export default class MentionUI extends Plugin {
 	 * @private
 	 */
 	_getBalloonPanelPositionData( mentionMarker, preferredPosition ) {
+		const editor = this.editor;
 		const editing = this.editor.editing;
 		const domConverter = editing.view.domConverter;
 		const mapper = editing.mapper;
 
 		return {
 			target: () => {
-				const viewRange = mapper.toViewRange( mentionMarker.getRange() );
+				let modelRange = mentionMarker.getRange();
+
+				// Target the UI to the model selection range - the marker has been removed so probably the UI will not be shown anyway.
+				// The logic is used by ContextualBalloon to display another panel in the same place.
+				if ( modelRange.start.root.rootName == '$graveyard' ) {
+					modelRange = editor.model.document.selection.getFirstRange();
+				}
 
+				const viewRange = mapper.toViewRange( modelRange );
 				const rangeRects = Rect.getDomRangeRects( domConverter.viewRangeToDom( viewRange ) );
 
 				return rangeRects.pop();

+ 119 - 1
packages/ckeditor5-mention/tests/mention-integration.js

@@ -3,18 +3,25 @@
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
-/* global document */
+/* global document, setTimeout */
 
 import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote';
 import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import Table from '@ckeditor/ckeditor5-table/src/table';
+import TableToolbar from '@ckeditor/ckeditor5-table/src/tabletoolbar';
 import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting';
+import Link from '@ckeditor/ckeditor5-link/src/link';
+import Delete from '@ckeditor/ckeditor5-typing/src/delete';
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import { parse as parseView, getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
+import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 import MentionEditing from '../src/mentionediting';
+import Mention from '../src/mention';
+import MentionUI from '../src/mentionui';
 
 describe( 'Mention feature - integration', () => {
 	let div, editor, model, doc;
@@ -208,4 +215,115 @@ describe( 'Mention feature - integration', () => {
 				.to.equal( expectedData );
 		} );
 	} );
+
+	describe( 'with table toolbar', () => {
+		beforeEach( () => {
+			return ClassicTestEditor
+				.create( div, {
+					plugins: [ Paragraph, Table, TableToolbar, Mention ],
+					table: {
+						contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ]
+					},
+					mention: {
+						feeds: [
+							{
+								marker: '@',
+								feed: [ '@Barney', '@Lily', '@Marshall', '@Robin', '@Ted' ]
+							}
+						]
+					}
+				} )
+				.then( newEditor => {
+					editor = newEditor;
+					model = editor.model;
+					doc = model.document;
+				} );
+		} );
+
+		it( 'should work with table toolbar', () => {
+			editor.ui.focusTracker.isFocused = true;
+
+			setData( model, '<table><tableRow><tableCell><paragraph>foo []</paragraph></tableCell></tableRow></table>' );
+
+			const balloon = editor.plugins.get( 'ContextualBalloon' );
+			const panelView = balloon.view;
+			const mentionUI = editor.plugins.get( MentionUI );
+			const mentionsView = mentionUI._mentionsView;
+
+			return new Promise( resolve => setTimeout( resolve, 200 ) )
+				.then( () => {
+					model.change( writer => {
+						writer.insertText( '@', doc.selection.getFirstPosition() );
+					} );
+				} )
+				.then( () => new Promise( resolve => setTimeout( resolve, 200 ) ) )
+				.then( () => {
+					expect( panelView.isVisible ).to.be.true;
+					expect( balloon.visibleView === mentionsView ).to.be.true;
+
+					model.change( writer => {
+						writer.setSelection( doc.getRoot().getNodeByPath( [ 0, 0, 0, 0 ] ), '1' );
+					} );
+
+					expect( panelView.isVisible ).to.be.true;
+					expect( balloon.visibleView === mentionsView ).to.be.false;
+				} );
+		} );
+	} );
+
+	describe( 'with link toolbar', () => {
+		beforeEach( () => {
+			return ClassicTestEditor
+				.create( div, {
+					plugins: [ Paragraph, Link, Delete, Mention ],
+					mention: {
+						feeds: [
+							{
+								marker: '@',
+								feed: [ '@Barney', '@Lily', '@Marshall', '@Robin', '@Ted' ]
+							}
+						]
+					}
+				} )
+				.then( newEditor => {
+					editor = newEditor;
+					model = editor.model;
+					doc = model.document;
+				} );
+		} );
+
+		it( 'should work with link toolbar', () => {
+			editor.ui.focusTracker.isFocused = true;
+
+			setData( model, '<paragraph>[]</paragraph>' );
+
+			const balloon = editor.plugins.get( 'ContextualBalloon' );
+			const panelView = balloon.view;
+			const mentionUI = editor.plugins.get( MentionUI );
+			const mentionsView = mentionUI._mentionsView;
+
+			return new Promise( resolve => setTimeout( resolve, 200 ) )
+				.then( () => {
+					// Show link UI
+					editor.execute( 'link', '@' );
+					editor.editing.view.document.fire( 'click' );
+
+					expect( panelView.isVisible ).to.be.true;
+					expect( balloon.visibleView === mentionsView ).to.be.false; // LinkUI
+
+					model.change( writer => {
+						writer.setSelection( doc.getRoot().getChild( 0 ), 'end' );
+					} );
+				} )
+				.then( () => new Promise( resolve => setTimeout( resolve, 200 ) ) )
+				.then( () => {
+					expect( panelView.isVisible ).to.be.true;
+					expect( balloon.visibleView === mentionsView ).to.be.true;
+
+					editor.execute( 'delete' );
+
+					expect( panelView.isVisible ).to.be.false;
+				} );
+		} );
+	} );
 } );

+ 136 - 4
packages/ckeditor5-mention/tests/mentionediting.js

@@ -6,10 +6,11 @@
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
-import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
+import { stringify as stringifyView, getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
+import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 
-import MentionEditing from '../src/mentionediting';
+import MentionEditing, { _toMentionAttribute } from '../src/mentionediting';
 import MentionCommand from '../src/mentioncommand';
 
 describe( 'MentionEditing', () => {
@@ -84,6 +85,25 @@ describe( 'MentionEditing', () => {
 			expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal( expectedView );
 		} );
 
+		it( 'should be overridable', () => {
+			addCustomMentionConverters( editor );
+
+			editor.setData( '<p>Hello <b class="mention" data-mention="@Ted Mosby">Ted Mosby</b></p>' );
+
+			const textNode = doc.getRoot().getChild( 0 ).getChild( 1 );
+
+			expect( textNode ).to.not.be.null;
+			expect( textNode.hasAttribute( 'mention' ) ).to.be.true;
+			expect( textNode.getAttribute( 'mention' ) ).to.have.property( 'id', '@Ted Mosby' );
+			expect( textNode.getAttribute( 'mention' ) ).to.have.property( '_text', 'Ted Mosby' );
+			expect( textNode.getAttribute( 'mention' ) ).to.have.property( '_uid' );
+
+			const expectedView = '<p>Hello <b class="mention" data-mention="@Ted Mosby">Ted Mosby</b></p>';
+
+			expect( editor.getData() ).to.equal( expectedView );
+			expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal( expectedView );
+		} );
+
 		it( 'should convert consecutive mentions spans as two text nodes and two spans in the view', () => {
 			editor.setData(
 				'<p>' +
@@ -122,7 +142,7 @@ describe( 'MentionEditing', () => {
 			}
 		} );
 
-		it( 'should not convert partial mentions', () => {
+		it( 'should upcast partial mention', () => {
 			editor.setData( '<p><span class="mention" data-mention="@John">@Jo</span></p>' );
 
 			const textNode = doc.getRoot().getChild( 0 ).getChild( 0 );
@@ -139,6 +159,72 @@ describe( 'MentionEditing', () => {
 			expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal( expectedView );
 		} );
 
+		it( 'should not downcast partial mention (default converter)', done => {
+			editor.setData( '<p>Hello <span class="mention" data-mention="@John">@John</span></p>' );
+
+			model.change( writer => {
+				const start = writer.createPositionAt( doc.getRoot().getChild( 0 ), 0 );
+				const end = writer.createPositionAt( doc.getRoot().getChild( 0 ), 9 );
+				writer.setSelection( writer.createRange( start, end ) );
+			} );
+
+			const dataTransferMock = createDataTransfer();
+			const preventDefaultSpy = sinon.spy();
+
+			editor.editing.view.document.on( 'clipboardOutput', ( evt, data ) => {
+				expect( stringifyView( data.content ) ).to.equal( 'Hello @Jo' );
+
+				done();
+			} );
+
+			editor.editing.view.document.fire( 'copy', {
+				dataTransfer: dataTransferMock,
+				preventDefault: preventDefaultSpy
+			} );
+		} );
+
+		it( 'should not downcast partial mention (custom converter)', done => {
+			addCustomMentionConverters( editor );
+
+			editor.conversion.for( 'downcast' ).attributeToElement( {
+				model: 'mention',
+				view: ( modelAttributeValue, viewWriter ) => {
+					if ( !modelAttributeValue ) {
+						return;
+					}
+
+					return viewWriter.createAttributeElement( 'a', {
+						class: 'mention',
+						'data-mention': modelAttributeValue.id,
+						'href': modelAttributeValue.link
+					}, { id: modelAttributeValue._uid } );
+				},
+				converterPriority: 'high'
+			} );
+
+			editor.setData( '<p>Hello <b class="mention" data-mention="@Ted Mosby">Ted Mosby</b></p>' );
+
+			model.change( writer => {
+				const start = writer.createPositionAt( doc.getRoot().getChild( 0 ), 0 );
+				const end = writer.createPositionAt( doc.getRoot().getChild( 0 ), 9 );
+				writer.setSelection( writer.createRange( start, end ) );
+			} );
+
+			const dataTransferMock = createDataTransfer();
+			const preventDefaultSpy = sinon.spy();
+
+			editor.editing.view.document.on( 'clipboardOutput', ( evt, data ) => {
+				expect( stringifyView( data.content ) ).to.equal( 'Hello Ted' );
+
+				done();
+			} );
+
+			editor.editing.view.document.fire( 'copy', {
+				dataTransfer: dataTransferMock,
+				preventDefault: preventDefaultSpy
+			} );
+		} );
+
 		it( 'should not convert empty mentions', () => {
 			editor.setData( '<p>foo<span class="mention" data-mention="@John"></span></p>' );
 
@@ -526,8 +612,54 @@ describe( 'MentionEditing', () => {
 	function createTestEditor( mentionConfig ) {
 		return VirtualTestEditor
 			.create( {
-				plugins: [ Paragraph, MentionEditing ],
+				plugins: [ Paragraph, MentionEditing, Clipboard ],
 				mention: mentionConfig
 			} );
 	}
+
+	function createDataTransfer() {
+		const store = new Map();
+
+		return {
+			setData( type, data ) {
+				store.set( type, data );
+			},
+
+			getData( type ) {
+				return store.get( type );
+			}
+		};
+	}
 } );
+
+function addCustomMentionConverters( editor ) {
+	editor.conversion.for( 'upcast' ).elementToAttribute( {
+		view: {
+			name: 'b',
+			key: 'data-mention',
+			classes: 'mention'
+		},
+		model: {
+			key: 'mention',
+			value: viewItem => {
+				return _toMentionAttribute( viewItem );
+			}
+		},
+		converterPriority: 'high'
+	} );
+
+	editor.conversion.for( 'downcast' ).attributeToElement( {
+		model: 'mention',
+		view: ( modelAttributeValue, viewWriter ) => {
+			if ( !modelAttributeValue ) {
+				return;
+			}
+
+			return viewWriter.createAttributeElement( 'b', {
+				class: 'mention',
+				'data-mention': modelAttributeValue.id
+			}, { id: modelAttributeValue._uid } );
+		},
+		converterPriority: 'high'
+	} );
+}

+ 7 - 0
packages/ckeditor5-mention/tests/mentionui.js

@@ -96,10 +96,15 @@ describe( 'MentionUI', () => {
 	} );
 
 	describe( 'contextual balloon', () => {
+		let balloonAddSpy;
+
 		beforeEach( () => {
 			return createClassicTestEditor( staticConfig )
 				.then( () => {
 					setData( model, '<paragraph>foo []</paragraph>' );
+					const contextualBalloon = editor.plugins.get( ContextualBalloon );
+
+					balloonAddSpy = sinon.spy( contextualBalloon, 'add' );
 
 					model.change( writer => {
 						writer.insertText( '@', doc.selection.getFirstPosition() );
@@ -109,6 +114,8 @@ describe( 'MentionUI', () => {
 		} );
 
 		it( 'should disable arrow', () => {
+			sinon.assert.calledOnce( balloonAddSpy );
+			sinon.assert.calledWithExactly( balloonAddSpy, sinon.match( data => data.singleViewMode ) );
 			expect( panelView.isVisible ).to.be.true;
 			expect( panelView.withArrow ).to.be.false;
 		} );