UNPKG

@shopify/polaris

Version:

Shopify’s admin product component library

166 lines (143 loc) 4.42 kB
interface DebounceSettings { leading?: boolean; maxWait?: number; trailing?: boolean; } export function debounce<T extends (this: unknown, ...args: any[]) => any>( func: T, waitArg?: number, options?: DebounceSettings, ) { let lastArgs: any; let lastThis: any; let maxWait: number | undefined; let result: any; let timerId: any; let lastCallTime: any; let lastInvokeTime = 0; let leading = false; let maxing = false; let trailing = true; // Bypass `requestAnimationFrame` by explicitly setting `wait=0`. const useRAF = !waitArg && waitArg !== 0; if (typeof func !== 'function') { throw new TypeError('Expected a function'); } const wait = waitArg || 0; if (typeof options === 'object') { leading = Boolean(options.leading); maxing = 'maxWait' in options; maxWait = maxing ? Math.max(Number(options.maxWait) || 0, wait) : undefined; trailing = 'trailing' in options ? Boolean(options.trailing) : trailing; } function invokeFunc(time: number) { const args = lastArgs; const thisArg = lastThis; lastArgs = undefined; lastThis = undefined; lastInvokeTime = time; result = func.apply(thisArg, args); return result; } function startTimer(pendingFunc: any, wait: number) { if (useRAF) { cancelAnimationFrame(timerId); return requestAnimationFrame(pendingFunc); } return setTimeout(pendingFunc, wait); } function cancelTimer(id: number) { if (useRAF) { return cancelAnimationFrame(id); } clearTimeout(id); } function leadingEdge(time: number) { // Reset any `maxWait` timer. lastInvokeTime = time; // Start the timer for the trailing edge. timerId = startTimer(timerExpired, wait); // Invoke the leading edge. return leading ? invokeFunc(time) : result; } function remainingWait(time: number) { const timeSinceLastCall = time - lastCallTime; const timeSinceLastInvoke = time - lastInvokeTime; const timeWaiting = wait - timeSinceLastCall; return maxing && maxWait ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting; } function shouldInvoke(time: number) { const timeSinceLastCall = time - lastCallTime; const timeSinceLastInvoke = time - lastInvokeTime; // Either this is the first call, activity has stopped and we're at the // trailing edge, the system time has gone backwards and we're treating // it as the trailing edge, or we've hit the `maxWait` limit. return ( lastCallTime === undefined || timeSinceLastCall >= wait || timeSinceLastCall < 0 || (maxing && maxWait && timeSinceLastInvoke >= maxWait) ); } function timerExpired() { const time = Date.now(); if (shouldInvoke(time)) { return trailingEdge(time); } // Restart the timer. timerId = startTimer(timerExpired, remainingWait(time)); } function trailingEdge(time: number) { timerId = undefined; // Only invoke if we have `lastArgs` which means `func` has been // debounced at least once. if (trailing && lastArgs) { return invokeFunc(time); } // eslint-disable-next-line no-multi-assign lastArgs = lastThis = undefined; return result; } function cancel() { if (timerId !== undefined) { cancelTimer(timerId); } lastInvokeTime = 0; // eslint-disable-next-line no-multi-assign lastArgs = lastCallTime = lastThis = timerId = undefined; } function flush() { return timerId === undefined ? result : trailingEdge(Date.now()); } function pending() { return timerId !== undefined; } function debounced(this: unknown, ...args: any[]) { const time = Date.now(); const isInvoking = shouldInvoke(time); lastArgs = args; // eslint-disable-next-line consistent-this, @typescript-eslint/no-this-alias lastThis = this; lastCallTime = time; if (isInvoking) { if (timerId === undefined) { return leadingEdge(lastCallTime); } if (maxing) { // Handle invocations in a tight loop. timerId = startTimer(timerExpired, wait); return invokeFunc(lastCallTime); } } if (timerId === undefined) { timerId = startTimer(timerExpired, wait); } return result; } debounced.cancel = cancel; debounced.flush = flush; debounced.pending = pending; return debounced; }