diff --git a/Docs/Types/Function.md b/Docs/Types/Function.md index 9c46fa4cd..82511c7fd 100644 --- a/Docs/Types/Function.md +++ b/Docs/Types/Function.md @@ -368,6 +368,34 @@ Executes a function in the specified intervals of time. Periodic execution can b - [MDN setInterval][], [MDN clearInterval][] +Function: Function.debounce {#Function:Function-debounce} +--------------------------------------------------------- + +This method will return a new function that will be called only once per group of close calls. After a defined delay it will be able to be called again. + +### Syntax: + + var debounceFn = myFn.debounce(delay, leading); + +### Arguments: + +1. delay - (*number*, optional, defaults to 250ms) The delay to wait before a call to the debounced function can happen again. +2. leading - (*boolean*, optional, defaults to false) If the call to the debounced function should happen in leading phase of group of calls or after. + +### Returns: + +* (*function*) A debounce function that will be called only once per group of close function calls. + +### Examples: + + // get scroll position after scroll has stopped + var getNewScrollPosition = function () { + var scroll = window.getScroll(); + alert(scroll.y); + } + window.addEvent('scroll', getNewScrollPosition.debounce(500)); + + Deprecated Functions {#Deprecated-Functions} ============================================ diff --git a/Source/Types/Function.js b/Source/Types/Function.js index 10de57e54..b0d033b0a 100644 --- a/Source/Types/Function.js +++ b/Source/Types/Function.js @@ -72,6 +72,35 @@ Function.implement({ periodical: function(periodical, bind, args){ return setInterval(this.pass((args == null ? [] : args), bind), periodical); + }, + + debounce: function(delay, leading){ + + // in case delay is omitted and `leading` is first argument + if (typeof delay == 'boolean'){ + leading = delay; + delay = false; + } + + var timeout, args, self, + fn = this, + callNow = leading; + + var later = function(){ + if (leading) callNow = true; + else fn.apply(self, args); + timeout = null; + }; + + return function(){ + self = this; + args = arguments; + + clearTimeout(timeout); + timeout = setTimeout(later, delay || 250); + if (callNow) fn.apply(self, args); + callNow = false; + }; } }); diff --git a/Specs/Types/Function.js b/Specs/Types/Function.js index 2e6ecfab0..395897976 100644 --- a/Specs/Types/Function.js +++ b/Specs/Types/Function.js @@ -460,3 +460,136 @@ describe('Function.periodical', function(){ }); }); + +describe('Debounce', function(){ + var periodical, + counter = 0, + debounceCalls = 0; + + // spy, to count when original fn was called + function targetFn(){ + debounceCalls++; + } + + // call function every 10ms + function caller(debounceFn, cb){ + periodical = setInterval(function(){ + counter++; + debounceFn(); + }, 10); + } + + beforeEach(function(){ + expect(counter).to.equal(0); + expect(debounceCalls).to.equal(0); + }); + + afterEach(function(){ + counter = debounceCalls = 0; + clearInterval(periodical); + }); + + it('should debounce with default values', function(done){ + var debounceFn = targetFn.debounce(); + caller(debounceFn); + + var firstCheck = false; + var wait = setInterval(function(){ + // keep calling for 400ms, no call should be done + if (!firstCheck && counter > 40){ + clearInterval(periodical); + expect(debounceCalls).to.equal(0); + firstCheck = true; + } + // wait for debouced call to come + if (firstCheck && debounceCalls > 0){ + clearInterval(wait); + done(); + } + }, 10); + }); + + it('should debounce early', function(done){ + var debounceFn = targetFn.debounce(100, true), + time = 0, + firstCheck; + caller(debounceFn); + var wait = setInterval(function(){ + time++; + // there should already be a function called + if (counter > 5 && !firstCheck){ + clearInterval(periodical); + expect(debounceCalls).to.equal(1); + firstCheck = true; + } + // no more debounced call should be done + if (time > 40){ + expect(debounceCalls).to.equal(1); + clearInterval(wait); + done(); + } + }, 10); + }); + + it('should debounce early when `leading` is passed alone', function(done){ + var debounceFn = targetFn.debounce(true), + time = 0, + firstCheck; + caller(debounceFn); + var wait = setInterval(function(){ + time++; + // there should already be a function called + if (counter > 5 && !firstCheck){ + clearInterval(periodical); + expect(debounceCalls).to.equal(1); + firstCheck = true; + } + // no more debounced call should be done + if (time > 40){ + expect(debounceCalls).to.equal(1); + clearInterval(wait); + done(); + } + }, 10); + }); + + it('should debounce late', function(done){ + var debounceFn = targetFn.debounce(150); + caller(debounceFn); + var time = 0; + var firstCheck; + var wait = setInterval(function(){ + time++; + // no early calls + if (counter > 5 && !firstCheck){ + clearInterval(periodical); + expect(debounceCalls).to.equal(0); + firstCheck = true; + } + // should have been called after + if (time > 30){ + expect(debounceCalls).to.equal(1); + clearInterval(wait); + done(); + } + }, 10); + }); + + it('should have the right context', function(done){ + var context = {}; + var _context; + var fn = function(){ + _context = this; + }.bind(context); + var debounceFn = fn.debounce(); + debounceFn(); // trigger the function, once is enough + var wait = setInterval(function(){ + if (_context){ + clearInterval(wait); + expect(_context).to.equal(context); + done(); + } + }, 10); + }); + +});