diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1c6314a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +indent_style = tab +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.yml] +indent_style = space +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.gitignore b/.gitignore index aa6fd7c..239ecff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ -components -build -node_modules \ No newline at end of file +node_modules +yarn.lock diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..43c97e7 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/CONTRIBUTORS b/CONTRIBUTORS deleted file mode 100644 index 89d7032..0000000 --- a/CONTRIBUTORS +++ /dev/null @@ -1,13 +0,0 @@ -Ben Carpenter -Billy Moon -Josh Goldberg -Julian Gruber -Kristofer Selbekk -Matt Mueller -Matthew Mueller -Nathan Rajlich -Oleg Pudeyev -Stephen Mathieson -TJ Holowaychuk -suhaotian -ven diff --git a/History.md b/History.md deleted file mode 100644 index ca26ba5..0000000 --- a/History.md +++ /dev/null @@ -1,55 +0,0 @@ - -1.2.1 / 2021-03-09 -================== - - * Add CONTRIBUTORS and MIT LICENSE file. (#28) - -1.2.0 / 2018-08-14 -================== - - * Added a .debounce member to debounce (#21) - -1.1.0 / 2017-10-30 -================== - - * Ability to force execution (#16) - -1.0.2 / 2017-04-21 -================== - - * Fixes #3 - Debounced function executing early? (#15) - * Merge pull request #13 from selbekk/master - * Remove date-now from package.json - * Remove date-now dependency from component.json - * Remove date-now usage - -1.0.1 / 2016-07-25 -================== - - * add ability to clear timer (#10) - -1.0.0 / 2014-06-21 -================== - - * Readme: attribute underscore.js in the License section - * index: rewrite to use underscore.js' implementation (#2, @TooTallNate) - * component, package: add "date-now" as a dependency - * test: fix test - * component, package: add "keywords" array - * package: adjust "description" - * package: added "repository" field (#1, @juliangruber) - -0.0.3 / 2013-08-21 -================== - - * immediate now defaults to `false` - -0.0.2 / 2013-07-27 -================== - - * consolidated with TJ's debounce - -0.0.1 / 2012-11-5 -================== - - * Initial release diff --git a/LICENSE b/LICENSE index 5a4d942..f1cf0eb 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,11 @@ MIT License -Copyright (c) 2012-2018 The Debounce Contributors. See CONTRIBUTORS. +Coprighht (c) Jeremy Ashkenas, Julian Gonggrijp, DocumentCloud, Investigative Reporters & Editors +Copyright (c) Ben Carpenter, Billy Moon, Josh Goldberg, Julian Gruber, Kristofer Selbekk, Matthew Mueller, Nathan Rajlich, Oleg Pudeyev, Stephen Mathieson, TJ Holowaychuk, suhaotian, ven +Copyright (c) Sindre Sorhus (https://sindresorhus.com) -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile deleted file mode 100644 index 0f14dac..0000000 --- a/Makefile +++ /dev/null @@ -1,11 +0,0 @@ - -build: components index.js - @component build --dev - -components: component.json - @component install --dev - -clean: - rm -fr build components template.js - -.PHONY: clean diff --git a/Readme.md b/Readme.md deleted file mode 100644 index 8639f0b..0000000 --- a/Readme.md +++ /dev/null @@ -1,69 +0,0 @@ - -# debounce - - Useful for implementing behavior that should only happen after a repeated - action has completed. - -## Installation - - $ component install component/debounce - - Or in node: - - $ npm install debounce - -## Example - -```js -var debounce = require('debounce'); -window.onresize = debounce(resize, 200); - -function resize(e) { - console.log('height', window.innerHeight); - console.log('width', window.innerWidth); -} -``` - -To later clear the timer and cancel currently scheduled executions: -``` -window.onresize.clear(); -``` - -To execute any pending invocations and reset the timer: -``` -window.onresize.flush(); -``` - -Alternately, if using newer syntax: - -```js -import { debounce } from "debounce"; -``` - -## API - -### debounce(fn, wait, [ immediate || false ]) - - Creates and returns a new debounced version of the passed function that - will postpone its execution until after wait milliseconds have elapsed - since the last time it was invoked. - - Pass `true` for the `immediate` parameter to cause debounce to trigger - the function on the leading edge instead of the trailing edge of the wait - interval. Useful in circumstances like preventing accidental double-clicks - on a "submit" button from firing a second time. - - The debounced function returned has a property 'clear' that is a - function that will clear any scheduled future executions of your function. - - The debounced function returned has a property 'flush' that is a - function that will immediately execute the function if and only if execution is scheduled, - and reset the execution timer for subsequent invocations of the debounced - function. - -## License - - MIT - - Original implementation is from [`underscore.js`](http://underscorejs.org/) - which also has an MIT license. diff --git a/component.json b/component.json deleted file mode 100644 index 0ef2236..0000000 --- a/component.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "debounce", - "repo": "component/debounce", - "description": "Creates and returns a new debounced version of the passed function that will postpone its execution until after wait milliseconds have elapsed since the last time it was invoked", - "version": "1.2.1", - "main": "index.js", - "scripts": [ - "index.js" - ], - "keywords": [ - "function", - "throttle", - "invoke" - ], - "dependencies": {}, - "development": {}, - "license": "MIT" -} diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..4cbc485 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,22 @@ +type AnyFunction = (...arguments_: readonly any[]) => unknown; + +/** +Returns a function, that, as long as it continues to be invoked, will not be triggered. The function will be called after it stops being called for N milliseconds. + +If `immediate` is passed, trigger the function on the leading edge, instead of the trailing. The function also has a property 'clear' that is a function which will clear the timer to prevent previously scheduled executions. +*/ +declare function debounce( + function_: F, + wait?: number, + immediate?: boolean +): debounce.DebouncedFunction; + +declare namespace debounce { + type DebouncedFunction = { + (...arguments_: Parameters): ReturnType | undefined; + clear(): void; + flush(): void; + }; +} + +export = debounce; diff --git a/index.js b/index.js index db1cdd7..bbc1e73 100644 --- a/index.js +++ b/index.js @@ -1,73 +1,78 @@ -/** - * Returns a function, that, as long as it continues to be invoked, will not - * be triggered. The function will be called after it stops being called for - * N milliseconds. If `immediate` is passed, trigger the function on the - * leading edge, instead of the trailing. The function also has a property 'clear' - * that is a function which will clear the timer to prevent previously scheduled executions. - * - * @source underscore.js - * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/ - * @param {Function} func function to wrap - * @param {number} [wait=100] time to wait in ms (`100`) - * @param {boolean} [immediate=false] should execute at the beginning (`false`) - * @api public - */ -function debounce(func, wait, immediate){ - var timeout, args, context, timestamp, result; - if (null == wait) wait = 100; - - function later() { - var last = Date.now() - timestamp; - - if (last < wait && last >= 0) { - timeout = setTimeout(later, wait - last); - } else { - timeout = null; - if (!immediate) { - var callContext = context, callArgs = args - context = args = null; - result = func.apply(callContext, callArgs); - } - } - }; - - var debounced = function(){ - context = this; - args = arguments; - timestamp = Date.now(); - var callNow = immediate && !timeout; - if (!timeout) timeout = setTimeout(later, wait); - if (callNow) { - var callContext = context, callArgs = args - context = args = null; - result = func.apply(callContext, callArgs); - } - - return result; - }; - - debounced.clear = function() { - if (timeout) { - clearTimeout(timeout); - timeout = null; - } - }; - - debounced.flush = function() { - if (timeout) { - var callContext = context, callArgs = args; - context = args = null; - result = func.apply(callContext, callArgs); - - clearTimeout(timeout); - timeout = null; - } - }; - - return debounced; -}; +function debounce(function_, wait = 100, immediate) { + let storedContext; + let storedArguments; + let timeoutId; + let timestamp; + let result; + + function later() { + const last = Date.now() - timestamp; + + if (last < wait && last >= 0) { + timeoutId = setTimeout(later, wait - last); + } else { + timeoutId = undefined; + + if (!immediate) { + const callContext = storedContext; + const callArguments = storedArguments; + storedContext = undefined; + storedArguments = undefined; + result = function_.apply(callContext, callArguments); + } + } + } + + const debounced = function (...arguments_) { + storedContext = this; // eslint-disable-line unicorn/no-this-assignment + storedArguments = arguments_; + timestamp = Date.now(); + + const callNow = immediate && !timeoutId; + + if (!timeoutId) { + timeoutId = setTimeout(later, wait); + } + + if (callNow) { + const callContext = storedContext; + const callArguments = storedArguments; + storedContext = undefined; + storedArguments = undefined; + result = function_.apply(callContext, callArguments); + } + + return result; + }; + + debounced.clear = () => { + if (!timeoutId) { + return; + } + + clearTimeout(timeoutId); + timeoutId = undefined; + }; + + debounced.flush = () => { + if (!timeoutId) { + return; + } + + const callContext = storedContext; + const callArguments = storedArguments; + storedContext = undefined; + storedArguments = undefined; + result = function_.apply(callContext, callArguments); + + clearTimeout(timeoutId); + timeoutId = undefined; + }; + + return debounced; +} // Adds compatibility for ES modules -debounce.debounce = debounce; +module.exports.debounce = debounce; module.exports = debounce; diff --git a/package.json b/package.json index 70d1466..52153d7 100644 --- a/package.json +++ b/package.json @@ -1,27 +1,42 @@ { - "name": "debounce", - "description": "Creates and returns a new debounced version of the passed function that will postpone its execution until after wait milliseconds have elapsed since the last time it was invoked", - "version": "1.2.1", - "repository": "git://github.com/component/debounce", - "main": "index.js", - "scripts": { - "test": "minijasminenode test.js" - }, - "license": "MIT", - "keywords": [ - "function", - "throttle", - "invoke" - ], - "devDependencies": { - "minijasminenode": "^1.1.1", - "sinon": "^1.17.7", - "mocha": "*", - "should": "*" - }, - "component": { - "scripts": { - "debounce/index.js": "index.js" - } - } + "name": "debounce", + "version": "1.2.1", + "description": "Creates and returns a new debounced version of the passed function that will postpone its execution until after wait milliseconds have elapsed since the last time it was invoked", + "license": "MIT", + "repository": "sindresorhus/debounce", + "funding": "https://github.com/sponsors/sindresorhus", + "exports": { + "types": "./index.d.ts", + "default": "./index.js" + }, + "main": "./index.js", + "types": "./index.d.ts", + "sideEffects": false, + "engines": { + "node": ">=18" + }, + "scripts": { + "test": "xo && minijasminenode test.js" + }, + "files": [ + "index.js", + "index.d.ts" + ], + "keywords": [ + "function", + "throttle", + "invoke" + ], + "devDependencies": { + "minijasminenode": "^1.1.1", + "mocha": "^10.2.0", + "should": "^13.2.3", + "sinon": "^1.17.0", + "xo": "^0.56.0" + }, + "xo": { + "rules": { + "unicorn/prefer-module": "off" + } + } } diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..2d5b0fe --- /dev/null +++ b/readme.md @@ -0,0 +1,46 @@ +# debounce + +> Useful for implementing behavior that should only happen after a repeated action has completed + +## Install + +```sh +npm install debounce +``` + +## Usage + +```js +import debounce from 'debounce'; + +function resize() { + console.log('height', window.innerHeight); + console.log('width', window.innerWidth); +} + +window.onresize = debounce(resize, 200); +``` + +To later clear the timer and cancel currently scheduled executions: + +```js +window.onresize.clear(); +``` + +To execute any pending invocations and reset the timer: + +```js +window.onresize.flush(); +``` + +## API + +### debounce(fn, wait, immediate?) + +Creates and returns a new debounced version of the passed function that will postpone its execution until after wait milliseconds have elapsed since the last time it was invoked. + +Pass `true` for the `immediate` parameter to cause debounce to trigger the function on the leading edge instead of the trailing edge of the wait interval. Useful in circumstances like preventing accidental double-clicks on a "submit" button from firing a second time. + +The debounced function returned has a property 'clear' that is a function that will clear any scheduled future executions of your function. + +The debounced function returned has a property 'flush' that is a function that will immediately execute the function if and only if execution is scheduled, and reset the execution timer for subsequent invocations of the debounced function. diff --git a/test.html b/test.html deleted file mode 100644 index c5e2681..0000000 --- a/test.html +++ /dev/null @@ -1,32 +0,0 @@ - - - Debounce Component - - - Resize the window! -
- Cancel Print -
- Print Now - - - - - diff --git a/test.js b/test.js index be0d95a..c31d43c 100644 --- a/test.js +++ b/test.js @@ -1,190 +1,175 @@ -var debounce = require('.') -var sinon = require('sinon') +/* eslint-env jasmine */ +const sinon = require('sinon'); +const debounce = require('./index.js'); -describe('housekeeping', function() { - it('should be defined as a function', function() { - expect(typeof debounce).toEqual('function') - }) -}) +describe('housekeeping', () => { + it('should be defined as a function', () => { + expect(typeof debounce).toEqual('function'); + }); +}); -describe('catch issue #3 - Debounced function executing early?', function() { +describe('catch issue #3 - Debounced function executing early?', () => { + // Use sinon to control the clock + let clock; - // use sinon to control the clock - var clock + beforeEach(() => { + clock = sinon.useFakeTimers(); + }); - beforeEach(function(){ - clock = sinon.useFakeTimers() - }) + afterEach(() => { + clock.restore(); + }); - afterEach(function(){ - clock.restore() - }) + it('should debounce with fast timeout', () => { + const callback = sinon.spy(); - it('should debounce with fast timeout', function() { + // Set up debounced function with wait of 100 + const fn = debounce(callback, 100); - var callback = sinon.spy() + // Call debounced function at interval of 50 + setTimeout(fn, 100); + setTimeout(fn, 150); + setTimeout(fn, 200); + setTimeout(fn, 250); - // set up debounced function with wait of 100 - var fn = debounce(callback, 100) + // Set the clock to 100 (period of the wait) ticks after the last debounced call + clock.tick(350); - // call debounced function at interval of 50 - setTimeout(fn, 100) - setTimeout(fn, 150) - setTimeout(fn, 200) - setTimeout(fn, 250) + // The callback should have been triggered once + expect(callback.callCount).toEqual(1); + }); +}); - // set the clock to 100 (period of the wait) ticks after the last debounced call - clock.tick(350) +describe('forcing execution', () => { + // Use sinon to control the clock + let clock; - // the callback should have been triggered once - expect(callback.callCount).toEqual(1) + beforeEach(() => { + clock = sinon.useFakeTimers(); + }); - }) + afterEach(() => { + clock.restore(); + }); -}) + it('should not execute prior to timeout', () => { + const callback = sinon.spy(); -describe('forcing execution', function() { + // Set up debounced function with wait of 100 + const fn = debounce(callback, 100); - // use sinon to control the clock - var clock + // Call debounced function at interval of 50 + setTimeout(fn, 100); + setTimeout(fn, 150); - beforeEach(function(){ - clock = sinon.useFakeTimers() - }) + // Set the clock to 25 (period of the wait) ticks after the last debounced call + clock.tick(175); - afterEach(function(){ - clock.restore() - }) + // The callback should not have been called yet + expect(callback.callCount).toEqual(0); + }); - it('should not execute prior to timeout', function() { + it('should execute prior to timeout when flushed', () => { + const callback = sinon.spy(); - var callback = sinon.spy() + // Set up debounced function with wait of 100 + const fn = debounce(callback, 100); - // set up debounced function with wait of 100 - var fn = debounce(callback, 100) + // Call debounced function at interval of 50 + setTimeout(fn, 100); + setTimeout(fn, 150); - // call debounced function at interval of 50 - setTimeout(fn, 100) - setTimeout(fn, 150) + // Set the clock to 25 (period of the wait) ticks after the last debounced call + clock.tick(175); - // set the clock to 25 (period of the wait) ticks after the last debounced call - clock.tick(175) + fn.flush(); - // the callback should not have been called yet - expect(callback.callCount).toEqual(0) + // The callback has been called + expect(callback.callCount).toEqual(1); + }); - }) + it('should not execute again after timeout when flushed before the timeout', () => { + const callback = sinon.spy(); - it('should execute prior to timeout when flushed', function() { + // Set up debounced function with wait of 100 + const fn = debounce(callback, 100); - var callback = sinon.spy() + // Call debounced function at interval of 50 + setTimeout(fn, 100); + setTimeout(fn, 150); - // set up debounced function with wait of 100 - var fn = debounce(callback, 100) + // Set the clock to 25 (period of the wait) ticks after the last debounced call + clock.tick(175); - // call debounced function at interval of 50 - setTimeout(fn, 100) - setTimeout(fn, 150) + fn.flush(); - // set the clock to 25 (period of the wait) ticks after the last debounced call - clock.tick(175) - - fn.flush() + // The callback has been called here + expect(callback.callCount).toEqual(1); - // the callback has been called - expect(callback.callCount).toEqual(1) + // Move to past the timeout + clock.tick(225); - }) + // The callback should have only been called once + expect(callback.callCount).toEqual(1); + }); - it('should not execute again after timeout when flushed before the timeout', function() { + it('should not execute on a timer after being flushed', () => { + const callback = sinon.spy(); - var callback = sinon.spy() + // Set up debounced function with wait of 100 + const fn = debounce(callback, 100); - // set up debounced function with wait of 100 - var fn = debounce(callback, 100) + // Call debounced function at interval of 50 + setTimeout(fn, 100); + setTimeout(fn, 150); - // call debounced function at interval of 50 - setTimeout(fn, 100) - setTimeout(fn, 150) + // Set the clock to 25 (period of the wait) ticks after the last debounced call + clock.tick(175); - // set the clock to 25 (period of the wait) ticks after the last debounced call - clock.tick(175) - - fn.flush() - - // the callback has been called here - expect(callback.callCount).toEqual(1) - - // move to past the timeout - clock.tick(225) + fn.flush(); - // the callback should have only been called once - expect(callback.callCount).toEqual(1) + // The callback has been called here + expect(callback.callCount).toEqual(1); - }) + // Schedule again + setTimeout(fn, 250); - it('should not execute on a timer after being flushed', function() { + // Move to past the new timeout + clock.tick(400); - var callback = sinon.spy() + // The callback should have been called again + expect(callback.callCount).toEqual(2); + }); - // set up debounced function with wait of 100 - var fn = debounce(callback, 100) + it('should not execute when flushed if nothing was scheduled', () => { + const callback = sinon.spy(); - // call debounced function at interval of 50 - setTimeout(fn, 100) - setTimeout(fn, 150) + // Set up debounced function with wait of 100 + const fn = debounce(callback, 100); - // set the clock to 25 (period of the wait) ticks after the last debounced call - clock.tick(175) - - fn.flush() - - // the callback has been called here - expect(callback.callCount).toEqual(1) - - // schedule again - setTimeout(fn, 250) - - // move to past the new timeout - clock.tick(400) + fn.flush(); - // the callback should have been called again - expect(callback.callCount).toEqual(2) + // The callback should not have been called + expect(callback.callCount).toEqual(0); + }); - }) + it('should execute with correct args when called again from within timeout', () => { + const callback = sinon.spy(n => + // Recursively call debounced function until n == 0 + --n && fn(n), + ); - it('should not execute when flushed if nothing was scheduled', function() { + const fn = debounce(callback, 100); - var callback = sinon.spy() + fn(3); - // set up debounced function with wait of 100 - var fn = debounce(callback, 100) + clock.tick(125); + clock.tick(250); + clock.tick(375); - fn.flush() - - // the callback should not have been called - expect(callback.callCount).toEqual(0) - - }) - - it('should execute with correct args when called again from within timeout', function() { - const callback = sinon.spy(n => - // recursively call debounced function until n == 0 - --n && fn(n) - ); - - const fn = debounce(callback, 100) - - fn(3) - - clock.tick(125) - clock.tick(250) - clock.tick(375) - - expect(callback.callCount).toEqual(3) - expect(callback.args[0]).toEqual([3]) - expect(callback.args[1]).toEqual([2]) - expect(callback.args[2]).toEqual([1]) - }); - -}) + expect(callback.callCount).toEqual(3); + expect(callback.args[0]).toEqual([3]); + expect(callback.args[1]).toEqual([2]); + expect(callback.args[2]).toEqual([1]); + }); +});