8
0
Просмотр исходного кода

Store marker and mention in the model.

Maciej Gołaszewski 6 лет назад
Родитель
Сommit
9eaf1ed524

+ 5 - 0
packages/ckeditor5-mention/src/mentioncommand.js

@@ -9,6 +9,7 @@
 
 import Command from '@ckeditor/ckeditor5-core/src/command';
 import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
+import uid from '@ckeditor/ckeditor5-utils/src/uid';
 
 /**
  * The mention command.
@@ -61,6 +62,10 @@ export default class MentionCommand extends Command {
 
 		const mention = typeof options.mention == 'string' ? { name: options.mention } : options.mention;
 
+		// Set internal attributes on mention object.
+		mention._id = uid();
+		mention._marker = marker;
+
 		const range = options.range || selection.getFirstRange();
 
 		model.change( writer => {

+ 110 - 71
packages/ckeditor5-mention/src/mentionediting.js

@@ -8,12 +8,17 @@
  */
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import MentionCommand from './mentioncommand';
 import uid from '@ckeditor/ckeditor5-utils/src/uid';
 
+import MentionCommand from './mentioncommand';
+
 /**
  * The mention editing feature.
  *
+ * It introduces the {@link module:mention/mentioncommand~MentionCommand command} and the `mention`
+ * attribute in the {@link module:engine/model/model~Model model} which renders in the {@link module:engine/view/view view}
+ * as a `<span class="mention" data-mention="name">`.
+ *
  * @extends module:core/plugin~Plugin
  */
 export default class MentionEditing extends Plugin {
@@ -29,9 +34,11 @@ export default class MentionEditing extends Plugin {
 	 */
 	init() {
 		const editor = this.editor;
+		const model = editor.model;
+		const doc = model.document;
 
-		// Allow mention attribute on text nodes.
-		editor.model.schema.extend( '$text', { allowAttributes: 'mention' } );
+		// Allow mention attribute on all text nodes.
+		model.schema.extend( '$text', { allowAttributes: 'mention' } );
 
 		editor.conversion.for( 'upcast' ).elementToAttribute( {
 			view: {
@@ -41,99 +48,131 @@ export default class MentionEditing extends Plugin {
 			},
 			model: {
 				key: 'mention',
-				value: viewItem => {
-					const dataMention = viewItem.getAttribute( 'data-mention' );
+				value: parseMentionViewItemAttributes
+			}
+		} );
 
-					const textNode = viewItem.getChild( 0 );
+		editor.conversion.for( 'downcast' ).attributeToElement( {
+			model: 'mention',
+			view: createViewMentionElement
+		} );
 
-					if ( !textNode || !textNode.is( 'text' ) ) {
-						return;
-					}
+		doc.registerPostFixer( writer => removePartialMentionPostFixer( writer, doc ) );
+		doc.registerPostFixer( writer => selectionMentionAttributePostFixer( writer, doc ) );
 
-					const mentionString = textNode.data;
+		editor.commands.add( 'mention', new MentionCommand( editor ) );
+	}
+}
 
-					// const marker = mentionString.slice( 0, 1 );
-					const name = mentionString.slice( 1 );
+// Parses matched view element to mention attribute value.
+//
+// @param {module:engine/view/element} viewElement
+// @returns {Object} Mention attribute value
+function parseMentionViewItemAttributes( viewElement ) {
+	const dataMention = viewElement.getAttribute( 'data-mention' );
 
-					if ( name != dataMention ) {
-						return;
-					}
+	const textNode = viewElement.getChild( 0 );
 
-					return { name: dataMention, _id: uid() };
-				}
-			}
-		} );
+	// Do not parse empty mentions.
+	if ( !textNode || !textNode.is( 'text' ) ) {
+		return;
+	}
 
-		editor.conversion.for( 'downcast' ).attributeToElement( {
-			model: 'mention',
-			view: ( mention, viewWriter ) => {
-				if ( !mention ) {
-					return;
-				}
+	const mentionString = textNode.data;
 
-				const attributes = {
-					class: 'mention',
-					'data-mention': mention.name
-				};
+	// Assume that mention is set as marker + mention name.
+	const marker = mentionString.slice( 0, 1 );
+	const name = mentionString.slice( 1 );
 
-				const options = {
-					id: mention._id
-				};
+	// Do not upcast partial mentions - might come from copy-paste of partially selected mention.
+	if ( name != dataMention ) {
+		return;
+	}
 
-				return viewWriter.createAttributeElement( 'span', attributes, options );
-			}
-		} );
+	// Set UID for mention to not merge mentions in the same block that are next to each other.
+	return { name: dataMention, _marker: marker, _id: uid() };
+}
 
-		// Remove mention attribute if text was edited.
-		editor.model.document.registerPostFixer( writer => {
-			const changes = editor.model.document.differ.getChanges();
+// Creates mention element from mention data.
+//
+// @param {Object} mention
+// @param {module:engine/view/downcastwriter~DowncastWriter} viewWriter
+// @returns {module:engine/view/attributeelement~AttributeElement}
+function createViewMentionElement( mention, viewWriter ) {
+	if ( !mention ) {
+		return;
+	}
 
-			let wasChanged = false;
+	const attributes = {
+		class: 'mention',
+		'data-mention': mention.name
+	};
 
-			for ( const change of changes ) {
-				if ( change.type == 'insert' || change.type == 'remove' ) {
-					const textNode = change.position.textNode;
+	const options = {
+		id: mention._id
+	};
 
-					if ( change.name == '$text' && textNode && textNode.hasAttribute( 'mention' ) ) {
-						writer.removeAttribute( 'mention', textNode );
-						wasChanged = true;
-					}
-				}
+	return viewWriter.createAttributeElement( 'span', attributes, options );
+}
 
-				if ( change.type == 'remove' ) {
-					const nodeBefore = change.position.nodeBefore;
+// Model post-fixer that disallows typing with selection when selection is placed after the text node with mention attribute.
+//
+// @param {module:engine/model/writer~Writer} writer
+// @param {module:engine/model/document~Document} doc
+// @returns {Boolean} Returns true if selection was fixed.
+function selectionMentionAttributePostFixer( writer, doc ) {
+	const selection = doc.selection;
+	const focus = selection.focus;
 
-					if ( nodeBefore && nodeBefore.hasAttribute( 'mention' ) ) {
-						const text = nodeBefore.data;
+	if ( selection.isCollapsed && selection.hasAttribute( 'mention' ) && isNodeBeforeAText( focus ) ) {
+		writer.removeSelectionAttribute( 'mention' );
 
-						const mention = nodeBefore.getAttribute( 'mention' );
+		return true;
+	}
 
-						const name = mention.name;
+	function isNodeBeforeAText( position ) {
+		return position.nodeBefore && position.nodeBefore.is( 'text' );
+	}
+}
 
-						const textName = text.slice( 1 );
+// Model post-fixer that removes mention attribute from 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.
+function removePartialMentionPostFixer( writer, doc ) {
+	const changes = doc.differ.getChanges();
 
-						if ( textName != name ) {
-							writer.removeAttribute( 'mention', nodeBefore );
-							wasChanged = true;
-						}
-					}
-				}
+	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;
+
+			if ( change.name == '$text' && textNode && textNode.hasAttribute( 'mention' ) ) {
+				writer.removeAttribute( 'mention', textNode );
+				wasChanged = true;
 			}
+		}
 
-			return wasChanged;
-		} );
+		// Additional check for deleting last character of a text node.
+		if ( change.type == 'remove' ) {
+			const nodeBefore = change.position.nodeBefore;
 
-		editor.model.document.registerPostFixer( writer => {
-			const selection = editor.model.document.selection;
-			const focus = selection.focus;
+			if ( nodeBefore && nodeBefore.hasAttribute( 'mention' ) ) {
+				const text = nodeBefore.data;
+				const mention = nodeBefore.getAttribute( 'mention' );
 
-			if ( selection.hasAttribute( 'mention' ) && selection.isCollapsed && focus.nodeBefore && focus.nodeBefore.is( 'text' ) ) {
-				writer.removeSelectionAttribute( 'mention' );
+				const expectedText = mention._marker + mention.name;
 
-				return true;
+				if ( text != expectedText ) {
+					writer.removeAttribute( 'mention', nodeBefore );
+					wasChanged = true;
+				}
 			}
-		} );
-
-		editor.commands.add( 'mention', new MentionCommand( editor ) );
+		}
 	}
+
+	return wasChanged;
 }

+ 15 - 12
packages/ckeditor5-mention/tests/mentioncommand.js

@@ -4,7 +4,7 @@
  */
 
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
-import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 import MentionCommand from '../src/mentioncommand';
 
@@ -62,7 +62,7 @@ describe( 'MentionCommand', () => {
 				range: model.createRange( selection.focus.getShiftedBy( -3 ), selection.focus )
 			} );
 
-			expect( getData( model ) ).to.equal( '<paragraph>foo <$text mention="{"name":"John"}">@John</$text> []bar</paragraph>' );
+			assertMention( doc.getRoot().getChild( 0 ).getChild( 1 ), '@', 'John' );
 		} );
 
 		it( 'inserts mention object if mention was passed as string', () => {
@@ -73,7 +73,7 @@ describe( 'MentionCommand', () => {
 				range: model.createRange( selection.focus.getShiftedBy( -3 ), selection.focus )
 			} );
 
-			expect( getData( model ) ).to.equal( '<paragraph>foo <$text mention="{"name":"John"}">@John</$text> []bar</paragraph>' );
+			assertMention( doc.getRoot().getChild( 0 ).getChild( 1 ), '@', 'John' );
 		} );
 
 		it( 'inserts mention attribute with passed marker for given range', () => {
@@ -88,7 +88,7 @@ describe( 'MentionCommand', () => {
 				marker: '#'
 			} );
 
-			expect( getData( model ) ).to.equal( '<paragraph>foo <$text mention="{"name":"John"}">#John</$text> []bar</paragraph>' );
+			assertMention( doc.getRoot().getChild( 0 ).getChild( 1 ), '#', 'John' );
 		} );
 
 		it( 'inserts mention attribute at current selection if no range was passed', () => {
@@ -98,7 +98,7 @@ describe( 'MentionCommand', () => {
 				mention: { name: 'John' }
 			} );
 
-			expect( getData( model ) ).to.equal( '<paragraph>foo <$text mention="{"name":"John"}">@John</$text> []bar</paragraph>' );
+			assertMention( doc.getRoot().getChild( 0 ).getChild( 1 ), '@', 'John' );
 		} );
 
 		it( 'should set also other styles in inserted text', () => {
@@ -111,13 +111,16 @@ describe( 'MentionCommand', () => {
 				range: model.createRange( selection.focus.getShiftedBy( -5 ), selection.focus )
 			} );
 
-			expect( getData( model ) ).to.equal(
-				'<paragraph>' +
-					'<$text bold="true">foo</$text>' +
-					'<$text bold="true" mention="{"name":"John"}">@John</$text>' +
-					'<$text bold="true"> []bar</$text>' +
-				'</paragraph>'
-			);
+			const textNode = doc.getRoot().getChild( 0 ).getChild( 1 );
+			assertMention( textNode, '@', 'John' );
+			expect( textNode.hasAttribute( 'bold' ) ).to.be.true;
 		} );
 	} );
+
+	function assertMention( textNode, marker, name ) {
+		expect( textNode.hasAttribute( 'mention' ) ).to.be.true;
+		expect( textNode.getAttribute( 'mention' ) ).to.have.property( '_id' );
+		expect( textNode.getAttribute( 'mention' ) ).to.have.property( '_marker', marker );
+		expect( textNode.getAttribute( 'mention' ) ).to.have.property( 'name', name );
+	}
 } );

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

@@ -12,8 +12,7 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting';
 import MentionCommand from '../src/mentioncommand';
 
-describe( 'MentionEditing', () => {
-	testUtils.createSinonSandbox();
+describe.only( 'MentionEditing', () => {
 	let editor, model, doc;
 
 	testUtils.createSinonSandbox();
@@ -76,6 +75,7 @@ describe( 'MentionEditing', () => {
 			expect( textNode ).to.not.be.null;
 			expect( textNode.hasAttribute( 'mention' ) ).to.be.true;
 			expect( textNode.getAttribute( 'mention' ) ).to.have.property( '_id' );
+			expect( textNode.getAttribute( 'mention' ) ).to.have.property( '_marker', '@' );
 			expect( textNode.getAttribute( 'mention' ) ).to.have.property( 'name', 'John' );
 
 			expect( editor.getData() ).to.equal( '<p>foo <span class="mention" data-mention="John">@John</span> bar</p>' );
@@ -115,6 +115,7 @@ describe( 'MentionEditing', () => {
 				expect( textNode ).to.not.be.null;
 				expect( textNode.hasAttribute( 'mention' ) ).to.be.true;
 				expect( textNode.getAttribute( 'mention' ) ).to.have.property( '_id' );
+				expect( textNode.getAttribute( 'mention' ) ).to.have.property( '_marker', '@' );
 				expect( textNode.getAttribute( 'mention' ) ).to.have.property( 'name', 'John' );
 			}
 		} );
@@ -183,6 +184,7 @@ describe( 'MentionEditing', () => {
 			expect( textNode ).to.not.be.null;
 			expect( textNode.hasAttribute( 'mention' ) ).to.be.true;
 			expect( textNode.getAttribute( 'mention' ) ).to.have.property( '_id' );
+			expect( textNode.getAttribute( 'mention' ) ).to.have.property( '_marker', '@' );
 			expect( textNode.getAttribute( 'mention' ) ).to.have.property( 'name', 'John' );
 
 			model.change( writer => {