瀏覽代碼

Merge pull request #213 from ckeditor/t/208

Feature: Introduced `Input#isInput()`. Closes #214. Fixed the `TextTransformation` feature so it willl trigger only for typing changes. Closes #208.
Szymon Cofalik 6 年之前
父節點
當前提交
326738399d

+ 23 - 0
packages/ckeditor5-typing/src/input.js

@@ -40,4 +40,27 @@ export default class Input extends Plugin {
 		injectUnsafeKeystrokesHandling( editor );
 		injectTypingMutationsHandling( editor );
 	}
+
+	/**
+	 * Checks batch if it is a result of user input - e.g. typing.
+	 *
+	 *		const input = editor.plugins.get( 'Input' );
+	 *
+	 *		editor.model.document.on( 'change:data', ( evt, batch ) => {
+	 *			if ( input.isTyping( batch ) ) {
+	 *				console.log( 'The user typed something...' );
+	 *			}
+	 *		} );
+	 *
+	 * **Note:** This method checks if the batch was created using {@link module:typing/inputcommand~InputCommand 'input'}
+	 * command as typing changes coming from user input are inserted to the document using that command.
+	 *
+	 * @param {module:engine/model/batch~Batch} batch A batch to check.
+	 * @returns {Boolean}
+	 */
+	isInput( batch ) {
+		const inputCommand = this.editor.commands.get( 'input' );
+
+		return inputCommand._batches.has( batch );
+	}
 }

+ 12 - 0
packages/ckeditor5-typing/src/inputcommand.js

@@ -35,6 +35,15 @@ export default class InputCommand extends Command {
 		 * @member {module:typing/utils/changebuffer~ChangeBuffer} #_buffer
 		 */
 		this._buffer = new ChangeBuffer( editor.model, undoStepSize );
+
+		/**
+		 * Stores batches created by the input command. The batches are used to differentiate input batches from other batches using
+		 * {@link module:typing/input~Input#isInput} method.
+		 *
+		 * @type {WeakSet<module:engine/model/batch~Batch>}
+		 * @protected
+		 */
+		this._batches = new WeakSet();
 	}
 
 	/**
@@ -98,6 +107,9 @@ export default class InputCommand extends Command {
 			this._buffer.unlock();
 
 			this._buffer.input( textInsertions );
+
+			// Store the batch as an 'input' batch for the Input.isInput( batch ) check.
+			this._batches.add( this._buffer.batch );
 		} );
 	}
 }

+ 5 - 0
packages/ckeditor5-typing/src/texttransformation.js

@@ -100,6 +100,7 @@ export default class TextTransformation extends Plugin {
 	init() {
 		const editor = this.editor;
 		const model = editor.model;
+		const input = editor.plugins.get( 'Input' );
 
 		const configuredTransformations = getConfiguredTransformations( editor.config.get( 'typing.transformations' ) );
 
@@ -110,6 +111,10 @@ export default class TextTransformation extends Plugin {
 			const watcher = new TextWatcher( editor.model, text => from.test( text ) );
 
 			watcher.on( 'matched:data', ( evt, data ) => {
+				if ( !input.isInput( data.batch ) ) {
+					return;
+				}
+
 				const matches = from.exec( data.text );
 				const replaces = to( matches.slice( 1 ) );
 

+ 11 - 4
packages/ckeditor5-typing/src/textwatcher.js

@@ -66,7 +66,7 @@ export default class TextWatcher {
 				return;
 			}
 
-			this._evaluateTextBeforeSelection( 'data' );
+			this._evaluateTextBeforeSelection( 'data', { batch } );
 		} );
 	}
 
@@ -79,8 +79,9 @@ export default class TextWatcher {
 	 *
 	 * @private
 	 * @param {'data'|'selection'} suffix Suffix used for generating event name.
+	 * @param {Object} data Data object for event.
 	 */
-	_evaluateTextBeforeSelection( suffix ) {
+	_evaluateTextBeforeSelection( suffix, data = {} ) {
 		const text = this._getText();
 
 		const textHasMatch = this.testCallback( text );
@@ -97,18 +98,24 @@ export default class TextWatcher {
 		this.hasMatch = textHasMatch;
 
 		if ( textHasMatch ) {
+			const eventData = Object.assign( data, { text } );
+
 			/**
 			 * Fired whenever the text watcher found a match for data changes.
 			 *
 			 * @event matched:data
+			 * @param {Object} data Event data.
+			 * @param {String} data.text The full text before selection.
+			 * @param {module:engine/model/batch~Batch} data.batch A batch associated with a change.
 			 */
-
 			/**
 			 * Fired whenever the text watcher found a match for selection changes.
 			 *
 			 * @event matched:selection
+			 * @param {Object} data Event data.
+			 * @param {String} data.text The full text before selection.
 			 */
-			this.fire( `matched:${ suffix }`, { text } );
+			this.fire( `matched:${ suffix }`, eventData );
 		}
 	}
 

+ 23 - 0
packages/ckeditor5-typing/tests/input.js

@@ -70,6 +70,29 @@ describe( 'Input feature', () => {
 		return editor.destroy();
 	} );
 
+	describe( 'isInput()', () => {
+		let input;
+
+		beforeEach( () => {
+			input = editor.plugins.get( 'Input' );
+		} );
+
+		it( 'returns true for batch created using "input" command', done => {
+			model.document.once( 'change:data', ( evt, batch ) => {
+				expect( input.isInput( batch ) ).to.be.true;
+				done();
+			} );
+
+			editor.execute( 'input', { text: 'foo' } );
+		} );
+
+		it( 'returns false for batch not created using "input" command', () => {
+			const batch = model.createBatch();
+
+			expect( input.isInput( batch ) ).to.be.false;
+		} );
+	} );
+
 	describe( 'mutations handling', () => {
 		it( 'should handle text mutation', () => {
 			viewDocument.fire( 'mutations', [

+ 5 - 9
packages/ckeditor5-typing/tests/texttransformation-integration.js

@@ -12,6 +12,7 @@ import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictest
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 
 import TextTransformation from '../src/texttransformation';
+import Typing from '../src/typing';
 
 describe( 'Text transformation feature - integration', () => {
 	let editorElement, editor, model, doc;
@@ -32,7 +33,7 @@ describe( 'Text transformation feature - integration', () => {
 	describe( 'with undo', () => {
 		beforeEach( () => {
 			return ClassicTestEditor
-				.create( editorElement, { plugins: [ Paragraph, TextTransformation, UndoEditing ] } )
+				.create( editorElement, { plugins: [ Typing, Paragraph, TextTransformation, UndoEditing ] } )
 				.then( newEditor => {
 					editor = newEditor;
 					model = editor.model;
@@ -48,10 +49,7 @@ describe( 'Text transformation feature - integration', () => {
 				writer.insertText( '(c', doc.selection.focus );
 			} );
 
-			model.enqueueChange( model.createBatch(), writer => {
-				writer.setSelection( doc.getRoot().getChild( 0 ), 'end' );
-				writer.insertText( ')', doc.selection.focus );
-			} );
+			editor.execute( 'input', { text: ')' } );
 
 			expect( editor.getData(), 'inserted text' ).to.equal( '<p>foo©</p>' );
 
@@ -72,10 +70,8 @@ describe( 'Text transformation feature - integration', () => {
 				writer.insertText( 'foo bar baz(c', doc.selection.focus );
 			} );
 
-			model.enqueueChange( model.createBatch(), writer => {
-				writer.setSelection( doc.getRoot().getChild( 0 ), 'end' );
-				writer.insertText( ')', doc.selection.focus );
-			} );
+			editor.execute( 'input', { text: ')' } );
+
 			expect( editor.getData() ).to.equal( '<p>foo bar baz©</p>' );
 
 			editor.execute( 'undo' );

+ 53 - 52
packages/ckeditor5-typing/tests/texttransformation.js

@@ -6,6 +6,7 @@
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
 
+import Typing from '../src/typing';
 import TextTransformation from '../src/texttransformation';
 import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
@@ -52,6 +53,30 @@ describe( 'Text transformation feature', () => {
 			expect( getData( model, { withoutSelection: true } ) ).to.equal( '<paragraph>foo bar(tm) baz</paragraph>' );
 		} );
 
+		it( 'should not work for deletion changes', () => {
+			setData( model, '<paragraph>foo bar(tm) []</paragraph>' );
+
+			// Simulate delete command.
+			model.change( writer => {
+				const selection = writer.createSelection( doc.selection );
+				model.modifySelection( selection, { direction: 'backward', unit: 'character' } );
+				model.deleteContent( selection, { doNotResetEntireContent: true } );
+			} );
+
+			expect( getData( model, { withoutSelection: true } ) ).to.equal( '<paragraph>foo bar(tm)</paragraph>' );
+		} );
+
+		it( 'should not work for merging changes', () => {
+			setData( model, '<paragraph>foo bar(tm)</paragraph><paragraph>[] baz</paragraph>' );
+
+			// Simulate delete command.
+			model.change( writer => {
+				writer.merge( writer.createPositionAfter( doc.getRoot().getChild( 0 ) ) );
+			} );
+
+			expect( getData( model, { withoutSelection: true } ) ).to.equal( '<paragraph>foo bar(tm) baz</paragraph>' );
+		} );
+
 		describe( 'symbols', () => {
 			testTransformation( '(c)', '©' );
 			testTransformation( '(r)', '®' );
@@ -92,21 +117,21 @@ describe( 'Text transformation feature', () => {
 		it( 'should replace only the parts of content which changed', () => {
 			setData( model, '<paragraph>Foo "<$text bold="true">Bar</$text>[]</paragraph>' );
 
-			model.change( writer => {
-				writer.insertText( '"', doc.selection.focus );
-			} );
+			simulateTyping( '"' );
 
 			expect( getData( model, { withoutSelection: true } ) )
-				.to.equal( '<paragraph>Foo “<$text bold="true">Bar</$text></paragraph>' );
+				.to.equal( '<paragraph>Foo “<$text bold="true">Bar</$text></paragraph>' );
 		} );
 
 		it( 'should keep styles of the replaced text #1', () => {
 			setData( model, '<paragraph>Foo <$text bold="true">"</$text>Bar[]</paragraph>' );
 
 			model.change( writer => {
-				writer.insertText( '"', { bold: true }, doc.selection.focus );
+				writer.setSelectionAttribute( { bold: true } );
 			} );
 
+			simulateTyping( '"' );
+
 			expect( getData( model, { withoutSelection: true } ) )
 				.to.equal( '<paragraph>Foo <$text bold="true">“</$text>Bar<$text bold="true">”</$text></paragraph>' );
 		} );
@@ -114,9 +139,7 @@ describe( 'Text transformation feature', () => {
 		it( 'should keep styles of the replaced text #2', () => {
 			setData( model, '<paragraph>F<$text bold="true">oo "B</$text>ar[]</paragraph>' );
 
-			model.change( writer => {
-				writer.insertText( '"', doc.selection.focus );
-			} );
+			simulateTyping( '"' );
 
 			expect( getData( model, { withoutSelection: true } ) )
 				.to.equal( '<paragraph>F<$text bold="true">oo “B</$text>ar”</paragraph>' );
@@ -126,13 +149,7 @@ describe( 'Text transformation feature', () => {
 			it( `should transform "${ transformFrom }" to "${ transformTo }"`, () => {
 				setData( model, `<paragraph>${ textInParagraph }[]</paragraph>` );
 
-				const letters = transformFrom.split( '' );
-
-				for ( const letter of letters ) {
-					model.enqueueChange( model.createBatch(), writer => {
-						writer.insertText( letter, doc.selection.focus );
-					} );
-				}
+				simulateTyping( transformFrom );
 
 				expect( getData( model, { withoutSelection: true } ) )
 					.to.equal( `<paragraph>${ textInParagraph }${ transformTo }</paragraph>` );
@@ -170,9 +187,7 @@ describe( 'Text transformation feature', () => {
 			} ).then( () => {
 				setData( model, '<paragraph>[]</paragraph>' );
 
-				model.enqueueChange( model.createBatch(), writer => {
-					writer.insertText( 'CKE', doc.selection.focus );
-				} );
+				simulateTyping( 'CKE' );
 
 				expect( getData( model, { withoutSelection: true } ) ).to.equal( '<paragraph>CKEditor</paragraph>' );
 			} );
@@ -190,9 +205,7 @@ describe( 'Text transformation feature', () => {
 			} ).then( () => {
 				setData( model, '<paragraph>[]</paragraph>' );
 
-				model.enqueueChange( model.createBatch(), writer => {
-					writer.insertText( 'user@example.com', doc.selection.focus );
-				} );
+				simulateTyping( 'user@example.com' );
 
 				expect( getData( model, { withoutSelection: true } ) ).to.equal( '<paragraph>user.at.example.com</paragraph>' );
 			} );
@@ -210,9 +223,7 @@ describe( 'Text transformation feature', () => {
 			} ).then( () => {
 				setData( model, '<paragraph>Foo. []</paragraph>' );
 
-				model.enqueueChange( model.createBatch(), writer => {
-					writer.insertText( 'b', doc.selection.focus );
-				} );
+				simulateTyping( 'b' );
 
 				expect( getData( model, { withoutSelection: true } ) ).to.equal( '<paragraph>Foo. B</paragraph>' );
 			} );
@@ -230,15 +241,11 @@ describe( 'Text transformation feature', () => {
 			} ).then( () => {
 				setData( model, '<paragraph>[]</paragraph>' );
 
-				model.change( writer => {
-					writer.insertText( 'CKE', doc.selection.focus );
-				} );
+				simulateTyping( 'CKE' );
 
 				expect( getData( model, { withoutSelection: true } ) ).to.equal( '<paragraph>CKEditor</paragraph>' );
 
-				model.change( writer => {
-					writer.insertText( '(tm)', doc.selection.focus );
-				} );
+				simulateTyping( '(tm)' );
 
 				expect( getData( model, { withoutSelection: true } ) ).to.equal( '<paragraph>CKEditor™</paragraph>' );
 			} );
@@ -256,15 +263,11 @@ describe( 'Text transformation feature', () => {
 			} ).then( () => {
 				setData( model, '<paragraph>[]</paragraph>' );
 
-				model.change( writer => {
-					writer.insertText( 'CKE', doc.selection.focus );
-				} );
+				simulateTyping( 'CKE' );
 
 				expect( getData( model, { withoutSelection: true } ) ).to.equal( '<paragraph>CKEditor</paragraph>' );
 
-				model.change( writer => {
-					writer.insertText( '(tm)', doc.selection.focus );
-				} );
+				simulateTyping( '(tm)' );
 
 				expect( getData( model, { withoutSelection: true } ) ).to.equal( '<paragraph>CKEditor(tm)</paragraph>' );
 			} );
@@ -281,15 +284,11 @@ describe( 'Text transformation feature', () => {
 			} ).then( () => {
 				setData( model, '<paragraph>[]</paragraph>' );
 
-				model.change( writer => {
-					writer.insertText( '(tm)', doc.selection.focus );
-				} );
+				simulateTyping( '(tm)' );
 
 				expect( getData( model, { withoutSelection: true } ) ).to.equal( '<paragraph>(tm)</paragraph>' );
 
-				model.change( writer => {
-					writer.insertText( '(r)', doc.selection.focus );
-				} );
+				simulateTyping( '(r)' );
 
 				expect( getData( model, { withoutSelection: true } ) ).to.equal( '<paragraph>(tm)®</paragraph>' );
 			} );
@@ -306,15 +305,11 @@ describe( 'Text transformation feature', () => {
 			} ).then( () => {
 				setData( model, '<paragraph>[]</paragraph>' );
 
-				model.change( writer => {
-					writer.insertText( '(tm)', doc.selection.focus );
-				} );
+				simulateTyping( '(tm)' );
 
 				expect( getData( model, { withoutSelection: true } ) ).to.equal( '<paragraph>(tm)</paragraph>' );
 
-				model.change( writer => {
-					writer.insertText( '...', doc.selection.focus );
-				} );
+				simulateTyping( '...' );
 
 				expect( getData( model, { withoutSelection: true } ) ).to.equal( '<paragraph>(tm)…</paragraph>' );
 			} );
@@ -340,9 +335,7 @@ describe( 'Text transformation feature', () => {
 			} ).then( () => {
 				setData( model, '<paragraph>[]</paragraph>' );
 
-				model.change( writer => {
-					writer.insertText( '(tm)', doc.selection.focus );
-				} );
+				simulateTyping( '(tm)' );
 
 				expect( getData( model, { withoutSelection: true } ) ).to.equal( '<paragraph>™</paragraph>' );
 			} );
@@ -352,7 +345,7 @@ describe( 'Text transformation feature', () => {
 	function createEditorInstance( additionalConfig = {} ) {
 		return ClassicTestEditor
 			.create( editorElement, Object.assign( {
-				plugins: [ Paragraph, Bold, TextTransformation ]
+				plugins: [ Typing, Paragraph, Bold, TextTransformation ]
 			}, additionalConfig ) )
 			.then( newEditor => {
 				editor = newEditor;
@@ -361,4 +354,12 @@ describe( 'Text transformation feature', () => {
 				doc = model.document;
 			} );
 	}
+
+	function simulateTyping( transformFrom ) {
+		const letters = transformFrom.split( '' );
+
+		for ( const letter of letters ) {
+			editor.execute( 'input', { text: letter } );
+		}
+	}
 } );