Sfoglia il codice sorgente

Add preventPartialMentionDowncast() converter to disallow partial mention downcast.

Maciej Gołaszewski 6 anni fa
parent
commit
beacf3cf1f

+ 25 - 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,28 @@ export function _toMentionAttribute( viewElementOrMention, data ) {
 	return _addMentionAttributes( baseMentionData, data );
 }
 
+// Converter that prevents other converters from converting partial mentions.
+//
+// This helper consumes partial mention which should not be downcasted to allow usage
+// of attributeToElement() converter to override default conversion without the need to write more specialised converter.
+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

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

@@ -10,7 +10,7 @@ import { stringify as stringifyView, getData as getViewData } from '@ckeditor/ck
 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', () => {
@@ -58,7 +58,7 @@ describe( 'MentionEditing', () => {
 			} );
 	} );
 
-	describe.only( 'conversion', () => {
+	describe( 'conversion', () => {
 		beforeEach( () => {
 			return createTestEditor()
 				.then( newEditor => {
@@ -85,6 +85,26 @@ describe( 'MentionEditing', () => {
 			expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal( expectedView );
 		} );
 
+		it( 'should be overridable', () => {
+			addCustomToLinkConverters( editor );
+
+			editor.setData( '<p>Hello <a class="mention" data-mention="@Ted Mosby" href="/foo/bar">Ted Mosby</a></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( 'link', '/foo/bar' );
+			expect( textNode.getAttribute( 'mention' ) ).to.have.property( '_text', 'Ted Mosby' );
+			expect( textNode.getAttribute( 'mention' ) ).to.have.property( '_uid' );
+
+			const expectedView = '<p>Hello <a class="mention" data-mention="@Ted Mosby" href="/foo/bar">Ted Mosby</a></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>' +
@@ -140,7 +160,7 @@ describe( 'MentionEditing', () => {
 			expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal( expectedView );
 		} );
 
-		it( 'should not downcast partial mention', done => {
+		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 => {
@@ -164,6 +184,48 @@ describe( 'MentionEditing', () => {
 			} );
 		} );
 
+		it( 'should not downcast partial mention (custom converter)', done => {
+			addCustomToLinkConverters( 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 <a class="mention" data-mention="@Ted Mosby" href="/foo/bar">Ted Mosby</a></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>' );
 
@@ -570,3 +632,41 @@ describe( 'MentionEditing', () => {
 		};
 	}
 } );
+
+function addCustomToLinkConverters( editor ) {
+	editor.conversion.for( 'upcast' ).elementToAttribute( {
+		view: {
+			name: 'a',
+			key: 'data-mention',
+			classes: 'mention',
+			attributes: {
+				href: true
+			}
+		},
+		model: {
+			key: 'mention',
+			value: viewItem => {
+				return _toMentionAttribute( viewItem, {
+					link: viewItem.getAttribute( 'href' )
+				} );
+			}
+		},
+		converterPriority: 'high'
+	} );
+
+	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'
+	} );
+}