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',