diff --git a/addons/web/static/src/legacy/js/core/minimal_dom.js b/addons/web/static/src/legacy/js/core/minimal_dom.js index 14f40007456..7ceb012e027 100644 --- a/addons/web/static/src/legacy/js/core/minimal_dom.js +++ b/addons/web/static/src/legacy/js/core/minimal_dom.js @@ -1,6 +1,7 @@ /** @odoo-module **/ export const DEBOUNCE = 400; +export const BUTTON_HANDLER_SELECTOR = 'a, button, input[type="submit"], input[type="button"], .btn'; /** * Protects a function which is to be used as a handler by preventing its @@ -20,6 +21,9 @@ export const DEBOUNCE = 400; * @param {function|boolean} stopPropagation */ export function makeAsyncHandler(fct, preventDefault, stopPropagation) { + // TODO in master, add those as arguments. + const stopImmediatePropagation = this && this.__makeAsyncHandler_stopImmediatePropagation; + let pending = false; function _isLocked() { return pending; @@ -37,6 +41,9 @@ export function makeAsyncHandler(fct, preventDefault, stopPropagation) { if (stopPropagation === true || stopPropagation && stopPropagation()) { ev.stopPropagation(); } + if (stopImmediatePropagation === true || stopImmediatePropagation && stopImmediatePropagation()) { + ev.stopImmediatePropagation(); + } if (_isLocked()) { // If a previous call to this handler is still pending, ignore @@ -66,15 +73,24 @@ export function makeAsyncHandler(fct, preventDefault, stopPropagation) { * re-enabled. Otherwise, the return is used as jQuery uses it. */ export function makeButtonHandler(fct) { + // TODO in master, add those as arguments. Even though buttons are probably + // blocked by the o_website_btn_loading and related classes, it is not + // necessarily true for all event types. + const preventDefault = this && this.__makeButtonHandler_preventDefault; + const stopPropagation = this && this.__makeButtonHandler_stopPropagation; + const stopImmediatePropagation = this && this.__makeButtonHandler_stopImmediatePropagation; + // 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); + fct = makeAsyncHandler.call({ + '__makeAsyncHandler_stopImmediatePropagation': stopImmediatePropagation, + }, fct, preventDefault, stopPropagation); return function (ev) { const result = fct.apply(this, arguments); - const buttonEl = ev.target && ev.target.closest && ev.target.closest('.btn'); + const buttonEl = ev.target && ev.target.closest && ev.target.closest(BUTTON_HANDLER_SELECTOR); if (!buttonEl) { return result; } @@ -109,8 +125,10 @@ export function makeButtonHandler(fct) { * initial state */ export function addButtonLoadingEffect(btnEl) { + // Note that pe-none is used alongside "disabled" so that the behavior is + // the same on links not using the "btn" class -> pointer-events disabled. if (btnEl.classList) { - btnEl.classList.add('o_website_btn_loading', 'disabled'); + btnEl.classList.add('o_website_btn_loading', 'disabled', 'pe-none'); } btnEl.disabled = true; const loaderEl = document.createElement('span'); @@ -118,7 +136,7 @@ export function addButtonLoadingEffect(btnEl) { btnEl.prepend(loaderEl); return () => { if (btnEl.classList) { - btnEl.classList.remove('o_website_btn_loading', 'disabled'); + btnEl.classList.remove('o_website_btn_loading', 'disabled', 'pe-none'); } btnEl.disabled = false; loaderEl.remove(); diff --git a/addons/web/static/src/legacy/js/public/lazyloader.js b/addons/web/static/src/legacy/js/public/lazyloader.js index 738315bd677..024e449af0b 100644 --- a/addons/web/static/src/legacy/js/public/lazyloader.js +++ b/addons/web/static/src/legacy/js/public/lazyloader.js @@ -1,10 +1,10 @@ /** @odoo-module **/ -var blockEvents = ['submit', 'click']; -var blockFunction = function (ev) { - ev.preventDefault(); - ev.stopImmediatePropagation(); -}; +import { + BUTTON_HANDLER_SELECTOR, + makeAsyncHandler, + makeButtonHandler, +} from '@web/legacy/js/core/minimal_dom'; // Track when all JS files have been lazy loaded. Will allow to unblock the // related DOM sections when the whole JS have been loaded and executed. @@ -13,12 +13,62 @@ const _allScriptsLoaded = new Promise(resolve => { allScriptsLoadedResolve = resolve; }).then(stopWaitingLazy); -var waitingLazy = false; +const retriggeringWaitingProms = []; +/** + * Function to use as an event handler to replay the incoming event after the + * whole lazy JS has been loaded. Note that blocking the incoming event is left + * up to the caller (i.e. a potential wrapper, @see waitLazy). + * + * @param {Event} ev + * @returns {Promise} + */ +async function waitForLazyAndRetrigger(ev) { + // Wait for the lazy JS to be loaded before re-triggering the event. + const targetEl = ev.target; + await _allScriptsLoaded; + // Loaded scripts were able to add a delay to wait for before re-triggering + // events: we wait for it here. + await Promise.all(retriggeringWaitingProms); + + // At the end of the current execution queue, retrigger the event. Note that + // the event is reconstructed: this is necessary in some cases, e.g. submit + // buttons. Probably because the event was originally defaultPrevented. + setTimeout(() => { + // Extra safety check: the element might have been removed from the DOM + if (targetEl.isConnected) { + targetEl.dispatchEvent(new ev.constructor(ev.type, ev)); + } + }, 0); +} + +const loadingEffectHandlers = []; +/** + * Adds the given event listener and saves it for later removal. + * + * @param {HTMLElement} el + * @param {string} type + * @param {Function} handler + */ +function registerLoadingEffectHandler(el, type, handler) { + el.addEventListener(type, handler, {capture: true}); + loadingEffectHandlers.push({el, type, handler}); +} + +let waitingLazy = false; /** - * Blocks the DOM sections which explicitly require the lazy loaded JS to be - * working (those sections should be marked with the 'o_wait_lazy_js' class). + * Automatically adds a loading effect on clicked buttons (that were not marked + * with a specific class). Once the whole JS has been loaded, the events will be + * triggered again. * + * For forms, we automatically prevent submit events (since can be triggered + * without click on a button) but we do not retrigger them (could be duplicate + * with re-trigger of a click on a submit button otherwise). However, submitting + * a form in any way should most of the time simulate a click on the submit + * button if any anyway. + * + * @todo This function used to consider the o_wait_lazy_js class. In master, the + * uses of this classes should be removed in XML templates. * @see stopWaitingLazy */ function waitLazy() { @@ -27,19 +77,55 @@ function waitLazy() { } waitingLazy = true; - var lazyEls = document.querySelectorAll('.o_wait_lazy_js'); - for (var i = 0; i < lazyEls.length; i++) { - var element = lazyEls[i]; - blockEvents.forEach(function (evType) { - element.addEventListener(evType, blockFunction); + document.body.classList.add('o_lazy_js_waiting'); + + // TODO should probably find the wrapwrap another way but in future versions + // the element will be gone anyway. + const mainEl = document.getElementById('wrapwrap') || document.body; + const loadingEffectButtonEls = [...mainEl.querySelectorAll(BUTTON_HANDLER_SELECTOR)] + // We target all buttons but... + .filter(el => { + // ... we allow to disable the effect by adding a specific class if + // needed. Note that if some non-lazy loaded code is adding an event + // handler on some buttons, it means that if they do not have that + // class, they will show a loading effect and not do anything until + // lazy JS is loaded anyway. This is not ideal, especially since + // this was added as a stable fix/imp, but this is a compromise: on + // next page visits, the cache should limit to effect of the lazy + // loading anyway. + return !el.classList.contains('o_no_wait_lazy_js') + // ... we also allow do not consider links with a href which is + // not "#". They could be linked to handlers that prevent their + // default behavior but we consider that following the link + // should still be relevant in that case. + && !(el.nodeName === 'A' && el.href && el.getAttribute('href') !== '#'); }); + // Note: this is a limitation/a "risk" to only block and retrigger those + // specific event types. + const loadingEffectEventTypes = ['mouseover', 'mouseenter', 'mousedown', 'mouseup', 'click', 'mouseout', 'mouseleave']; + for (const buttonEl of loadingEffectButtonEls) { + for (const eventType of loadingEffectEventTypes) { + const loadingEffectHandler = eventType === 'click' + ? makeButtonHandler.call({ + '__makeButtonHandler_preventDefault': true, + '__makeButtonHandler_stopImmediatePropagation': true, + }, waitForLazyAndRetrigger) + : makeAsyncHandler.call({ + '__makeAsyncHandler_stopImmediatePropagation': true, + }, waitForLazyAndRetrigger, true); + registerLoadingEffectHandler(buttonEl, eventType, loadingEffectHandler); + } } - document.body.classList.add('o_lazy_js_waiting'); + for (const formEl of document.querySelectorAll('form:not(.o_no_wait_lazy_js)')) { + registerLoadingEffectHandler(formEl, 'submit', ev => { + ev.preventDefault(); + ev.stopImmediatePropagation(); + }); + } } /** - * Unblocks the DOM sections blocked by @see waitLazy and removes the related - * 'o_wait_lazy_js' class from the whole DOM. + * Undo what @see waitLazy did. */ function stopWaitingLazy() { if (!waitingLazy) { @@ -47,16 +133,11 @@ function stopWaitingLazy() { } waitingLazy = false; - var lazyEls = document.querySelectorAll('.o_wait_lazy_js'); - for (var i = 0; i < lazyEls.length; i++) { - var element = lazyEls[i]; - blockEvents.forEach(function (evType) { - element.removeEventListener(evType, blockFunction); - }); - element.classList.remove('o_wait_lazy_js'); - } - document.body.classList.remove('o_lazy_js_waiting'); + + for (const { el, type, handler } of loadingEffectHandlers) { + el.removeEventListener(type, handler, {capture: true}); + } } // Start waiting for lazy loading as soon as the DOM is available @@ -102,4 +183,5 @@ function _loadScripts(scripts, index) { export default { loadScripts: _loadScripts, allScriptsLoaded: _allScriptsLoaded, + registerPageReadinessDelay: retriggeringWaitingProms.push.bind(retriggeringWaitingProms), }; diff --git a/addons/web/static/src/legacy/js/public/public_root_instance.js b/addons/web/static/src/legacy/js/public/public_root_instance.js index b18036634c1..dcb12339eb3 100644 --- a/addons/web/static/src/legacy/js/public/public_root_instance.js +++ b/addons/web/static/src/legacy/js/public/public_root_instance.js @@ -1,4 +1,7 @@ /** @odoo-module alias=root.widget */ import { PublicRoot, createPublicRoot } from "./public_root"; +import lazyloader from "@web/legacy/js/public/lazyloader"; -export default createPublicRoot(PublicRoot); +const prom = createPublicRoot(PublicRoot); +lazyloader.registerPageReadinessDelay(prom); +export default prom; diff --git a/addons/web/static/src/legacy/scss/lazyloader.scss b/addons/web/static/src/legacy/scss/lazyloader.scss index fb3f39f4086..797a7c8a158 100644 --- a/addons/web/static/src/legacy/scss/lazyloader.scss +++ b/addons/web/static/src/legacy/scss/lazyloader.scss @@ -1,8 +1 @@ - -// Disable buttons and links marked with the o_wait_lazy_js class or in a -// section which is marked with the o_wait_lazy_js class. -a[href], button, input[type="submit"], input[type="button"], .btn { - &.o_wait_lazy_js, .o_wait_lazy_js & { - pointer-events: none; - } -} +// TODO remove this file in master diff --git a/addons/website/static/src/js/content/website_root_instance.js b/addons/website/static/src/js/content/website_root_instance.js index 4a17d5690c2..2780ea866f2 100644 --- a/addons/website/static/src/js/content/website_root_instance.js +++ b/addons/website/static/src/js/content/website_root_instance.js @@ -1,10 +1,11 @@ /** @odoo-module alias=root.widget */ import { createPublicRoot } from "@web/legacy/js/public/public_root"; +import lazyloader from "@web/legacy/js/public/lazyloader"; import { WebsiteRoot } from "./website_root"; import { loadBundle } from "@web/core/assets"; -export default createPublicRoot(WebsiteRoot).then(async (rootInstance) => { +const prom = createPublicRoot(WebsiteRoot).then(async rootInstance => { // This data attribute is set by the WebsitePreview client action for a // restricted editor user. if (window.frameElement && window.frameElement.dataset.loadWysiwyg === 'true') { @@ -13,3 +14,5 @@ export default createPublicRoot(WebsiteRoot).then(async (rootInstance) => { } return rootInstance; }); +lazyloader.registerPageReadinessDelay(prom); +export default prom; diff --git a/addons/website/static/src/scss/website.scss b/addons/website/static/src/scss/website.scss index ffa4cefb702..472b50bd17f 100644 --- a/addons/website/static/src/scss/website.scss +++ b/addons/website/static/src/scss/website.scss @@ -1632,8 +1632,8 @@ $ribbon-padding: 100px; } .o_website_btn_loading { - cursor: wait; opacity: $btn-disabled-opacity; + .fa:not(.fa-spin) { display: none; }