Selaa lähdekoodia

Merge pull request #222 from ckeditor/i/5862

Feature: Add `TextWatcher#isEnabled` property to toggle text watching.
Maciej 5 vuotta sitten
vanhempi
commit
46705abfac

+ 1 - 0
packages/ckeditor5-typing/package.json

@@ -27,6 +27,7 @@
     "@ckeditor/ckeditor5-list": "^16.0.0",
     "@ckeditor/ckeditor5-paragraph": "^16.0.0",
     "@ckeditor/ckeditor5-undo": "^16.0.0",
+    "@ckeditor/ckeditor5-code-block": "^16.0.0",
     "eslint": "^5.5.0",
     "eslint-config-ckeditor5": "^2.0.0",
     "husky": "^1.3.1",

+ 24 - 2
packages/ckeditor5-typing/src/texttransformation.js

@@ -92,12 +92,31 @@ export default class TextTransformation extends Plugin {
 				include: DEFAULT_TRANSFORMATIONS
 			}
 		} );
+
+		this.editor = editor;
 	}
 
 	/**
 	 * @inheritDoc
 	 */
 	init() {
+		const model = this.editor.model;
+		const modelSelection = model.document.selection;
+
+		modelSelection.on( 'change:range', () => {
+			// Disable plugin when selection is inside a code block.
+			this.isEnabled = !modelSelection.anchor.parent.is( 'codeBlock' );
+		} );
+
+		this._enableTransformationWatchers();
+	}
+
+	/**
+	 * Create new set of TextWatchers listening to the editor for typing and selection events.
+	 *
+	 * @private
+	 */
+	_enableTransformationWatchers() {
 		const editor = this.editor;
 		const model = editor.model;
 		const input = editor.plugins.get( 'Input' );
@@ -110,7 +129,7 @@ export default class TextTransformation extends Plugin {
 
 			const watcher = new TextWatcher( editor.model, text => from.test( text ) );
 
-			watcher.on( 'matched:data', ( evt, data ) => {
+			const watcherCallback = ( evt, data ) => {
 				if ( !input.isInput( data.batch ) ) {
 					return;
 				}
@@ -142,7 +161,10 @@ export default class TextTransformation extends Plugin {
 						changeIndex += replaceWith.length;
 					}
 				} );
-			} );
+			};
+
+			watcher.on( 'matched:data', watcherCallback );
+			watcher.bind( 'isEnabled' ).to( this );
 		}
 	}
 }

+ 32 - 5
packages/ckeditor5-typing/src/textwatcher.js

@@ -8,7 +8,7 @@
  */
 
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
-import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
+import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
 import getLastTextLine from './utils/getlasttextline';
 
 /**
@@ -19,10 +19,12 @@ import getLastTextLine from './utils/getlasttextline';
  * {@link module:typing/textwatcher~TextWatcher#event:unmatched `unmatched`} events on typing or selection changes.
  *
  * @private
+ * @mixes module:utils/observablemixin~ObservableMixin
  */
 export default class TextWatcher {
 	/**
 	 * Creates a text watcher instance.
+	 *
 	 * @param {module:engine/model/model~Model} model
 	 * @param {Function} testCallback The function used to match the text.
 	 */
@@ -31,6 +33,32 @@ export default class TextWatcher {
 		this.testCallback = testCallback;
 		this.hasMatch = false;
 
+		/**
+		 * Flag indicating whether the `TextWatcher` instance is enabled or disabled.
+		 * A disabled TextWatcher will not evaluate text.
+		 *
+		 * To disable TextWatcher:
+		 *
+		 *		const watcher = new TextWatcher( editor.model, testCallback );
+		 *
+		 *		// After this a testCallback will not be called.
+		 *		watcher.isEnabled = false;
+		 *
+		 * @observable
+		 * @member {Boolean} #isEnabled
+		 */
+		this.set( 'isEnabled', true );
+
+		// Toggle text watching on isEnabled state change.
+		this.on( 'change:isEnabled', () => {
+			if ( this.isEnabled ) {
+				this._startListening();
+			} else {
+				this.stopListening( model.document.selection );
+				this.stopListening( model.document );
+			}
+		} );
+
 		this._startListening();
 	}
 
@@ -43,7 +71,7 @@ export default class TextWatcher {
 		const model = this.model;
 		const document = model.document;
 
-		document.selection.on( 'change:range', ( evt, { directChange } ) => {
+		this.listenTo( document.selection, 'change:range', ( evt, { directChange } ) => {
 			// Indirect changes (i.e. when the user types or external changes are applied) are handled in the document's change event.
 			if ( !directChange ) {
 				return;
@@ -62,7 +90,7 @@ export default class TextWatcher {
 			this._evaluateTextBeforeSelection( 'selection' );
 		} );
 
-		document.on( 'change:data', ( evt, batch ) => {
+		this.listenTo( document, 'change:data', ( evt, batch ) => {
 			if ( batch.type == 'transparent' ) {
 				return;
 			}
@@ -127,5 +155,4 @@ export default class TextWatcher {
 	}
 }
 
-mix( TextWatcher, EmitterMixin );
-
+mix( TextWatcher, ObservableMixin );

+ 32 - 5
packages/ckeditor5-typing/tests/texttransformation.js

@@ -11,6 +11,7 @@ import TextTransformation from '../src/texttransformation';
 import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
+import CodeBlock from '@ckeditor/ckeditor5-code-block/src/codeblock';
 
 describe( 'Text transformation feature', () => {
 	let editorElement, editor, model, doc;
@@ -40,6 +41,24 @@ describe( 'Text transformation feature', () => {
 		} );
 	} );
 
+	describe( '#isEnabled', () => {
+		let plugin;
+
+		beforeEach( () => {
+			return createEditorInstance().then( () => {
+				plugin = editor.plugins.get( TextTransformation );
+			} );
+		} );
+
+		afterEach( () => {
+			plugin.destroy();
+		} );
+
+		it( 'should be enabled after initialization', () => {
+			expect( plugin.isEnabled ).to.be.true;
+		} );
+	} );
+
 	describe( 'transformations', () => {
 		beforeEach( createEditorInstance );
 
@@ -155,6 +174,18 @@ describe( 'Text transformation feature', () => {
 				.to.equal( '<paragraph>"Foo <softBreak></softBreak>“Bar”</paragraph>' );
 		} );
 
+		it( 'should be disabled inside code blocks', () => {
+			setData( model, '<codeBlock language="plaintext">some [] code</codeBlock>' );
+
+			simulateTyping( '1/2' );
+
+			const plugin = editor.plugins.get( 'TextTransformation' );
+
+			expect( plugin.isEnabled ).to.be.false;
+			expect( getData( model, { withoutSelection: true } ) )
+				.to.equal( '<codeBlock language="plaintext">some 1/2 code</codeBlock>' );
+		} );
+
 		function testTransformation( transformFrom, transformTo, textInParagraph = 'A foo' ) {
 			it( `should transform "${ transformFrom }" to "${ transformTo }"`, () => {
 				setData( model, `<paragraph>${ textInParagraph }[]</paragraph>` );
@@ -355,7 +386,7 @@ describe( 'Text transformation feature', () => {
 	function createEditorInstance( additionalConfig = {} ) {
 		return ClassicTestEditor
 			.create( editorElement, Object.assign( {
-				plugins: [ Typing, Paragraph, Bold, TextTransformation ]
+				plugins: [ Typing, Paragraph, Bold, TextTransformation, CodeBlock ]
 			}, additionalConfig ) )
 			.then( newEditor => {
 				editor = newEditor;
@@ -363,10 +394,6 @@ describe( 'Text transformation feature', () => {
 				model = editor.model;
 				doc = model.document;
 
-				model.schema.register( 'softBreak', {
-					allowWhere: '$text',
-					isInline: true
-				} );
 				editor.conversion.elementToElement( {
 					model: 'softBreak',
 					view: 'br'

+ 103 - 1
packages/ckeditor5-typing/tests/textwatcher.js

@@ -46,6 +46,18 @@ describe( 'TextWatcher', () => {
 		}
 	} );
 
+	describe( '#isEnabled', () => {
+		it( 'should be enabled after initialization', () => {
+			expect( watcher.isEnabled ).to.be.true;
+		} );
+
+		it( 'should be disabled after setting #isEnabled to false', () => {
+			watcher.isEnabled = false;
+
+			expect( watcher.isEnabled ).to.be.false;
+		} );
+	} );
+
 	describe( 'testCallback', () => {
 		it( 'should evaluate text before caret for data changes', () => {
 			model.change( writer => {
@@ -97,6 +109,35 @@ describe( 'TextWatcher', () => {
 
 			sinon.assert.notCalled( testCallbackStub );
 		} );
+
+		it( 'should not evaluate text when watcher is disabled', () => {
+			watcher.isEnabled = false;
+
+			model.change( writer => {
+				writer.insertText( '@', doc.selection.getFirstPosition() );
+			} );
+
+			sinon.assert.notCalled( testCallbackStub );
+		} );
+
+		it( 'should evaluate text when watcher is enabled again', () => {
+			watcher.isEnabled = false;
+
+			model.change( writer => {
+				writer.insertText( '@', doc.selection.getFirstPosition() );
+			} );
+
+			sinon.assert.notCalled( testCallbackStub );
+
+			watcher.isEnabled = true;
+
+			model.change( writer => {
+				writer.insertText( '@', doc.selection.getFirstPosition() );
+			} );
+
+			sinon.assert.calledOnce( testCallbackStub );
+			sinon.assert.calledWithExactly( testCallbackStub, 'foo @@' );
+		} );
 	} );
 
 	describe( 'events', () => {
@@ -113,6 +154,22 @@ describe( 'TextWatcher', () => {
 			sinon.assert.notCalled( unmatchedSpy );
 		} );
 
+		it( 'should not fire "matched:data" event when watcher is disabled' +
+		' (even when test callback returns true for model data changes)', () => {
+			watcher.isEnabled = false;
+
+			testCallbackStub.returns( true );
+
+			model.change( writer => {
+				writer.insertText( '@', doc.selection.getFirstPosition() );
+			} );
+
+			sinon.assert.notCalled( testCallbackStub );
+			sinon.assert.notCalled( matchedDataSpy );
+			sinon.assert.notCalled( matchedSelectionSpy );
+			sinon.assert.notCalled( unmatchedSpy );
+		} );
+
 		it( 'should fire "matched:selection" event when test callback returns true for model data changes', () => {
 			testCallbackStub.returns( true );
 
@@ -130,6 +187,26 @@ describe( 'TextWatcher', () => {
 			sinon.assert.notCalled( unmatchedSpy );
 		} );
 
+		it( 'should not fire "matched:selection" event when when watcher is disabled' +
+		' (even when test callback returns true for model data changes)', () => {
+			watcher.isEnabled = false;
+
+			testCallbackStub.returns( true );
+
+			model.enqueueChange( 'transparent', writer => {
+				writer.insertText( '@', doc.selection.getFirstPosition() );
+			} );
+
+			model.change( writer => {
+				writer.setSelection( doc.getRoot().getChild( 0 ), 0 );
+			} );
+
+			sinon.assert.notCalled( testCallbackStub );
+			sinon.assert.notCalled( matchedDataSpy );
+			sinon.assert.notCalled( matchedSelectionSpy );
+			sinon.assert.notCalled( unmatchedSpy );
+		} );
+
 		it( 'should not fire "matched" event when test callback returns false', () => {
 			testCallbackStub.returns( false );
 
@@ -188,6 +265,31 @@ describe( 'TextWatcher', () => {
 			sinon.assert.notCalled( matchedSelectionSpy );
 			sinon.assert.calledOnce( unmatchedSpy );
 		} );
+
+		it( 'should not fire "umatched" event when selection is expanded if watcher is disabled', () => {
+			watcher.isEnabled = false;
+
+			testCallbackStub.returns( true );
+
+			model.change( writer => {
+				writer.insertText( '@', doc.selection.getFirstPosition() );
+			} );
+
+			sinon.assert.notCalled( testCallbackStub );
+			sinon.assert.notCalled( matchedDataSpy );
+			sinon.assert.notCalled( matchedSelectionSpy );
+			sinon.assert.notCalled( unmatchedSpy );
+
+			model.change( writer => {
+				const start = writer.createPositionAt( doc.getRoot().getChild( 0 ), 0 );
+
+				writer.setSelection( writer.createRange( start, start.getShiftedBy( 1 ) ) );
+			} );
+
+			sinon.assert.notCalled( testCallbackStub );
+			sinon.assert.notCalled( matchedDataSpy );
+			sinon.assert.notCalled( matchedSelectionSpy );
+			sinon.assert.notCalled( unmatchedSpy );
+		} );
 	} );
 } );
-