From e5152781cc3e9b64cf8d7d153f3c0a00dff8a786 Mon Sep 17 00:00:00 2001 From: Samuel Degueldre Date: Thu, 23 Jun 2022 07:16:48 +0000 Subject: [PATCH] [IMP] web: debounce: support "animationFrame" delay By passing "animationFrame" as delay value, the debounced function will be called just before the next animation frame following the moment if has been effectively called. Part-of: odoo/odoo#92475 --- addons/web/static/src/core/utils/timing.js | 33 +++++++++++-------- .../web/static/tests/core/ui_service_tests.js | 2 +- .../static/tests/core/utils/timing_tests.js | 18 +++++++++- 3 files changed, 37 insertions(+), 16 deletions(-) diff --git a/addons/web/static/src/core/utils/timing.js b/addons/web/static/src/core/utils/timing.js index 5d9101335fa..7cc64925030 100644 --- a/addons/web/static/src/core/utils/timing.js +++ b/addons/web/static/src/core/utils/timing.js @@ -1,6 +1,6 @@ /** @odoo-module **/ -import { browser } from "../browser/browser"; +import { browser } from "@web/core/browser/browser"; /** * Creates a version of the function where only the last call between two @@ -33,7 +33,7 @@ export function throttleForAnimation(func) { /** * Creates and returns a new debounced version of the passed function (func) - * which will postpone its execution until after wait milliseconds + * which will postpone its execution until after 'delay' milliseconds * have elapsed since the last time it was invoked. The debounced function * will return a Promise that will be resolved when the function (func) * has been fully executed. If `immediate` is passed, trigger the function @@ -41,26 +41,31 @@ export function throttleForAnimation(func) { * * @template T the return type of the original function * @param {(...args) => T} func the function to debounce - * @param {number} wait how long should elapse before the function is called + * @param {number | "animationFrame"} delay how long should elapse before the function + * is called. If 'animationFrame' is given instead of a number, 'requestAnimationFrame' + * will be used instead of 'setTimeout'. * @param {boolean} [immediate=false] whether the function should be called on * the leading edge instead of the trailing edge. * @returns {{ (...args): Promise, cancel: () => void }} the debounced function */ -export function debounce(func, wait, immediate = false) { - let timeout; +export function debounce(func, delay, immediate = false) { + let handle; const funcName = func.name ? func.name + " (debounce)" : "debounce"; + const useAnimationFrame = delay === "animationFrame"; + const setFnName = useAnimationFrame ? "requestAnimationFrame" : "setTimeout"; + const clearFnName = useAnimationFrame ? "cancelAnimationFrame" : "clearTimeout"; return Object.assign( { [funcName](...args) { return new Promise((resolve) => { - const callNow = immediate && !timeout; - browser.clearTimeout(timeout); - timeout = browser.setTimeout(() => { - timeout = null; + const callNow = immediate && !handle; + browser[clearFnName](handle); + handle = browser[setFnName](() => { + handle = null; if (!immediate) { Promise.resolve(func.apply(this, args)).then(resolve); } - }, wait); + }, delay); if (callNow) { Promise.resolve(func.apply(this, args)).then(resolve); } @@ -69,7 +74,7 @@ export function debounce(func, wait, immediate = false) { }[funcName], { cancel() { - browser.clearTimeout(timeout); + browser[clearFnName](handle); }, } ); @@ -82,10 +87,10 @@ export function debounce(func, wait, immediate = false) { * @deprecated this function has behaviour that is unexpected considering its * name, prefer _.throttle until this function is rewritten * @param {Function} func - * @param {number} wait + * @param {number} delay * @returns {Function} */ -export function throttle(func, wait) { +export function throttle(func, delay) { let waiting = false; const funcName = func.name ? func.name + " (throttle)" : "throttle"; return { @@ -96,7 +101,7 @@ export function throttle(func, wait) { browser.setTimeout(function () { waiting = false; func.call(context, ...args); - }, wait); + }, delay); } }, }[funcName]; diff --git a/addons/web/static/tests/core/ui_service_tests.js b/addons/web/static/tests/core/ui_service_tests.js index a528702c9f8..3a145436bef 100644 --- a/addons/web/static/tests/core/ui_service_tests.js +++ b/addons/web/static/tests/core/ui_service_tests.js @@ -14,7 +14,7 @@ let browser; let baseConfig; let BlockUI, props; -QUnit.module("UI", { +QUnit.module("UI service", { async beforeEach() { target = getFixture(); serviceRegistry.add("ui", uiService); diff --git a/addons/web/static/tests/core/utils/timing_tests.js b/addons/web/static/tests/core/utils/timing_tests.js index 87a1746062c..db0f03d4042 100644 --- a/addons/web/static/tests/core/utils/timing_tests.js +++ b/addons/web/static/tests/core/utils/timing_tests.js @@ -74,7 +74,7 @@ QUnit.module("utils", () => { assert.step("myFunc"); return 42; }; - const myDebouncedFunc = debounce(myFunc, 3000, true); + const myDebouncedFunc = debounce(myFunc, 3000, { immediate: true }); myDebouncedFunc().then((x) => { assert.step("resolved " + x); }); @@ -98,6 +98,22 @@ QUnit.module("utils", () => { assert.verifySteps(["resolved 42"]); }); + QUnit.test("debounce with 'animationFrame' delay", async function (assert) { + const { execRegisteredTimeouts } = mockTimeout(); + const execRegisteredAnimationFrames = mockAnimationFrame(); + const myFunc = () => { + assert.step("myFunc"); + }; + debounce(myFunc, "animationFrame")(); + assert.verifySteps([]); + + execRegisteredTimeouts(); // should have no effect as we wait for the animation frame + assert.verifySteps([]); + + execRegisteredAnimationFrames(); // should call the function + assert.verifySteps(["myFunc"]); + }); + QUnit.test("debounced call can be cancelled", async function (assert) { assert.expect(3); const { execRegisteredTimeouts } = mockTimeout();