8
0
Pārlūkot izejas kodu

Add extend attribute on mention post-fixer and fix remove partial mention post-fixer.

Maciej Gołaszewski 6 gadi atpakaļ
vecāks
revīzija
a4f34a6da8

+ 95 - 13
packages/ckeditor5-mention/src/mentionediting.js

@@ -58,6 +58,7 @@ export default class MentionEditing extends Plugin {
 		} );
 
 		doc.registerPostFixer( writer => removePartialMentionPostFixer( writer, doc ) );
+		doc.registerPostFixer( writer => extendAttributeOnMentionPostFixer( writer, doc ) );
 		doc.registerPostFixer( writer => selectionMentionAttributePostFixer( writer, doc ) );
 
 		editor.commands.add( 'mention', new MentionCommand( editor ) );
@@ -146,23 +147,59 @@ function removePartialMentionPostFixer( writer, doc ) {
 	let wasChanged = false;
 
 	for ( const change of changes ) {
-		// Check if user edited part of a mention.
-		if ( change.name == '$text' && ( change.type == 'insert' || change.type == 'remove' ) ) {
-			const textNode = change.position.textNode;
+		// Check text node on current position;
+		if ( change.name == '$text' ) {
+			// Check textNode where the change occurred.
+			if ( change.type == 'insert' || change.type == 'remove' ) {
+				const textNode = change.position.textNode;
+
+				if ( !checkMentionAttributeOnNode( textNode ) ) {
+					writer.removeAttribute( 'mention', textNode );
+					wasChanged = true;
+				}
+			}
+
+			// Additional check: when removing text on mention boundaries.
+			if ( change.type == 'remove' ) {
+				const nodeBefore = change.position.nodeBefore;
+
+				if ( !checkMentionAttributeOnNode( nodeBefore ) ) {
+					writer.removeAttribute( 'mention', nodeBefore );
+					wasChanged = true;
+				}
 
-			if ( !checkMentionAttributeOnNode( textNode ) ) {
-				writer.removeAttribute( 'mention', textNode );
-				wasChanged = true;
+				const nodeAfter = change.position.nodeAfter;
+
+				if ( !checkMentionAttributeOnNode( nodeAfter ) ) {
+					writer.removeAttribute( 'mention', nodeAfter );
+					wasChanged = true;
+				}
 			}
 		}
+	}
 
-		// Additional check for deleting last character of a text node.
-		if ( change.type == 'remove' ) {
-			const nodeBefore = change.position.nodeBefore;
+	return wasChanged;
+}
 
-			if ( !checkMentionAttributeOnNode( nodeBefore ) ) {
-				writer.removeAttribute( 'mention', nodeBefore );
-				wasChanged = true;
+// 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();
+
+	let wasChanged = false;
+
+	for ( const change of changes ) {
+		if ( change.type === 'attribute' ) {
+			for ( const node of getBrokenMentionsFromRange( change.range ) ) {
+				for ( const partialMention of getFullMentionNode( node ) ) {
+					if ( partialMention.getAttribute( change.attributeKey ) !== change.attributeNewValue ) {
+						writer.setAttribute( change.attributeKey, change.attributeNewValue, partialMention );
+						wasChanged = true;
+					}
+				}
 			}
 		}
 	}
@@ -176,7 +213,7 @@ function removePartialMentionPostFixer( writer, doc ) {
 // @param {module:engine/model/node~Node} node a node to check
 // @returns {Boolean}
 function checkMentionAttributeOnNode( node ) {
-	if ( !node || !node.is( 'text' ) || !node.hasAttribute( 'mention' ) ) {
+	if ( !node || !( node.is( 'text' ) || node.is( 'textProxy' ) ) || !node.hasAttribute( 'mention' ) ) {
 		return true;
 	}
 
@@ -187,3 +224,48 @@ function checkMentionAttributeOnNode( node ) {
 
 	return text == expectedText;
 }
+
+function* getBrokenMentionsFromRange( range ) {
+	for ( const item of range.getItems() ) {
+		if ( !checkMentionAttributeOnNode( item ) ) {
+			yield item;
+		}
+	}
+}
+
+function getFullMentionNode( node ) {
+	const textNode = node.textNode;
+
+	const previousSibling = textNode.previousSibling;
+	const nextSibling = textNode.nextSibling;
+
+	const mention = textNode.getAttribute( 'mention' );
+
+	const nodes = [];
+
+	const isPreviousPartial = !checkMentionAttributeOnNode( previousSibling );
+
+	if ( isPreviousPartial ) {
+		const previousSiblingMention = previousSibling.getAttribute( 'mention' );
+		const sameMention = previousSiblingMention._id == mention._id;
+
+		if ( sameMention ) {
+			nodes.push( previousSibling );
+		}
+	}
+
+	nodes.push( textNode );
+
+	const isNextPartial = !checkMentionAttributeOnNode( nextSibling );
+
+	if ( isNextPartial ) {
+		const nextSiblingMention = nextSibling.getAttribute( 'mention' );
+		const sameMention = nextSiblingMention._id == mention._id;
+
+		if ( sameMention ) {
+			nodes.push( nextSibling );
+		}
+	}
+
+	return nodes;
+}

+ 82 - 3
packages/ckeditor5-mention/tests/mentionediting.js

@@ -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,24 @@ 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 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 +256,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 +285,69 @@ describe( 'MentionEditing', () => {
 
 			expect( editor.getData() ).to.equal( '<p>foo <span class="mention" data-mention="John">@John</span>bar</p>' );
 		} );
+
+		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>' );
+		} );
 	} );
 
 	function createTestEditor( mentionConfig ) {