Skip to content

Commit dfaee36

Browse files
authored
Merge pull request #46 from ckeditor/t/42
Fix: Improved performance when editing large content. Debounced the component `#input` event. Closes #42.
2 parents b3a451a + f2bb2ae commit dfaee36

File tree

5 files changed

+93
-22
lines changed

5 files changed

+93
-22
lines changed

Diff for: dist/ckeditor.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: dist/ckeditor.js.map

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: package.json

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"karma-sinon": "^1.0.5",
4343
"karma-sourcemap-loader": "^0.3.7",
4444
"karma-webpack": "^3.0.0",
45+
"lodash-es": "^4.17.11",
4546
"minimist": "^1.2.0",
4647
"mocha": "^5.2.0",
4748
"sinon": "^7.2.3",

Diff for: src/ckeditor.js

+47-13
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55

66
/* global console */
77

8+
import { debounce } from 'lodash-es';
9+
10+
const INPUT_EVENT_DEBOUNCE_WAIT = 300;
11+
812
export default {
913
name: 'ckeditor',
1014

@@ -39,7 +43,12 @@ export default {
3943
return {
4044
// Don't define it in #props because it produces a warning.
4145
// https://vuejs.org/v2/guide/components-props.html#One-Way-Data-Flow
42-
instance: null
46+
instance: null,
47+
48+
$_lastEditorData: {
49+
type: String,
50+
default: ''
51+
}
4352
};
4453
},
4554

@@ -77,14 +86,31 @@ export default {
7786
},
7887

7988
watch: {
80-
// Synchronize changes of #value.
81-
value( val ) {
82-
// If the change is the result of typing, the #value is the same as instance.getData().
83-
// In that case, the change has been triggered by instance.model.document#change:data
84-
// so #value and instance.getData() are already in sync. Executing instance#setData()
85-
// would demolish the selection.
86-
if ( this.instance.getData() !== val ) {
87-
this.instance.setData( val );
89+
value( newValue, oldValue ) {
90+
// Synchronize changes of instance#value. There are two sources of changes:
91+
//
92+
// External value change ------\
93+
// -----> +-----------+
94+
// | Component |
95+
// -----> +-----------+
96+
// Internal data change ------/
97+
// (typing, commands, collaboration)
98+
//
99+
// Case 1: If the change was external (via props), the editor data must be synced with
100+
// the component using instance#setData() and it is OK to destroy the selection.
101+
//
102+
// Case 2: If the change is the result of internal data change, the #value is the same as
103+
// instance#$_lastEditorData, which has been cached on instance#change:data. If we called
104+
// instance#setData() at this point, that would demolish the selection.
105+
//
106+
// To limit the number of instance#setData() which is time-consuming when there is a
107+
// lot of data we make sure:
108+
// * the new value is at least different than the old value (Case 1.)
109+
// * the new value is different than the last internal instance state (Case 2.)
110+
//
111+
// See: https://github.com/ckeditor/ckeditor5-vue/issues/42.
112+
if ( newValue !== oldValue && newValue !== this.$_lastEditorData ) {
113+
this.instance.setData( newValue );
88114
}
89115
},
90116

@@ -97,13 +123,21 @@ export default {
97123
methods: {
98124
$_setUpEditorEvents() {
99125
const editor = this.instance;
100-
101-
editor.model.document.on( 'change:data', evt => {
102-
const data = editor.getData();
126+
const emitInputEvent = evt => {
127+
// Cache the last editor data. This kind of data is a result of typing,
128+
// editor command execution, collaborative changes to the document, etc.
129+
// This data is compared when the component value changes in a 2-way binding.
130+
const data = this.$_lastEditorData = editor.getData();
103131

104132
// The compatibility with the v-model and general Vue.js concept of input–like components.
105133
this.$emit( 'input', data, evt, editor );
106-
} );
134+
};
135+
136+
// Debounce emitting the #input event. When data is huge, instance#getData()
137+
// takes a lot of time to execute on every single key press and ruins the UX.
138+
//
139+
// See: https://github.com/ckeditor/ckeditor5-vue/issues/42
140+
editor.model.document.on( 'change:data', debounce( emitInputEvent, INPUT_EVENT_DEBOUNCE_WAIT ) );
107141

108142
editor.editing.view.document.on( 'focus', evt => {
109143
this.$emit( 'focus', evt, editor );

Diff for: tests/ckeditor.js

+43-7
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ describe( 'CKEditor Component', () => {
3030
} );
3131

3232
it( 'calls editor#create when initializing', done => {
33+
Vue.config.errorHandler = done;
34+
3335
const stub = sandbox.stub( MockEditor, 'create' ).resolves( new MockEditor() );
3436
const { wrapper } = createComponent();
3537

@@ -42,6 +44,8 @@ describe( 'CKEditor Component', () => {
4244
} );
4345

4446
it( 'calls editor#destroy when destroying', done => {
47+
Vue.config.errorHandler = done;
48+
4549
const stub = sandbox.stub( MockEditor.prototype, 'destroy' ).resolves();
4650
const { wrapper, vm } = createComponent();
4751

@@ -55,6 +59,8 @@ describe( 'CKEditor Component', () => {
5559
} );
5660

5761
it( 'passes editor promise rejection error to console.error', done => {
62+
Vue.config.errorHandler = done;
63+
5864
const error = new Error( 'Something went wrong.' );
5965
const consoleErrorStub = sandbox.stub( console, 'error' );
6066

@@ -75,6 +81,8 @@ describe( 'CKEditor Component', () => {
7581
describe( 'properties', () => {
7682
it( '#editor', () => {
7783
it( 'accepts a string', done => {
84+
Vue.config.errorHandler = done;
85+
7886
expect( vm.editor ).to.equal( 'classic' );
7987

8088
Vue.nextTick( () => {
@@ -85,6 +93,8 @@ describe( 'CKEditor Component', () => {
8593
} );
8694

8795
it( 'accepts an editor constructor', done => {
96+
Vue.config.errorHandler = done;
97+
8898
const { wrapper, vm } = createComponent( {
8999
editor: MockEditor
90100
} );
@@ -105,6 +115,8 @@ describe( 'CKEditor Component', () => {
105115
} );
106116

107117
it( 'should set the initial data', done => {
118+
Vue.config.errorHandler = done;
119+
108120
const setDataStub = sandbox.stub( MockEditor.prototype, 'setData' );
109121
const { wrapper } = createComponent( {
110122
value: 'foo'
@@ -141,6 +153,8 @@ describe( 'CKEditor Component', () => {
141153
} );
142154

143155
it( 'should set the initial editor#isReadOnly', done => {
156+
Vue.config.errorHandler = done;
157+
144158
const { wrapper, vm } = createComponent( {
145159
disabled: true
146160
} );
@@ -159,6 +173,8 @@ describe( 'CKEditor Component', () => {
159173
} );
160174

161175
it( 'should set the initial editor#config', done => {
176+
Vue.config.errorHandler = done;
177+
162178
const { wrapper, vm } = createComponent( {
163179
config: { foo: 'bar' }
164180
} );
@@ -172,6 +188,8 @@ describe( 'CKEditor Component', () => {
172188
} );
173189

174190
it( '#instance should be defined', done => {
191+
Vue.config.errorHandler = done;
192+
175193
Vue.nextTick( () => {
176194
expect( vm.instance ).to.be.instanceOf( MockEditor );
177195

@@ -182,6 +200,8 @@ describe( 'CKEditor Component', () => {
182200

183201
describe( 'bindings', () => {
184202
it( '#disabled should control editor#isReadOnly', done => {
203+
Vue.config.errorHandler = done;
204+
185205
const { wrapper, vm } = createComponent( {
186206
disabled: true
187207
} );
@@ -198,18 +218,22 @@ describe( 'CKEditor Component', () => {
198218
} );
199219

200220
it( '#value should trigger editor#setData', done => {
221+
Vue.config.errorHandler = done;
222+
201223
Vue.nextTick( () => {
202224
const spy = sandbox.spy( vm.instance, 'setData' );
203225

204226
wrapper.setProps( { value: 'foo' } );
205227
wrapper.setProps( { value: 'bar' } );
206228
wrapper.setProps( { value: 'bar' } );
207229

230+
sinon.assert.calledTwice( spy );
231+
208232
// Simulate typing: The #value changes but at the same time, the instance update
209233
// its own data so instance.getData() and #value are immediately the same.
210234
// Make sure instance.setData() is not called in this situation because it would destroy
211235
// the selection.
212-
sandbox.stub( vm.instance, 'getData' ).returns( 'barq' );
236+
wrapper.vm.$_lastEditorData = 'barq';
213237
wrapper.setProps( { value: 'barq' } );
214238

215239
sinon.assert.calledTwice( spy );
@@ -223,6 +247,8 @@ describe( 'CKEditor Component', () => {
223247

224248
describe( 'events', () => {
225249
it( 'emits #ready when editor is created', done => {
250+
Vue.config.errorHandler = done;
251+
226252
Vue.nextTick( () => {
227253
expect( wrapper.emitted().ready.length ).to.equal( 1 );
228254
expect( wrapper.emitted().ready[ 0 ] ).to.deep.equal( [ vm.instance ] );
@@ -232,6 +258,8 @@ describe( 'CKEditor Component', () => {
232258
} );
233259

234260
it( 'emits #destroy when editor is destroyed', done => {
261+
Vue.config.errorHandler = done;
262+
235263
const { wrapper, vm } = createComponent();
236264

237265
Vue.nextTick( () => {
@@ -244,7 +272,9 @@ describe( 'CKEditor Component', () => {
244272
} );
245273
} );
246274

247-
it( 'emits #input when editor data changes', done => {
275+
it( 'emits debounced #input when editor data changes', done => {
276+
Vue.config.errorHandler = done;
277+
248278
sandbox.stub( ModelDocument.prototype, 'on' );
249279
sandbox.stub( MockEditor.prototype, 'getData' ).returns( 'foo' );
250280

@@ -260,16 +290,20 @@ describe( 'CKEditor Component', () => {
260290

261291
on.firstCall.args[ 1 ]( evtStub );
262292

263-
expect( wrapper.emitted().input.length ).to.equal( 1 );
264-
expect( wrapper.emitted().input[ 0 ] ).to.deep.equal( [
265-
'foo', evtStub, vm.instance
266-
] );
293+
setTimeout( () => {
294+
expect( wrapper.emitted().input.length ).to.equal( 1 );
295+
expect( wrapper.emitted().input[ 0 ] ).to.deep.equal( [
296+
'foo', evtStub, vm.instance
297+
] );
267298

268-
done();
299+
done();
300+
}, 350 );
269301
} );
270302
} );
271303

272304
it( 'emits #focus when editor editable is focused', done => {
305+
Vue.config.errorHandler = done;
306+
273307
sandbox.stub( ViewlDocument.prototype, 'on' );
274308

275309
Vue.nextTick( () => {
@@ -294,6 +328,8 @@ describe( 'CKEditor Component', () => {
294328
} );
295329

296330
it( 'emits #blur when editor editable is focused', done => {
331+
Vue.config.errorHandler = done;
332+
297333
sandbox.stub( ViewlDocument.prototype, 'on' );
298334

299335
Vue.nextTick( () => {

0 commit comments

Comments
 (0)