Skip to content
This repository was archived by the owner on Jun 26, 2020. It is now read-only.

Commit fa79d00

Browse files
authored
Merge pull request #222 from ckeditor/i/5862
Feature: Add `TextWatcher#isEnabled` property to toggle text watching.
2 parents 139989c + 0a889f2 commit fa79d00

File tree

5 files changed

+192
-13
lines changed

5 files changed

+192
-13
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"@ckeditor/ckeditor5-list": "^16.0.0",
2828
"@ckeditor/ckeditor5-paragraph": "^16.0.0",
2929
"@ckeditor/ckeditor5-undo": "^16.0.0",
30+
"@ckeditor/ckeditor5-code-block": "^16.0.0",
3031
"eslint": "^5.5.0",
3132
"eslint-config-ckeditor5": "^2.0.0",
3233
"husky": "^1.3.1",

src/texttransformation.js

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,31 @@ export default class TextTransformation extends Plugin {
9292
include: DEFAULT_TRANSFORMATIONS
9393
}
9494
} );
95+
96+
this.editor = editor;
9597
}
9698

9799
/**
98100
* @inheritDoc
99101
*/
100102
init() {
103+
const model = this.editor.model;
104+
const modelSelection = model.document.selection;
105+
106+
modelSelection.on( 'change:range', () => {
107+
// Disable plugin when selection is inside a code block.
108+
this.isEnabled = !modelSelection.anchor.parent.is( 'codeBlock' );
109+
} );
110+
111+
this._enableTransformationWatchers();
112+
}
113+
114+
/**
115+
* Create new set of TextWatchers listening to the editor for typing and selection events.
116+
*
117+
* @private
118+
*/
119+
_enableTransformationWatchers() {
101120
const editor = this.editor;
102121
const model = editor.model;
103122
const input = editor.plugins.get( 'Input' );
@@ -110,7 +129,7 @@ export default class TextTransformation extends Plugin {
110129

111130
const watcher = new TextWatcher( editor.model, text => from.test( text ) );
112131

113-
watcher.on( 'matched:data', ( evt, data ) => {
132+
const watcherCallback = ( evt, data ) => {
114133
if ( !input.isInput( data.batch ) ) {
115134
return;
116135
}
@@ -142,7 +161,10 @@ export default class TextTransformation extends Plugin {
142161
changeIndex += replaceWith.length;
143162
}
144163
} );
145-
} );
164+
};
165+
166+
watcher.on( 'matched:data', watcherCallback );
167+
watcher.bind( 'isEnabled' ).to( this );
146168
}
147169
}
148170
}

src/textwatcher.js

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99

1010
import mix from '@ckeditor/ckeditor5-utils/src/mix';
11-
import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
11+
import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
1212
import getLastTextLine from './utils/getlasttextline';
1313

1414
/**
@@ -19,10 +19,12 @@ import getLastTextLine from './utils/getlasttextline';
1919
* {@link module:typing/textwatcher~TextWatcher#event:unmatched `unmatched`} events on typing or selection changes.
2020
*
2121
* @private
22+
* @mixes module:utils/observablemixin~ObservableMixin
2223
*/
2324
export default class TextWatcher {
2425
/**
2526
* Creates a text watcher instance.
27+
*
2628
* @param {module:engine/model/model~Model} model
2729
* @param {Function} testCallback The function used to match the text.
2830
*/
@@ -31,6 +33,32 @@ export default class TextWatcher {
3133
this.testCallback = testCallback;
3234
this.hasMatch = false;
3335

36+
/**
37+
* Flag indicating whether the `TextWatcher` instance is enabled or disabled.
38+
* A disabled TextWatcher will not evaluate text.
39+
*
40+
* To disable TextWatcher:
41+
*
42+
* const watcher = new TextWatcher( editor.model, testCallback );
43+
*
44+
* // After this a testCallback will not be called.
45+
* watcher.isEnabled = false;
46+
*
47+
* @observable
48+
* @member {Boolean} #isEnabled
49+
*/
50+
this.set( 'isEnabled', true );
51+
52+
// Toggle text watching on isEnabled state change.
53+
this.on( 'change:isEnabled', () => {
54+
if ( this.isEnabled ) {
55+
this._startListening();
56+
} else {
57+
this.stopListening( model.document.selection );
58+
this.stopListening( model.document );
59+
}
60+
} );
61+
3462
this._startListening();
3563
}
3664

@@ -43,7 +71,7 @@ export default class TextWatcher {
4371
const model = this.model;
4472
const document = model.document;
4573

46-
document.selection.on( 'change:range', ( evt, { directChange } ) => {
74+
this.listenTo( document.selection, 'change:range', ( evt, { directChange } ) => {
4775
// Indirect changes (i.e. when the user types or external changes are applied) are handled in the document's change event.
4876
if ( !directChange ) {
4977
return;
@@ -62,7 +90,7 @@ export default class TextWatcher {
6290
this._evaluateTextBeforeSelection( 'selection' );
6391
} );
6492

65-
document.on( 'change:data', ( evt, batch ) => {
93+
this.listenTo( document, 'change:data', ( evt, batch ) => {
6694
if ( batch.type == 'transparent' ) {
6795
return;
6896
}
@@ -127,5 +155,4 @@ export default class TextWatcher {
127155
}
128156
}
129157

130-
mix( TextWatcher, EmitterMixin );
131-
158+
mix( TextWatcher, ObservableMixin );

tests/texttransformation.js

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import TextTransformation from '../src/texttransformation';
1111
import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
1212
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
1313
import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
14+
import CodeBlock from '@ckeditor/ckeditor5-code-block/src/codeblock';
1415

1516
describe( 'Text transformation feature', () => {
1617
let editorElement, editor, model, doc;
@@ -40,6 +41,24 @@ describe( 'Text transformation feature', () => {
4041
} );
4142
} );
4243

44+
describe( '#isEnabled', () => {
45+
let plugin;
46+
47+
beforeEach( () => {
48+
return createEditorInstance().then( () => {
49+
plugin = editor.plugins.get( TextTransformation );
50+
} );
51+
} );
52+
53+
afterEach( () => {
54+
plugin.destroy();
55+
} );
56+
57+
it( 'should be enabled after initialization', () => {
58+
expect( plugin.isEnabled ).to.be.true;
59+
} );
60+
} );
61+
4362
describe( 'transformations', () => {
4463
beforeEach( createEditorInstance );
4564

@@ -155,6 +174,18 @@ describe( 'Text transformation feature', () => {
155174
.to.equal( '<paragraph>"Foo <softBreak></softBreak>“Bar”</paragraph>' );
156175
} );
157176

177+
it( 'should be disabled inside code blocks', () => {
178+
setData( model, '<codeBlock language="plaintext">some [] code</codeBlock>' );
179+
180+
simulateTyping( '1/2' );
181+
182+
const plugin = editor.plugins.get( 'TextTransformation' );
183+
184+
expect( plugin.isEnabled ).to.be.false;
185+
expect( getData( model, { withoutSelection: true } ) )
186+
.to.equal( '<codeBlock language="plaintext">some 1/2 code</codeBlock>' );
187+
} );
188+
158189
function testTransformation( transformFrom, transformTo, textInParagraph = 'A foo' ) {
159190
it( `should transform "${ transformFrom }" to "${ transformTo }"`, () => {
160191
setData( model, `<paragraph>${ textInParagraph }[]</paragraph>` );
@@ -355,18 +386,14 @@ describe( 'Text transformation feature', () => {
355386
function createEditorInstance( additionalConfig = {} ) {
356387
return ClassicTestEditor
357388
.create( editorElement, Object.assign( {
358-
plugins: [ Typing, Paragraph, Bold, TextTransformation ]
389+
plugins: [ Typing, Paragraph, Bold, TextTransformation, CodeBlock ]
359390
}, additionalConfig ) )
360391
.then( newEditor => {
361392
editor = newEditor;
362393

363394
model = editor.model;
364395
doc = model.document;
365396

366-
model.schema.register( 'softBreak', {
367-
allowWhere: '$text',
368-
isInline: true
369-
} );
370397
editor.conversion.elementToElement( {
371398
model: 'softBreak',
372399
view: 'br'

tests/textwatcher.js

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,18 @@ describe( 'TextWatcher', () => {
4646
}
4747
} );
4848

49+
describe( '#isEnabled', () => {
50+
it( 'should be enabled after initialization', () => {
51+
expect( watcher.isEnabled ).to.be.true;
52+
} );
53+
54+
it( 'should be disabled after setting #isEnabled to false', () => {
55+
watcher.isEnabled = false;
56+
57+
expect( watcher.isEnabled ).to.be.false;
58+
} );
59+
} );
60+
4961
describe( 'testCallback', () => {
5062
it( 'should evaluate text before caret for data changes', () => {
5163
model.change( writer => {
@@ -97,6 +109,35 @@ describe( 'TextWatcher', () => {
97109

98110
sinon.assert.notCalled( testCallbackStub );
99111
} );
112+
113+
it( 'should not evaluate text when watcher is disabled', () => {
114+
watcher.isEnabled = false;
115+
116+
model.change( writer => {
117+
writer.insertText( '@', doc.selection.getFirstPosition() );
118+
} );
119+
120+
sinon.assert.notCalled( testCallbackStub );
121+
} );
122+
123+
it( 'should evaluate text when watcher is enabled again', () => {
124+
watcher.isEnabled = false;
125+
126+
model.change( writer => {
127+
writer.insertText( '@', doc.selection.getFirstPosition() );
128+
} );
129+
130+
sinon.assert.notCalled( testCallbackStub );
131+
132+
watcher.isEnabled = true;
133+
134+
model.change( writer => {
135+
writer.insertText( '@', doc.selection.getFirstPosition() );
136+
} );
137+
138+
sinon.assert.calledOnce( testCallbackStub );
139+
sinon.assert.calledWithExactly( testCallbackStub, 'foo @@' );
140+
} );
100141
} );
101142

102143
describe( 'events', () => {
@@ -113,6 +154,22 @@ describe( 'TextWatcher', () => {
113154
sinon.assert.notCalled( unmatchedSpy );
114155
} );
115156

157+
it( 'should not fire "matched:data" event when watcher is disabled' +
158+
' (even when test callback returns true for model data changes)', () => {
159+
watcher.isEnabled = false;
160+
161+
testCallbackStub.returns( true );
162+
163+
model.change( writer => {
164+
writer.insertText( '@', doc.selection.getFirstPosition() );
165+
} );
166+
167+
sinon.assert.notCalled( testCallbackStub );
168+
sinon.assert.notCalled( matchedDataSpy );
169+
sinon.assert.notCalled( matchedSelectionSpy );
170+
sinon.assert.notCalled( unmatchedSpy );
171+
} );
172+
116173
it( 'should fire "matched:selection" event when test callback returns true for model data changes', () => {
117174
testCallbackStub.returns( true );
118175

@@ -130,6 +187,26 @@ describe( 'TextWatcher', () => {
130187
sinon.assert.notCalled( unmatchedSpy );
131188
} );
132189

190+
it( 'should not fire "matched:selection" event when when watcher is disabled' +
191+
' (even when test callback returns true for model data changes)', () => {
192+
watcher.isEnabled = false;
193+
194+
testCallbackStub.returns( true );
195+
196+
model.enqueueChange( 'transparent', writer => {
197+
writer.insertText( '@', doc.selection.getFirstPosition() );
198+
} );
199+
200+
model.change( writer => {
201+
writer.setSelection( doc.getRoot().getChild( 0 ), 0 );
202+
} );
203+
204+
sinon.assert.notCalled( testCallbackStub );
205+
sinon.assert.notCalled( matchedDataSpy );
206+
sinon.assert.notCalled( matchedSelectionSpy );
207+
sinon.assert.notCalled( unmatchedSpy );
208+
} );
209+
133210
it( 'should not fire "matched" event when test callback returns false', () => {
134211
testCallbackStub.returns( false );
135212

@@ -188,6 +265,31 @@ describe( 'TextWatcher', () => {
188265
sinon.assert.notCalled( matchedSelectionSpy );
189266
sinon.assert.calledOnce( unmatchedSpy );
190267
} );
268+
269+
it( 'should not fire "umatched" event when selection is expanded if watcher is disabled', () => {
270+
watcher.isEnabled = false;
271+
272+
testCallbackStub.returns( true );
273+
274+
model.change( writer => {
275+
writer.insertText( '@', doc.selection.getFirstPosition() );
276+
} );
277+
278+
sinon.assert.notCalled( testCallbackStub );
279+
sinon.assert.notCalled( matchedDataSpy );
280+
sinon.assert.notCalled( matchedSelectionSpy );
281+
sinon.assert.notCalled( unmatchedSpy );
282+
283+
model.change( writer => {
284+
const start = writer.createPositionAt( doc.getRoot().getChild( 0 ), 0 );
285+
286+
writer.setSelection( writer.createRange( start, start.getShiftedBy( 1 ) ) );
287+
} );
288+
289+
sinon.assert.notCalled( testCallbackStub );
290+
sinon.assert.notCalled( matchedDataSpy );
291+
sinon.assert.notCalled( matchedSelectionSpy );
292+
sinon.assert.notCalled( unmatchedSpy );
293+
} );
191294
} );
192295
} );
193-

0 commit comments

Comments
 (0)