diff --git a/addons/web_editor/static/src/js/editor/snippets.editor.js b/addons/web_editor/static/src/js/editor/snippets.editor.js index 4b416d96c0c..fbad9839dde 100644 --- a/addons/web_editor/static/src/js/editor/snippets.editor.js +++ b/addons/web_editor/static/src/js/editor/snippets.editor.js @@ -2214,22 +2214,29 @@ var SnippetsMenu = Widget.extend({ async _execWithLoadingEffect(action, contentLoading = true, delay = 500) { const mutexExecResult = this._mutex.exec(action); if (!this.loadingTimers[contentLoading]) { - this.loadingTimers[contentLoading] = setTimeout(() => { + const addLoader = () => { this.loadingElements[contentLoading] = this._createLoadingElement(); if (contentLoading) { this.$snippetEditorArea.append(this.loadingElements[contentLoading]); } else { this.el.appendChild(this.loadingElements[contentLoading]); } - }, delay); + }; + if (delay) { + this.loadingTimers[contentLoading] = setTimeout(addLoader, delay); + } else { + addLoader(); + } this._mutex.getUnlockedDef().then(() => { // Note: we remove the loading element at the end of the // execution queue *even if subsequent actions are content // related or not*. This is a limitation of the loading feature, // the goal is still to limit the number of elements in that // queue anyway. - clearTimeout(this.loadingTimers[contentLoading]); - this.loadingTimers[contentLoading] = undefined; + if (delay) { + clearTimeout(this.loadingTimers[contentLoading]); + this.loadingTimers[contentLoading] = undefined; + } if (this.loadingElements[contentLoading]) { this.loadingElements[contentLoading].remove(); diff --git a/addons/website/static/src/js/editor/snippets.editor.js b/addons/website/static/src/js/editor/snippets.editor.js index c5d64e11d54..77229934eac 100644 --- a/addons/website/static/src/js/editor/snippets.editor.js +++ b/addons/website/static/src/js/editor/snippets.editor.js @@ -154,37 +154,56 @@ weSnippetEditor.Class.include({ */ async _onThemeTabClick(ev) { // Note: nothing async here but start the loading effect asap - this._execWithLoadingEffect(async () => new Promise(resolve => setTimeout(() => resolve(), 0)), false, 0); + let releaseLoader; + try { + const promise = new Promise(resolve => releaseLoader = resolve); + this._execWithLoadingEffect(() => promise, false, 0); + // loader is added to the DOM synchronously + await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))); + // ensure loader is rendered: first call asks for the (already done) DOM update, + // second call happens only after rendering the first "updates" - if (!this.topFakeOptionEl) { - let el; - for (const [elementName, title] of this.optionsTabStructure) { - const newEl = document.createElement(elementName); - newEl.dataset.name = title; - if (el) { - el.appendChild(newEl); - } else { - this.topFakeOptionEl = newEl; + if (!this.topFakeOptionEl) { + let el; + for (const [elementName, title] of this.optionsTabStructure) { + const newEl = document.createElement(elementName); + newEl.dataset.name = title; + if (el) { + el.appendChild(newEl); + } else { + this.topFakeOptionEl = newEl; + } + el = newEl; } - el = newEl; + this.bottomFakeOptionEl = el; + this.el.appendChild(this.topFakeOptionEl); } - this.bottomFakeOptionEl = el; - this.el.appendChild(this.topFakeOptionEl); + + // Need all of this in that order so that: + // - the element is visible and can be enabled and the onFocus method is + // called each time. + // - the element is hidden afterwards so it does not take space in the + // DOM, same as the overlay which may make a scrollbar appear. + this.topFakeOptionEl.classList.remove('d-none'); + const editorPromise = this._activateSnippet($(this.bottomFakeOptionEl)); + releaseLoader(); // because _activateSnippet uses the same mutex as the loader + releaseLoader = undefined; + const editor = await editorPromise; + this.topFakeOptionEl.classList.add('d-none'); + editor.toggleOverlay(false); + + this._updateLeftPanelContent({ + tab: this.tabs.THEME, + }); + } catch (e) { + // Normally the loading effect is removed in case of error during the action but here + // the actual activity is happening outside of the action, the effect must therefore + // be cleared in case of error as well + if (releaseLoader) { + releaseLoader(); + } + throw e; } - - // Need all of this in that order so that: - // - the element is visible and can be enabled and the onFocus method is - // called each time. - // - the element is hidden afterwards so it does not take space in the - // DOM, same as the overlay which may make a scrollbar appear. - this.topFakeOptionEl.classList.remove('d-none'); - const editor = await this._activateSnippet($(this.bottomFakeOptionEl)); - this.topFakeOptionEl.classList.add('d-none'); - editor.toggleOverlay(false); - - this._updateLeftPanelContent({ - tab: this.tabs.THEME, - }); }, });