From cca8ec712afd42407c2b7848cba966a3106cdcb6 Mon Sep 17 00:00:00 2001 From: qsm-odoo Date: Thu, 21 Mar 2024 11:16:20 +0100 Subject: [PATCH] [MOV] web: move some DOM utils to the non-lazy loaded bundle In the future, we might want to make most utils available in the non lazy JS bundle part. We are also considering moving the Bootstrap lib and some more in there. This commit is about moving the strict minimum to be able to improve the lazy loading behavior in stable: the DOM utils that allow to make async handlers and create button loading effects when we click on them. This also means that those utils were adapted to not use jQuery and other non available utils. Related to task-3770362 X-original-commit: 2e922105d8714704b3db94966defc922297e2ab6 Part-of: odoo/odoo#159680 --- addons/web/__manifest__.py | 2 + addons/web/static/src/legacy/js/core/dom.js | 121 +---------------- .../static/src/legacy/js/core/minimal_dom.js | 126 ++++++++++++++++++ addons/web_editor/__manifest__.py | 1 + 4 files changed, 132 insertions(+), 118 deletions(-) create mode 100644 addons/web/static/src/legacy/js/core/minimal_dom.js diff --git a/addons/web/__manifest__.py b/addons/web/__manifest__.py index 93c8d35ad7a..d4dea87752c 100644 --- a/addons/web/__manifest__.py +++ b/addons/web/__manifest__.py @@ -126,6 +126,7 @@ This module provides the core of the Odoo Web Client. 'web/static/src/module_loader.js', 'web/static/src/session.js', 'web/static/src/core/browser/cookie.js', + 'web/static/src/legacy/js/core/minimal_dom.js', 'web/static/src/legacy/js/public/lazyloader.js', ], 'web.assets_frontend': [ @@ -222,6 +223,7 @@ This module provides the core of the Odoo Web Client. ('remove', 'web/static/src/module_loader.js'), ('remove', 'web/static/src/session.js'), ('remove', 'web/static/src/core/browser/cookie.js'), + ('remove', 'web/static/src/legacy/js/core/minimal_dom.js'), ('remove', 'web/static/src/legacy/js/public/lazyloader.js'), ], # Optional Bundle for PDFJS lib diff --git a/addons/web/static/src/legacy/js/core/dom.js b/addons/web/static/src/legacy/js/core/dom.js index f7000641acc..03380799489 100644 --- a/addons/web/static/src/legacy/js/core/dom.js +++ b/addons/web/static/src/legacy/js/core/dom.js @@ -9,11 +9,9 @@ * something happens in the DOM. */ -import { delay } from "@web/core/utils/concurrency"; - -var dom = { - DEBOUNCE: 400, +import * as minimalDom from '@web/legacy/js/core/minimal_dom'; +const dom = Object.assign({}, minimalDom, { /** * jQuery find function behavior is:: * @@ -49,119 +47,6 @@ var dom = { return $results; }, - /** - * Protects a function which is to be used as a handler by preventing its - * execution for the duration of a previous call to it (including async - * parts of that call). - * - * Limitation: as the handler is ignored during async actions, - * the 'preventDefault' or 'stopPropagation' calls it may want to do - * will be ignored too. Using the 'preventDefault' and 'stopPropagation' - * arguments solves that problem. - * - * @param {function} fct - * The function which is to be used as a handler. If a promise - * is returned, it is used to determine when the handler's action is - * finished. Otherwise, the return is used as jQuery uses it. - * @param {function|boolean} preventDefault - * @param {function|boolean} stopPropagation - */ - makeAsyncHandler: function (fct, preventDefault, stopPropagation) { - var pending = false; - function _isLocked() { - return pending; - } - function _lock() { - pending = true; - } - function _unlock() { - pending = false; - } - return function (ev) { - if (preventDefault === true || preventDefault && preventDefault()) { - ev.preventDefault(); - } - if (stopPropagation === true || stopPropagation && stopPropagation()) { - ev.stopPropagation(); - } - - if (_isLocked()) { - // If a previous call to this handler is still pending, ignore - // the new call. - return; - } - - _lock(); - var result = fct.apply(this, arguments); - Promise.resolve(result).finally(_unlock); - return result; - }; - }, - /** - * Creates a debounced version of a function to be used as a button click - * handler. Also improves the handler to disable the button for the time of - * the debounce and/or the time of the async actions it performs. - * - * Limitation: if two handlers are put on the same button, the button will - * become enabled again once any handler's action finishes (multiple click - * handlers should however not be binded to the same button). - * - * @param {function} fct - * The function which is to be used as a button click handler. If a - * promise is returned, it is used to determine when the button can be - * re-enabled. Otherwise, the return is used as jQuery uses it. - */ - makeButtonHandler: function (fct) { - // Fallback: if the final handler is not binded to a button, at least - // make it an async handler (also handles the case where some events - // might ignore the disabled state of the button). - fct = dom.makeAsyncHandler(fct); - - return function (ev) { - var result = fct.apply(this, arguments); - - var $button = $(ev.target).closest('.btn'); - if (!$button.length) { - return result; - } - - // Disable the button for the duration of the handler's action - // or at least for the duration of the click debounce. This makes - // a 'real' debounce creation useless. Also, during the debouncing - // part, the button is disabled without any visual effect. - $button.addClass('pe-none'); - Promise.resolve(dom.DEBOUNCE && delay(dom.DEBOUNCE)).then(function () { - $button.removeClass('pe-none'); - const restore = dom.addButtonLoadingEffect($button[0]); - return Promise.resolve(result).finally(restore); - }); - - return result; - }; - }, - /** - * Gives the button a loading effect by disabling it and adding a `fa` - * spinner icon. - * The existing button `fa` icons will be hidden through css. - * - * @param {HTMLElement} btn - the button to disable/load - * @return {function} a callback function that will restore the button - * initial state - */ - addButtonLoadingEffect: function (btn) { - const $btn = $(btn); - $btn.addClass('o_website_btn_loading disabled'); - $btn.prop('disabled', true); - const $loader = $('', { - class: 'fa fa-refresh fa-spin me-2', - }); - $btn.prepend($loader); - return () => { - $btn.removeClass('o_website_btn_loading disabled'); - $btn.prop('disabled', false); - $loader.remove(); - }; - }, /** * Renders a button with standard odoo template. This does not use any xml * template to avoid forcing the frontend part to lazy load a xml file for @@ -340,5 +225,5 @@ var dom = { $scrollable.animate({scrollTop: originalScrollTop}, clonedOptions); }); }, -}; +}); export default dom; diff --git a/addons/web/static/src/legacy/js/core/minimal_dom.js b/addons/web/static/src/legacy/js/core/minimal_dom.js new file mode 100644 index 00000000000..14f40007456 --- /dev/null +++ b/addons/web/static/src/legacy/js/core/minimal_dom.js @@ -0,0 +1,126 @@ +/** @odoo-module **/ + +export const DEBOUNCE = 400; + +/** + * Protects a function which is to be used as a handler by preventing its + * execution for the duration of a previous call to it (including async + * parts of that call). + * + * Limitation: as the handler is ignored during async actions, + * the 'preventDefault' or 'stopPropagation' calls it may want to do + * will be ignored too. Using the 'preventDefault' and 'stopPropagation' + * arguments solves that problem. + * + * @param {function} fct + * The function which is to be used as a handler. If a promise + * is returned, it is used to determine when the handler's action is + * finished. Otherwise, the return is used as jQuery uses it. + * @param {function|boolean} preventDefault + * @param {function|boolean} stopPropagation + */ +export function makeAsyncHandler(fct, preventDefault, stopPropagation) { + let pending = false; + function _isLocked() { + return pending; + } + function _lock() { + pending = true; + } + function _unlock() { + pending = false; + } + return function (ev) { + if (preventDefault === true || preventDefault && preventDefault()) { + ev.preventDefault(); + } + if (stopPropagation === true || stopPropagation && stopPropagation()) { + ev.stopPropagation(); + } + + if (_isLocked()) { + // If a previous call to this handler is still pending, ignore + // the new call. + return; + } + + _lock(); + const result = fct.apply(this, arguments); + Promise.resolve(result).finally(_unlock); + return result; + }; +} + +/** + * Creates a debounced version of a function to be used as a button click + * handler. Also improves the handler to disable the button for the time of + * the debounce and/or the time of the async actions it performs. + * + * Limitation: if two handlers are put on the same button, the button will + * become enabled again once any handler's action finishes (multiple click + * handlers should however not be bound to the same button). + * + * @param {function} fct + * The function which is to be used as a button click handler. If a + * promise is returned, it is used to determine when the button can be + * re-enabled. Otherwise, the return is used as jQuery uses it. + */ +export function makeButtonHandler(fct) { + // Fallback: if the final handler is not bound to a button, at least + // make it an async handler (also handles the case where some events + // might ignore the disabled state of the button). + fct = makeAsyncHandler(fct); + + return function (ev) { + const result = fct.apply(this, arguments); + + const buttonEl = ev.target && ev.target.closest && ev.target.closest('.btn'); + if (!buttonEl) { + return result; + } + + // Disable the button for the duration of the handler's action + // or at least for the duration of the click debounce. This makes + // a 'real' debounce creation useless. Also, during the debouncing + // part, the button is disabled without any visual effect. + if (buttonEl.classList) { + buttonEl.classList.add('pe-none'); + } + Promise.resolve(DEBOUNCE && new Promise(r => setTimeout(r, DEBOUNCE))) + .then(function () { + if (buttonEl.classList) { + buttonEl.classList.remove('pe-none'); + } + const restore = addButtonLoadingEffect(buttonEl); + return Promise.resolve(result).finally(restore); + }); + + return result; + }; +} + +/** + * Gives the button a loading effect by disabling it and adding a `fa` + * spinner icon. + * The existing button `fa` icons will be hidden through css. + * + * @param {HTMLElement} btnEl - the button to disable/load + * @return {function} a callback function that will restore the button + * initial state + */ +export function addButtonLoadingEffect(btnEl) { + if (btnEl.classList) { + btnEl.classList.add('o_website_btn_loading', 'disabled'); + } + btnEl.disabled = true; + const loaderEl = document.createElement('span'); + loaderEl.classList.add('fa', 'fa-refresh', 'fa-spin', 'me-2'); + btnEl.prepend(loaderEl); + return () => { + if (btnEl.classList) { + btnEl.classList.remove('o_website_btn_loading', 'disabled'); + } + btnEl.disabled = false; + loaderEl.remove(); + }; +} diff --git a/addons/web_editor/__manifest__.py b/addons/web_editor/__manifest__.py index 14f73b205bf..39b405c1113 100644 --- a/addons/web_editor/__manifest__.py +++ b/addons/web_editor/__manifest__.py @@ -118,6 +118,7 @@ Odoo Web Editor widget. 'web/static/src/legacy/js/core/class.js', 'web/static/src/legacy/js/core/dialog.js', 'web/static/src/legacy/xml/dialog.xml', + 'web/static/src/legacy/js/core/minimal_dom.js', 'web/static/src/legacy/js/core/dom.js', 'web/static/src/legacy/js/core/mixins.js', 'web/static/src/legacy/js/core/service_mixins.js',