events_debounce.js

import mimicFn from '../data/mimic-fn';

/**
 * @module debounce
 * @description Returns a debounced function that delays calling the input function until after wait milliseconds
 * have elapsed since the last time the debounced function was called.
 * It comes with a .cancel() method to cancel any scheduled input function calls.
 *
 * @since 1.0.0
 *
 * @param {Function} inputFunction Function to debounce.
 * @param {object}   options       {
 *                                 {number} wait Time in milliseconds to wait until the input function is called, default: 0.
 *                                 {number} maxWait The maximum time the input function is allowed to be delayed before it's invoked. This can be used to limit the number of calls handled in a constant stream. For example, a media player sending updates every few milliseconds but wants to be handled only once a second, default: infinity.
 *                                 {boolean} before Trigger the function on the leading edge of the wait interval. For example, can be useful for preventing accidental double-clicks on a "submit" button from firing a second time, default: false
 *                                 {boolean} after Trigger the function on the trailing edge of the wait interval, default: true
 *                                 }
 *
 * @requires mimicFn
 *
 * @return {Function} The debounced function.
 *
 * @example
 * import { debounce } from "@gravityforms/utils";
 *
 * function Example() {
 *   // do something on resize
 * }
 *
 * window.addEventListener( 'resize', debounce( Example, { wait: 400 } ) );
 *
 */
export default function debounce( inputFunction, options = {} ) {
	if ( typeof inputFunction !== 'function' ) {
		throw new TypeError( `Expected the first argument to be a function, got \`${ typeof inputFunction }\`` );
	}

	const {
		wait = 0,
		maxWait = Number.Infinity,
		before = false,
		after = true,
	} = options;

	if ( ! before && ! after ) {
		throw new Error( 'Both `before` and `after` are false, function wouldn\'t be called.' );
	}

	let timeout;
	let maxTimeout;
	let result;

	const debouncedFunction = function( ...arguments_ ) {
		const context = this;

		const later = () => {
			timeout = undefined;

			if ( maxTimeout ) {
				clearTimeout( maxTimeout );
				maxTimeout = undefined;
			}

			if ( after ) {
				result = inputFunction.apply( context, arguments_ );
			}
		};

		const maxLater = () => {
			maxTimeout = undefined;

			if ( timeout ) {
				clearTimeout( timeout );
				timeout = undefined;
			}

			if ( after ) {
				result = inputFunction.apply( context, arguments_ );
			}
		};

		const shouldCallNow = before && ! timeout;
		clearTimeout( timeout );
		timeout = setTimeout( later, wait );

		if ( maxWait > 0 && maxWait !== Number.Infinity && ! maxTimeout ) {
			maxTimeout = setTimeout( maxLater, maxWait );
		}

		if ( shouldCallNow ) {
			result = inputFunction.apply( context, arguments_ );
		}

		return result;
	};

	mimicFn( debouncedFunction, inputFunction );

	debouncedFunction.cancel = () => {
		if ( timeout ) {
			clearTimeout( timeout );
			timeout = undefined;
		}

		if ( maxTimeout ) {
			clearTimeout( maxTimeout );
			maxTimeout = undefined;
		}
	};

	return debouncedFunction;
}