Selaa lähdekoodia

Merge pull request #17 from ckeditor/t/12

Fix: Mention post-fixer now correctly handles partial mentions on insert, remove and paste. Closes #12. Closes #8.
Szymon Cofalik 6 vuotta sitten
vanhempi
commit
8196845e0e

+ 86 - 17
packages/ckeditor5-mention/src/mentionediting.js

@@ -57,7 +57,8 @@ export default class MentionEditing extends Plugin {
 			view: createViewMentionElement
 		} );
 
-		doc.registerPostFixer( writer => removePartialMentionPostFixer( writer, doc ) );
+		doc.registerPostFixer( writer => removePartialMentionPostFixer( writer, doc, model.schema ) );
+		doc.registerPostFixer( writer => extendAttributeOnMentionPostFixer( writer, doc ) );
 		doc.registerPostFixer( writer => selectionMentionAttributePostFixer( writer, doc ) );
 
 		editor.commands.add( 'mention', new MentionCommand( editor ) );
@@ -140,34 +141,69 @@ function selectionMentionAttributePostFixer( writer, doc ) {
 // @param {module:engine/model/writer~Writer} writer
 // @param {module:engine/model/document~Document} doc
 // @returns {Boolean} Returns true if selection was fixed.
-function removePartialMentionPostFixer( writer, doc ) {
+function removePartialMentionPostFixer( writer, doc, schema ) {
 	const changes = doc.differ.getChanges();
 
 	let wasChanged = false;
 
 	for ( const change of changes ) {
-		// Check if user edited part of a mention.
-		if ( change.type == 'insert' || change.type == 'remove' ) {
-			const textNode = change.position.textNode;
+		// Check text node on current position;
+		const position = change.position;
 
-			if ( change.name == '$text' && textNode && textNode.hasAttribute( 'mention' ) ) {
-				writer.removeAttribute( 'mention', textNode );
-				wasChanged = true;
+		if ( change.name == '$text' ) {
+			const nodeAfterInsertedTextNode = position.textNode && position.textNode.nextSibling;
+
+			// Check textNode where the change occurred.
+			wasChanged = checkAndFix( position.textNode, writer ) || wasChanged;
+
+			// Occurs on paste occurs 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).
+		if ( change.name != '$text' && change.type == 'insert' ) {
+			const insertedNode = position.nodeAfter;
+
+			for ( const item of writer.createRangeIn( insertedNode ).getItems() ) {
+				wasChanged = checkAndFix( item, writer ) || wasChanged;
 			}
 		}
 
-		// Additional check for deleting last character of a text node.
-		if ( change.type == 'remove' ) {
-			const nodeBefore = change.position.nodeBefore;
+		// Inserted inline elements might break mention.
+		if ( change.type == 'insert' && schema.isInline( change.name ) ) {
+			const nodeAfterInserted = position.nodeAfter && position.nodeAfter.nextSibling;
+
+			wasChanged = checkAndFix( position.nodeBefore, writer ) || wasChanged;
+			wasChanged = checkAndFix( nodeAfterInserted, writer ) || wasChanged;
+		}
+	}
+
+	return wasChanged;
+}
 
-			if ( nodeBefore && nodeBefore.hasAttribute( 'mention' ) ) {
-				const text = nodeBefore.data;
-				const mention = nodeBefore.getAttribute( 'mention' );
+// This post-fixer will extend attribute applied on part of a mention so a whole text node of a mention will have added attribute.
+//
+// @param {module:engine/model/writer~Writer} writer
+// @param {module:engine/model/document~Document} doc
+// @returns {Boolean} Returns true if selection was fixed.
+function extendAttributeOnMentionPostFixer( writer, doc ) {
+	const changes = doc.differ.getChanges();
 
-				const expectedText = mention._marker + mention.name;
+	let wasChanged = false;
+
+	for ( const change of changes ) {
+		if ( change.type === 'attribute' && change.attributeKey != 'mention' ) {
+			// Check node at the left side of a range...
+			const nodeBefore = change.range.start.nodeBefore;
+			// ... and on right side of range.
+			const nodeAfter = change.range.end.nodeAfter;
+
+			for ( const node of [ nodeBefore, nodeAfter ] ) {
+				if ( isBrokenMentionNode( node ) && node.getAttribute( change.attributeKey ) != change.attributeNewValue ) {
+					writer.setAttribute( change.attributeKey, change.attributeNewValue, node );
 
-				if ( text != expectedText ) {
-					writer.removeAttribute( 'mention', nodeBefore );
 					wasChanged = true;
 				}
 			}
@@ -176,3 +212,36 @@ function removePartialMentionPostFixer( 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.
+//
+// @param {module:engine/model/node~Node} node a node to check
+// @returns {Boolean}
+function isBrokenMentionNode( node ) {
+	if ( !node || !( node.is( 'text' ) || node.is( 'textProxy' ) ) || !node.hasAttribute( 'mention' ) ) {
+		return false;
+	}
+
+	const text = node.data;
+	const mention = node.getAttribute( 'mention' );
+
+	const expectedText = mention._marker + mention.name;
+
+	return text != expectedText;
+}
+
+// Fixes mention on text node it needs a fix.
+//
+// @param {module:engine/model/text~Text} textNode
+// @param {module:engine/model/writer~Writer} writer
+// @returns {Boolean}
+function checkAndFix( textNode, writer ) {
+	if ( isBrokenMentionNode( textNode ) ) {
+		writer.removeAttribute( 'mention', textNode );
+
+		return true;
+	}
+
+	return false;
+}

+ 135 - 13
packages/ckeditor5-mention/tests/mention-integration.js

@@ -5,15 +5,18 @@
 
 /* global document */
 
-import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
-import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
+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 UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting';
 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 MentionEditing from '../src/mentionediting';
 
-describe( 'MentionEditing - integration', () => {
+describe( 'Mention feature - integration', () => {
 	let div, editor, model, doc;
 
 	testUtils.createSinonSandbox();
@@ -21,13 +24,6 @@ describe( 'MentionEditing - integration', () => {
 	beforeEach( () => {
 		div = document.createElement( 'div' );
 		document.body.appendChild( div );
-
-		return ClassicTestEditor.create( div, { plugins: [ Paragraph, MentionEditing, UndoEditing ] } )
-			.then( newEditor => {
-				editor = newEditor;
-				model = editor.model;
-				doc = model.document;
-			} );
 	} );
 
 	afterEach( () => {
@@ -36,7 +32,19 @@ describe( 'MentionEditing - integration', () => {
 		return editor.destroy();
 	} );
 
-	describe( 'undo', () => {
+	describe( 'with undo', () => {
+		beforeEach( () => {
+			return ClassicTestEditor.create( div, { plugins: [ Paragraph, MentionEditing, UndoEditing ] } )
+				.then( newEditor => {
+					editor = newEditor;
+					model = editor.model;
+					doc = model.document;
+
+					model.schema.extend( '$text', { allowAttributes: [ 'bold' ] } );
+					editor.conversion.attributeToElement( { model: 'bold', view: 'strong' } );
+				} );
+		} );
+
 		// Failing test. See ckeditor/ckeditor5#1645.
 		it( 'should restore removed mention on adding a text inside mention', () => {
 			editor.setData( '<p>foo <span class="mention" data-mention="John">@John</span> bar</p>' );
@@ -57,7 +65,7 @@ describe( 'MentionEditing - integration', () => {
 			editor.execute( 'undo' );
 
 			expect( editor.getData() ).to.equal( '<p>foo <span class="mention" data-mention="John">@John</span> bar</p>' );
-			expect( getViewData( editor.editing.view ) )
+			expect( getViewData( editor.editing.view, { withoutSelection: true } ) )
 				.to.equal( '<p>foo <span class="mention" data-mention="John">@John</span> bar</p>' );
 		} );
 
@@ -82,8 +90,122 @@ describe( 'MentionEditing - integration', () => {
 			editor.execute( 'undo' );
 
 			expect( editor.getData() ).to.equal( '<p>foo <span class="mention" data-mention="John">@John</span> bar</p>' );
-			expect( getViewData( editor.editing.view ) )
+			expect( getViewData( editor.editing.view, { withoutSelection: true } ) )
 				.to.equal( '<p>foo <span class="mention" data-mention="John">@John</span> bar</p>' );
 		} );
+
+		it( 'should work with attribute post-fixer (beginning formatted)', () => {
+			testAttributePostFixer(
+				'<p>foo <span class="mention" data-mention="John">@John</span> bar</p>',
+				'<p><strong>foo </strong><span class="mention" data-mention="John"><strong>@John</strong></span> bar</p>',
+				() => {
+					model.change( writer => {
+						const paragraph = doc.getRoot().getChild( 0 );
+						const start = writer.createPositionAt( paragraph, 0 );
+						const range = writer.createRange( start, start.getShiftedBy( 6 ) );
+
+						writer.setSelection( range );
+
+						writer.setAttribute( 'bold', true, range );
+					} );
+				} );
+		} );
+
+		it( 'should work with attribute post-fixer (end formatted)', () => {
+			testAttributePostFixer(
+				'<p>foo <span class="mention" data-mention="John">@John</span> bar</p>',
+				'<p>foo <span class="mention" data-mention="John"><strong>@John</strong></span><strong> ba</strong>r</p>',
+				() => {
+					model.change( writer => {
+						const paragraph = doc.getRoot().getChild( 0 );
+						const start = writer.createPositionAt( paragraph, 6 );
+						const range = writer.createRange( start, start.getShiftedBy( 6 ) );
+
+						writer.setSelection( range );
+
+						writer.setAttribute( 'bold', true, range );
+					} );
+				} );
+		} );
+
+		it( 'should work with attribute post-fixer (middle formatted)', () => {
+			testAttributePostFixer(
+				'<p>foo <span class="mention" data-mention="John">@John</span> bar</p>',
+				'<p>foo <span class="mention" data-mention="John"><strong>@John</strong></span> bar</p>',
+				() => {
+					model.change( writer => {
+						const paragraph = doc.getRoot().getChild( 0 );
+						const start = writer.createPositionAt( paragraph, 6 );
+						const range = writer.createRange( start, start.getShiftedBy( 1 ) );
+
+						writer.setSelection( range );
+
+						writer.setAttribute( 'bold', true, range );
+					} );
+				} );
+		} );
+
+		function testAttributePostFixer( initialData, expectedData, testCallback ) {
+			editor.setData( initialData );
+
+			expect( editor.getData() ).to.equal( initialData );
+
+			testCallback();
+
+			expect( editor.getData() )
+				.to.equal( expectedData );
+			expect( getViewData( editor.editing.view, { withoutSelection: true } ) )
+				.to.equal( expectedData );
+
+			editor.execute( 'undo' );
+
+			expect( editor.getData() ).to.equal( initialData );
+			expect( getViewData( editor.editing.view, { withoutSelection: true } ) )
+				.to.equal( initialData );
+
+			editor.execute( 'redo' );
+
+			expect( editor.getData() )
+				.to.equal( expectedData );
+			expect( getViewData( editor.editing.view, { withoutSelection: true } ) )
+				.to.equal( expectedData );
+		}
+	} );
+
+	describe( 'with clipboard', () => {
+		let clipboard;
+
+		beforeEach( () => {
+			return ClassicTestEditor
+				.create( div, { plugins: [ Clipboard, Paragraph, BlockQuote, MentionEditing, UndoEditing ] } )
+				.then( newEditor => {
+					editor = newEditor;
+					model = editor.model;
+					doc = model.document;
+
+					clipboard = editor.plugins.get( 'Clipboard' );
+				} );
+		} );
+
+		it( 'should fix broken mention inside pasted content', () => {
+			editor.setData( '<p>foobar</p>' );
+
+			model.change( writer => {
+				writer.setSelection( doc.getRoot().getChild( 0 ), 3 );
+			} );
+
+			clipboard.fire( 'inputTransformation', {
+				content: parseView( '<blockquote><p>xxx<span class="mention" data-mention="John">@Joh</span></p></blockquote>' )
+			} );
+
+			const expectedData = '<p>foo</p>' +
+				'<blockquote><p>xxx@Joh</p></blockquote>' +
+				'<p>bar</p>';
+
+			expect( editor.getData() )
+				.to.equal( expectedData );
+			expect( getViewData( editor.editing.view, { withoutSelection: true } ) )
+				.to.equal( expectedData );
+		} );
 	} );
 } );

+ 249 - 5
packages/ckeditor5-mention/tests/mentionediting.js

@@ -145,7 +145,7 @@ describe( 'MentionEditing', () => {
 		} );
 	} );
 
-	describe( 'selection post fixer', () => {
+	describe( 'selection post-fixer', () => {
 		beforeEach( () => {
 			return createTestEditor()
 				.then( newEditor => {
@@ -182,7 +182,7 @@ describe( 'MentionEditing', () => {
 		} );
 	} );
 
-	describe( 'removing partial mention post fixer', () => {
+	describe( 'removing partial mention post-fixer', () => {
 		beforeEach( () => {
 			return createTestEditor()
 				.then( newEditor => {
@@ -192,7 +192,7 @@ describe( 'MentionEditing', () => {
 				} );
 		} );
 
-		it( 'should remove mention on adding a text inside mention', () => {
+		it( 'should remove mention on adding a text inside mention (in the middle)', () => {
 			editor.setData( '<p>foo <span class="mention" data-mention="John">@John</span> bar</p>' );
 
 			const textNode = doc.getRoot().getChild( 0 ).getChild( 1 );
@@ -217,7 +217,45 @@ describe( 'MentionEditing', () => {
 			expect( editor.getData() ).to.equal( '<p>foo @Jaohn bar</p>' );
 		} );
 
-		it( 'should remove mention on removing a text inside mention', () => {
+		it( 'should remove mention on typing in mention node with selection attributes set', () => {
+			editor.setData( '<p>foo <span class="mention" data-mention="John">@John</span> bar</p>' );
+
+			const textNode = doc.getRoot().getChild( 0 ).getChild( 1 );
+
+			expect( textNode ).to.not.be.null;
+			expect( textNode.hasAttribute( 'mention' ) ).to.be.true;
+
+			model.change( writer => {
+				const paragraph = doc.getRoot().getChild( 0 );
+
+				writer.setSelection( paragraph, 6 );
+				writer.setSelectionAttribute( 'bold', true );
+
+				writer.insertText( 'a', doc.selection.getAttributes(), writer.createPositionAt( paragraph, 6 ) );
+			} );
+
+			expect( getModelData( model, { withoutSelection: true } ) )
+				.to.equal( '<paragraph>foo @J<$text bold="true">a</$text>ohn bar</paragraph>' );
+		} );
+
+		it( 'should remove mention on removing a text at the beginning of a mention', () => {
+			editor.setData( '<p>foo <span class="mention" data-mention="John">@John</span> bar</p>' );
+
+			const paragraph = doc.getRoot().getChild( 0 );
+
+			model.change( writer => {
+				writer.setSelection( paragraph, 4 );
+			} );
+
+			model.enqueueChange( () => {
+				model.modifySelection( doc.selection, { direction: 'forward', unit: 'codepoint' } );
+				model.deleteContent( doc.selection );
+			} );
+
+			expect( editor.getData() ).to.equal( '<p>foo John bar</p>' );
+		} );
+
+		it( 'should remove mention on removing a text in the middle a mention', () => {
 			editor.setData( '<p>foo <span class="mention" data-mention="John">@John</span> bar</p>' );
 
 			const paragraph = doc.getRoot().getChild( 0 );
@@ -239,7 +277,6 @@ describe( 'MentionEditing', () => {
 
 			const paragraph = doc.getRoot().getChild( 0 );
 
-			// Set selection at the end of a John.
 			model.change( writer => {
 				writer.setSelection( paragraph, 9 );
 			} );
@@ -269,6 +306,213 @@ describe( 'MentionEditing', () => {
 
 			expect( editor.getData() ).to.equal( '<p>foo <span class="mention" data-mention="John">@John</span>bar</p>' );
 		} );
+
+		it( 'should remove mention on inserting text node inside a mention', () => {
+			editor.setData( '<p>foo <span class="mention" data-mention="John">@John</span> bar</p>' );
+
+			const paragraph = doc.getRoot().getChild( 0 );
+
+			model.change( writer => {
+				writer.insertText( 'baz', paragraph, 7 );
+			} );
+
+			expect( editor.getData() ).to.equal( '<p>foo @Jobazhn bar</p>' );
+		} );
+
+		it( 'should remove mention on inserting inline element inside a mention', () => {
+			model.schema.register( 'inline', {
+				allowWhere: '$text',
+				isInline: true
+			} );
+			editor.conversion.elementToElement( { model: 'inline', view: 'br' } );
+
+			editor.setData( '<p>foo <span class="mention" data-mention="John">@John</span> bar</p>' );
+
+			const paragraph = doc.getRoot().getChild( 0 );
+
+			model.change( writer => {
+				writer.insertElement( 'inline', paragraph, 7 );
+			} );
+
+			expect( editor.getData() ).to.equal( '<p>foo @Jo<br>hn bar</p>' );
+		} );
+
+		it( 'should remove mention when splitting paragraph with a mention', () => {
+			editor.setData( '<p>foo <span class="mention" data-mention="John">@John</span> bar</p>' );
+
+			const paragraph = doc.getRoot().getChild( 0 );
+
+			model.change( writer => {
+				writer.split( writer.createPositionAt( paragraph, 7 ) );
+			} );
+
+			expect( editor.getData() ).to.equal( '<p>foo @Jo</p><p>hn bar</p>' );
+		} );
+
+		it( 'should remove mention when deep splitting elements', () => {
+			model.schema.register( 'blockQuote', {
+				allowWhere: '$block',
+				allowContentOf: '$root'
+			} );
+
+			editor.conversion.elementToElement( { model: 'blockQuote', view: 'blockquote' } );
+			editor.setData( '<blockquote><p>foo <span class="mention" data-mention="John">@John</span> bar</p></blockquote>' );
+
+			model.change( writer => {
+				const paragraph = doc.getRoot().getChild( 0 ).getChild( 0 );
+
+				writer.split( writer.createPositionAt( paragraph, 7 ), doc.getRoot() );
+			} );
+
+			expect( editor.getData() ).to.equal( '<blockquote><p>foo @Jo</p></blockquote><blockquote><p>hn bar</p></blockquote>' );
+		} );
+	} );
+
+	describe( 'extend attribute on mention post-fixer', () => {
+		beforeEach( () => {
+			return createTestEditor()
+				.then( newEditor => {
+					editor = newEditor;
+					model = editor.model;
+					doc = model.document;
+				} );
+		} );
+
+		it( 'should set attribute on whole mention when formatting part of a mention (beginning formatted)', () => {
+			model.schema.extend( '$text', { allowAttributes: [ 'bold' ] } );
+			editor.conversion.attributeToElement( { model: 'bold', view: 'strong' } );
+
+			editor.setData( '<p>foo <span class="mention" data-mention="John">@John</span> bar</p>' );
+
+			const paragraph = doc.getRoot().getChild( 0 );
+
+			model.change( writer => {
+				const start = writer.createPositionAt( paragraph, 0 );
+				const range = writer.createRange( start, start.getShiftedBy( 6 ) );
+
+				writer.setSelection( range );
+
+				writer.setAttribute( 'bold', true, range );
+			} );
+
+			expect( editor.getData() )
+				.to.equal( '<p><strong>foo </strong><span class="mention" data-mention="John"><strong>@John</strong></span> bar</p>' );
+		} );
+
+		it( 'should set attribute on whole mention when formatting part of a mention (end formatted)', () => {
+			model.schema.extend( '$text', { allowAttributes: [ 'bold' ] } );
+			editor.conversion.attributeToElement( { model: 'bold', view: 'strong' } );
+
+			editor.setData( '<p>foo <span class="mention" data-mention="John">@John</span> bar</p>' );
+
+			const paragraph = doc.getRoot().getChild( 0 );
+
+			model.change( writer => {
+				const start = writer.createPositionAt( paragraph, 6 );
+				const range = writer.createRange( start, start.getShiftedBy( 6 ) );
+
+				writer.setSelection( range );
+
+				writer.setAttribute( 'bold', true, range );
+			} );
+
+			expect( editor.getData() )
+				.to.equal( '<p>foo <span class="mention" data-mention="John"><strong>@John</strong></span><strong> ba</strong>r</p>' );
+		} );
+
+		it( 'should set attribute on whole mention when formatting part of a mention (middle of mention formatted)', () => {
+			model.schema.extend( '$text', { allowAttributes: [ 'bold' ] } );
+			editor.conversion.attributeToElement( { model: 'bold', view: 'strong' } );
+
+			editor.setData( '<p>foo <span class="mention" data-mention="John">@John</span> bar</p>' );
+
+			const paragraph = doc.getRoot().getChild( 0 );
+
+			model.change( writer => {
+				const start = writer.createPositionAt( paragraph, 6 );
+				const range = writer.createRange( start, start.getShiftedBy( 1 ) );
+
+				writer.setSelection( range );
+
+				writer.setAttribute( 'bold', true, range );
+			} );
+
+			expect( editor.getData() )
+				.to.equal( '<p>foo <span class="mention" data-mention="John"><strong>@John</strong></span> bar</p>' );
+		} );
+
+		it( 'should set attribute on whole mention when formatting part of two mentions', () => {
+			model.schema.extend( '$text', { allowAttributes: [ 'bold' ] } );
+			editor.conversion.attributeToElement( { model: 'bold', view: 'strong' } );
+
+			editor.setData(
+				'<p><span class="mention" data-mention="John">@John</span><span class="mention" data-mention="John">@John</span></p>'
+			);
+
+			const paragraph = doc.getRoot().getChild( 0 );
+
+			model.change( writer => {
+				const start = writer.createPositionAt( paragraph, 4 );
+				const range = writer.createRange( start, start.getShiftedBy( 4 ) );
+
+				writer.setSelection( range );
+
+				writer.setAttribute( 'bold', true, range );
+			} );
+
+			expect( editor.getData() ).to.equal(
+				'<p>' +
+					'<span class="mention" data-mention="John"><strong>@John</strong></span>' +
+					'<span class="mention" data-mention="John"><strong>@John</strong></span>' +
+				'</p>'
+			);
+		} );
+
+		it( 'should work with multiple ranges in change set', () => {
+			model.schema.extend( '$text', { allowAttributes: [ 'foo' ] } );
+			editor.conversion.attributeToElement( {
+				model: {
+					key: 'foo',
+					values: [ 'a', 'b' ]
+				},
+				view: {
+					a: {
+						name: 'span',
+						classes: 'mark-a'
+					},
+					b: {
+						name: 'span',
+						classes: 'mark-b'
+					}
+				},
+				converterPriority: 'high'
+			} );
+
+			editor.setData(
+				'<p>' +
+					'<span class="mark-a">foo <span class="mention" data-mention="John">@John</span></span>' +
+					'<span class="mention" data-mention="John">@John</span> bar' +
+				'</p>'
+			);
+
+			model.change( writer => {
+				const paragraph = doc.getRoot().getChild( 0 );
+				const start = writer.createPositionAt( paragraph, 7 );
+				const range = writer.createRange( start, start.getShiftedBy( 5 ) );
+
+				writer.setAttribute( 'foo', 'b', range );
+			} );
+
+			expect( editor.getData() ).to.equal(
+				'<p>' +
+					'<span class="mark-a">foo </span>' +
+					'<span class="mark-b">' +
+						'<span class="mention" data-mention="John">@John</span>' +
+						'<span class="mention" data-mention="John">@John</span>' +
+					'</span> bar' +
+				'</p>'
+			);
+		} );
 	} );
 
 	function createTestEditor( mentionConfig ) {

+ 28 - 2
packages/ckeditor5-mention/tests/mentionui.js

@@ -334,7 +334,10 @@ describe( 'MentionUI', () => {
 			} );
 
 			it( 'should not show panel when selection is inside a mention', () => {
-				setData( model, '<paragraph>foo <$text mention="{\'name\':\'John\'}">@John</$text> bar</paragraph>' );
+				setData( model, '<paragraph>foo [@John] bar</paragraph>' );
+				model.change( writer => {
+					writer.setAttribute( 'mention', { name: 'John', _marker: '@', _id: 1234 }, doc.selection.getFirstRange() );
+				} );
 
 				model.change( writer => {
 					writer.setSelection( doc.getRoot().getChild( 0 ), 7 );
@@ -347,7 +350,10 @@ describe( 'MentionUI', () => {
 			} );
 
 			it( 'should not show panel when selection is at the end of a mention', () => {
-				setData( model, '<paragraph>foo <$text mention="{\'name\':\'John\'}">@John</$text> bar</paragraph>' );
+				setData( model, '<paragraph>foo [@John] bar</paragraph>' );
+				model.change( writer => {
+					writer.setAttribute( 'mention', { name: 'John', _marker: '@', _id: 1234 }, doc.selection.getFirstRange() );
+				} );
 
 				model.change( writer => {
 					writer.setSelection( doc.getRoot().getChild( 0 ), 9 );
@@ -380,6 +386,26 @@ describe( 'MentionUI', () => {
 					} );
 			} );
 
+			it( 'should not show panel when selection is after existing mention', () => {
+				setData( model, '<paragraph>foo [@John] bar[]</paragraph>' );
+				model.change( writer => {
+					writer.setAttribute( 'mention', { name: 'John', _marker: '@', _id: 1234 }, doc.selection.getFirstRange() );
+				} );
+
+				return waitForDebounce()
+					.then( () => {
+						expect( panelView.isVisible ).to.be.false;
+
+						model.change( writer => {
+							writer.setSelection( doc.getRoot().getChild( 0 ), 8 );
+						} );
+					} )
+					.then( waitForDebounce )
+					.then( () => {
+						expect( panelView.isVisible ).to.be.false;
+					} );
+			} );
+
 			it( 'should show filtered results for matched text', () => {
 				setData( model, '<paragraph>foo []</paragraph>' );