Skip to content

Commit

Permalink
Merge pull request #1996 from ckeditor/t/1993
Browse files Browse the repository at this point in the history
Added throttling function
  • Loading branch information
mlewand authored Jun 1, 2018
2 parents 7211a96 + 2d2bea6 commit 77470cc
Show file tree
Hide file tree
Showing 5 changed files with 247 additions and 20 deletions.
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ API Changes:
* [#1712](https://github.com/ckeditor/ckeditor-dev/issues/1712): [`extraPlugins`](https://docs.ckeditor.com/ckeditor4/latest/api/CKEDITOR_config.html#cfg-extraPlugins), [`removePlugins`](https://docs.ckeditor.com/ckeditor4/latest/api/CKEDITOR_config.html#cfg-removePlugins) and [`plugins`](https://docs.ckeditor.com/ckeditor4/latest/api/CKEDITOR_config.html#cfg-plugins) configuration options allow whitespace.
* [#1724](https://github.com/ckeditor/ckeditor-dev/issues/1724): Added option to [`getClientRect`](https://docs.ckeditor.com/ckeditor4/latest/api/CKEDITOR_dom_element.html#method-getClientRect) function allowing to retrieve an absolute bounding rectangle of the element i.e. position relative to the upper-left corner of the topmost viewport.
* [#1498](https://github.com/ckeditor/ckeditor-dev/issues/1498) : Added new method 'getClientRects()' to CKEDITOR.dom.range, which returns list of rects for each selected element.
* [#1993](https://github.com/ckeditor/ckeditor-dev/issues/1993): Added [`CKEDITOR.tools.throttle`](http://docs.ckeditor.com/ckeditor4/docs/#!/api/CKEDITOR_tools.html#method-throttle) function.

## CKEditor 4.9.2

Expand Down
132 changes: 113 additions & 19 deletions core/tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,95 @@
}, milliseconds || 0 );
},

/**
* Returns a wrapper that exposes an `input` function, which acts as a proxy to the given `output` function, providing a throttling.
* This proxy guarantees that the `output` function is not called more often than `minInterval`.
*
* If multiple calls occur within a single `minInterval` time, the most recent `input` call with its arguments will be used to schedule
* next `output` call, and the previous throttled calls will be discarded.
*
* The first `input` call is always executed asynchronously which means that `output` call will be executed immediately.
*
* ```javascript
* var buffer = CKEDITOR.tools.throttle( 200, function( message ) {
* console.log( message );
* } );
*
* buffer.input( 'foo!' );
* // 'foo!' logged immediately.
* buffer.input( 'bar!' );
* // Nothing logged.
* buffer.input( 'baz!' );
* // Nothing logged.
* // … after 200ms a single 'baz!' will be logged.
* ```
*
* Can be easily used with events:
*
* ```javascript
* var buffer = CKEDITOR.tools.throttle( 200, function( evt ) {
* console.log( evt.data.text );
* } );
*
* editor.on( 'key', buffer.input );
* // Note: There is no need to bind buffer as a context.
* ```
*
* @since 4.10.0
* @param {Number} minInterval Minimum interval between `output` calls in milliseconds.
* @param {Function} output Function that will be executed as `output`.
* @param {Object} [contextObj] The object used to context the listener call (the `this` object).
* @returns {Object}
* @returns {Function} return.input Buffer's input method.
* Accepts parameters which will be directly passed into `output` function.
* @returns {Function} return.reset Resets buffered calls — `output` will not be executed
* until next `input` is triggered.
*/
throttle: function( minInterval, output, contextObj ) {
var scheduled,
lastOutput = 0;

contextObj = contextObj || {};

return {
input: input,
reset: reset
};

function input() {
var args = Array.prototype.slice.call( arguments );

if ( scheduled ) {
clearTimeout( scheduled );
scheduled = 0;
}

var diff = ( new Date() ).getTime() - lastOutput;

// If less than minInterval passed after last check,
// schedule next for minInterval after previous one.
if ( diff < minInterval ) {
scheduled = setTimeout( triggerOutput, minInterval - diff );
} else {
triggerOutput();
}

function triggerOutput() {
lastOutput = ( new Date() ).getTime();
scheduled = false;

output.apply( contextObj, args );
}
}

function reset() {
if ( scheduled ) {
clearTimeout( scheduled );
scheduled = lastOutput = 0;
}
}
},

/**
* Removes spaces from the start and the end of a string. The following
* characters are removed: space, tab, line break, line feed.
Expand Down Expand Up @@ -1189,45 +1278,50 @@
* Buffers `input` events (or any `input` calls)
* and triggers `output` not more often than once per `minInterval`.
*
* var buffer = CKEDITOR.tools.eventsBuffer( 200, function() {
* console.log( 'foo!' );
* } );
* ```javascript
* var buffer = CKEDITOR.tools.eventsBuffer( 200, function() {
* console.log( 'foo!' );
* } );
*
* buffer.input();
* // 'foo!' logged immediately.
* buffer.input();
* // Nothing logged.
* buffer.input();
* // Nothing logged.
* // ... after 200ms a single 'foo!' will be logged.
* buffer.input();
* // 'foo!' logged immediately.
* buffer.input();
* // Nothing logged.
* buffer.input();
* // Nothing logged.
* // … after 200ms a single 'foo!' will be logged.
* ```
*
* Can be easily used with events:
*
* var buffer = CKEDITOR.tools.eventsBuffer( 200, function() {
* console.log( 'foo!' );
* } );
* ```javascript
* var buffer = CKEDITOR.tools.eventsBuffer( 200, function() {
* console.log( 'foo!' );
* } );
*
* editor.on( 'key', buffer.input );
* // Note: There is no need to bind buffer as a context.
* ```
*
* editor.on( 'key', buffer.input );
* // Note: There is no need to bind buffer as a context.
*
* @since 4.2.1
* @param {Number} minInterval Minimum interval between `output` calls in milliseconds.
* @param {Function} output Function that will be executed as `output`.
* @param {Object} [scopeObj] The object used to scope the listener call (the `this` object).
* @param {Object} [contextObj] The object used to context the listener call (the `this` object).
* @returns {Object}
* @returns {Function} return.input Buffer's input method.
* @returns {Function} return.reset Resets buffered events &mdash; `output` will not be executed
* until next `input` is triggered.
*/
eventsBuffer: function( minInterval, output, scopeObj ) {
eventsBuffer: function( minInterval, output, contextObj ) {
var scheduled,
lastOutput = 0;

function triggerOutput() {
lastOutput = ( new Date() ).getTime();
scheduled = false;
if ( scopeObj ) {
output.call( scopeObj );
if ( contextObj ) {
output.call( contextObj );
} else {
output();
}
Expand Down
98 changes: 97 additions & 1 deletion tests/core/tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -602,7 +602,7 @@
}, 110 );
},

'test eventsBuffer contex': function() {
'test eventsBuffer context': function() {
var spy = sinon.spy(),
ctxObj = {},
buffer = CKEDITOR.tools.eventsBuffer( 100, spy, ctxObj );
Expand All @@ -628,6 +628,102 @@
assert.areSame( 'ABcDeF', c( 'aBcDeF', true ) );
},

'test throttle': function() {
var foo = 'foo',
baz = 'baz',
inputSpy = sinon.spy(),
buffer = CKEDITOR.tools.throttle( 200, inputSpy );

buffer.input( foo );

assert.areSame( 1, inputSpy.callCount, 'Call count after the first call' );
assert.isTrue( inputSpy.calledWithExactly( foo ), 'Call argument after the first call' );

buffer.input( baz );

assert.areSame( 1, inputSpy.callCount, 'Call count after the second call' );
assert.isTrue( inputSpy.calledWithExactly( foo ), 'Call argument the after second call' );

wait( function() {
assert.areSame( 1, inputSpy.callCount, 'Call count after the second call timeout (1st)' );
assert.isTrue( inputSpy.calledWithExactly( foo ), 'Call argument after the second call timeout (1st)' );

wait( function() {
assert.areSame( 2, inputSpy.callCount, 'Call count after the second call timeout (2nd)' );
assert.isTrue( inputSpy.getCall( 1 ).calledWithExactly( baz ), 'Call argument after the second call timeout (2nd)' );

buffer.input( foo );

wait( function() {
assert.areSame( 3, inputSpy.callCount, 'Call count after the third call' );
assert.isTrue( inputSpy.getCall( 2 ).calledWithExactly( foo ), 'Call argument after the third call' );

// Check that input triggered after 70ms from previous
// buffer.input will trigger output after next 140ms (200-70).
wait( function() {
buffer.input( baz );

assert.areSame( 3, inputSpy.callCount, 'Call count after the fourth call' );

wait( function() {
assert.areSame( 4, inputSpy.callCount, 'Call count after the fourth call timeout' );
assert.isTrue( inputSpy.getCall( 3 ).calledWithExactly( baz ), 'Call argument after the fourth call timeout' );
}, 140 );
}, 70 );
}, 210 );
}, 110 );
}, 100 );
},

'test throttle always uses the most recent argument': function() {
var input = sinon.stub(),
buffer = CKEDITOR.tools.throttle( 50, input );

buffer.input( 'first' );

assert.areSame( 1, input.callCount, 'Call count after the first call' );
sinon.assert.calledWithExactly( input.getCall( 0 ), 'first' );

buffer.input( 'second' );

buffer.input( 'third' );

wait( function() {
assert.areSame( 2, input.callCount, 'Call count after the timeout' );
sinon.assert.calledWithExactly( input.getCall( 1 ), 'third' );
}, 100 );
},

'test throttle.reset': function() {
var inputSpy = sinon.spy(),
buffer = CKEDITOR.tools.throttle( 100, inputSpy );

assert.areSame( 0, inputSpy.callCount, 'Initial call count' );

buffer.input();

assert.areSame( 1, inputSpy.callCount, 'Call count after the first call' );

buffer.input();
buffer.reset();

assert.areSame( 1, inputSpy.callCount, 'Call count after reset' );

buffer.input();

assert.areSame( 2, inputSpy.callCount, 'Call count after the second call' );
},

'test throttle context': function() {
var spy = sinon.spy(),
ctxObj = {},
buffer = CKEDITOR.tools.throttle( 100, spy, ctxObj );

buffer.input();

assert.areSame( ctxObj, spy.getCall( 0 ).thisValue, 'callback was executed with the right context' );
},

'test checkIfAnyObjectPropertyMatches': function() {
var c = CKEDITOR.tools.checkIfAnyObjectPropertyMatches,
r1 = /foo/,
Expand Down
21 changes: 21 additions & 0 deletions tests/core/tools/manual/throttle.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<style>
.red {
background-color: red;
}
.blue {
background-color: blue;
}
</style>

<button id="btn" class="red" >Click me!</button>

<script>
var button = document.getElementById( 'btn' ),
buffer = CKEDITOR.tools.throttle( 2000, function ( css ) {
button.className = css === 'red' ? 'blue' : 'red';
} );

button.addEventListener( 'click', function() {
buffer.input( button.className );
} );
</script>
15 changes: 15 additions & 0 deletions tests/core/tools/manual/throttle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
@bender-tags: feature, 4.10.0, 1993
@bender-ui: collapsed
@bender-ckeditor-plugins: toolbar, wysiwygarea

1. Click button `Click me` five times as fast as possible.

## Expected

Button changes its color:
1. After first click to blue.
1. After last click to red.

## Unexpected

Button changes its color in invalid order.

0 comments on commit 77470cc

Please sign in to comment.