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 4c576baecd4..5827253ef09 100644 --- a/addons/web_editor/static/src/js/editor/snippets.editor.js +++ b/addons/web_editor/static/src/js/editor/snippets.editor.js @@ -1514,6 +1514,7 @@ var SnippetsMenu = Widget.extend({ } core.bus.off('deactivate_snippet', this, this._onDeactivateSnippet); $(document.body).off('click', this._checkEditorToolbarVisibilityCallback); + this.el.ownerDocument.body.classList.remove('editor_has_snippets'); }, //-------------------------------------------------------------------------- diff --git a/addons/website/static/src/client_actions/website_preview/website_preview.js b/addons/website/static/src/client_actions/website_preview/website_preview.js index 54252022d77..e4d2ed87a5b 100644 --- a/addons/website/static/src/client_actions/website_preview/website_preview.js +++ b/addons/website/static/src/client_actions/website_preview/website_preview.js @@ -268,9 +268,19 @@ export class WebsitePreview extends Component { }); } else if (classList.contains('js_change_lang') && isEditing) { ev.preventDefault(); - // The switch to the right language is handled by the - // Website Root, inside the iframe. - this.websiteService.leaveEditMode(); + const lang = linkEl.dataset['url_code']; + // The "edit_translations" search param coming from keep_query + // is removed, and the hash is added. + const destinationUrl = new URL(href, window.location); + destinationUrl.searchParams.delete('edit_translations'); + destinationUrl.hash = this.websiteService.contentWindow.location.hash; + const forceLangUrl = `/website/lang/${lang}?r=${destinationUrl.toString()}`; + this.websiteService.bus.trigger('LEAVE-EDIT-MODE', { + onLeave: () => { + this.websiteService.goToWebsite({ path: forceLangUrl }); + }, + reloadIframe: false, + }); } else if (href && target !== '_blank' && !isEditing && this._isTopWindowURL(linkEl)) { ev.preventDefault(); this.router.redirect(href); diff --git a/addons/website/static/src/client_actions/website_preview/website_preview.scss b/addons/website/static/src/client_actions/website_preview/website_preview.scss index e031711dcbb..81b7558e622 100644 --- a/addons/website/static/src/client_actions/website_preview/website_preview.scss +++ b/addons/website/static/src/client_actions/website_preview/website_preview.scss @@ -57,7 +57,7 @@ } } -.o_website_fullscreen, .editor_has_snippets { +.o_website_fullscreen, .editor_has_snippets, .editor_has_dummy_snippets { header { .o_main_navbar { height: 0px; diff --git a/addons/website/static/src/components/editor/editor.js b/addons/website/static/src/components/editor/editor.js index d1f737f4bea..808f846845e 100644 --- a/addons/website/static/src/components/editor/editor.js +++ b/addons/website/static/src/components/editor/editor.js @@ -5,7 +5,7 @@ import { useService } from '@web/core/utils/hooks'; import { WysiwygAdapterComponent } from '../wysiwyg_adapter/wysiwyg_adapter'; import { useActiveElement } from '@web/core/ui/ui_service'; -const { markup, Component, useState, useChildSubEnv, useEffect, onWillStart, onMounted } = owl; +const { markup, Component, useState, useChildSubEnv, useEffect, onWillStart, onMounted, onWillUnmount } = owl; export class WebsiteEditorComponent extends Component { /** @@ -41,6 +41,12 @@ export class WebsiteEditorComponent extends Component { } }); + onWillUnmount(() => { + if (this.onWillUnmount) { + this.onWillUnmount(); + } + }); + useActiveElement('wysiwyg-adapter'); } /** @@ -61,7 +67,10 @@ export class WebsiteEditorComponent extends Component { */ wysiwygReady() { this.websiteContext.snippetsLoaded = true; - this.state.reloading = false; + if (this.state.reloading) { + document.body.classList.remove('editor_has_dummy_snippets'); + this.state.reloading = false; + } this.wysiwygOptions.invalidateSnippetCache = false; this.websiteService.unblockIframe(); this.websiteService.hideLoader(); @@ -76,13 +85,10 @@ export class WebsiteEditorComponent extends Component { willReload(widgetEl) { this.websiteService.blockIframe(); if (widgetEl) { - widgetEl.querySelectorAll('#oe_manipulators').forEach(el => el.remove()); - widgetEl.querySelectorAll('we-input input').forEach(input => { - input.setAttribute('value', input.closest('we-input').dataset.selectStyle || ''); - }); this.loadingDummy = markup(widgetEl.innerHTML); } this.state.reloading = true; + document.body.classList.add('editor_has_dummy_snippets'); } /** * Dismount the editor and reload the iframe. @@ -106,12 +112,18 @@ export class WebsiteEditorComponent extends Component { } /** * Blocks the iframe and start the hiding transition. - * + * @param {Boolean} [reloadIframe=true] + * @param {Function} onLeave A callback that will be played after the + * transition, when the component is unmounted. * @returns {Promise} */ - async quit() { - this.websiteService.blockIframe(true, 400); - document.body.classList.remove('editor_has_snippets'); + async quit({ reloadIframe = true, onLeave } = {}) { + this.onWillUnmount = onLeave; + if (reloadIframe) { + this.websiteService.blockIframe(true); + await this.props.reloadIframe(); + this.websiteService.unblockIframe(); + } this.websiteContext.snippetsLoaded = false; setTimeout(this.destroyAfterTransition.bind(this), 400); } @@ -120,11 +132,9 @@ export class WebsiteEditorComponent extends Component { * * @returns {Promise} */ - async destroyAfterTransition() { + destroyAfterTransition() { this.state.showWysiwyg = false; - await this.props.reloadIframe(); this.websiteContext.edition = false; - this.websiteService.unblockIframe(); } } WebsiteEditorComponent.components = { WysiwygAdapterComponent }; diff --git a/addons/website/static/src/components/editor/editor.scss b/addons/website/static/src/components/editor/editor.scss index 171daac7dca..83b15a2c762 100644 --- a/addons/website/static/src/components/editor/editor.scss +++ b/addons/website/static/src/components/editor/editor.scss @@ -1,4 +1,4 @@ -.editor_has_snippets { +.editor_has_snippets, .editor_has_dummy_snippets { .o_notification_manager { @include o-position-absolute($top: map-get($spacers, 2), $right: calc(#{$o-we-sidebar-width} + 0.5rem)); } diff --git a/addons/website/static/src/components/translator/translator.js b/addons/website/static/src/components/translator/translator.js index 056e441a10c..c7e42d47d38 100644 --- a/addons/website/static/src/components/translator/translator.js +++ b/addons/website/static/src/components/translator/translator.js @@ -91,11 +91,9 @@ export class WebsiteTranslator extends WebsiteEditorComponent { /** * @override */ - async destroyAfterTransition() { + destroyAfterTransition() { this.state.showWysiwyg = false; - await this.props.reloadIframe(); this.websiteContext.translation = false; - this.websiteService.unblockIframe(); } get savableSelector() { diff --git a/addons/website/static/src/components/wysiwyg_adapter/wysiwyg_adapter.js b/addons/website/static/src/components/wysiwyg_adapter/wysiwyg_adapter.js index e0f59cfdafc..579cb84a899 100644 --- a/addons/website/static/src/components/wysiwyg_adapter/wysiwyg_adapter.js +++ b/addons/website/static/src/components/wysiwyg_adapter/wysiwyg_adapter.js @@ -1,17 +1,17 @@ /** @odoo-module */ import { ComponentAdapter } from 'web.OwlCompatibility'; -import { _t } from '@web/core/l10n/translation'; import { useWowlService } from '@web/legacy/utils'; import { useHotkey } from '@web/core/hotkeys/hotkey_hook'; import { setEditableWindow } from 'web_editor.utils'; +import { useBus } from "@web/core/utils/hooks"; import { EditMenuDialog, MenuDialog } from "../dialog/edit_menu"; import { WebsiteDialog } from '../dialog/dialog'; import { PageOption } from "./page_options"; -const { onWillStart, useEffect } = owl; +const { onWillStart, useEffect, onWillUnmount } = owl; /** * This component adapts the Wysiwyg widget from @web_editor/wysiwyg.js. @@ -32,6 +32,8 @@ export class WysiwygAdapterComponent extends ComponentAdapter { this.dialogs = useWowlService('dialog'); this.action = useWowlService('action'); + useBus(this.websiteService.bus, 'LEAVE-EDIT-MODE', (ev) => this.leaveEditMode(ev.detail)); + this.oeStructureSelector = '#wrapwrap .oe_structure[data-oe-xpath][data-oe-id]'; this.oeFieldSelector = '#wrapwrap [data-oe-field]:not([data-oe-sanitize-prevent-edition])'; this.oeCoverSelector = '#wrapwrap .s_cover[data-res-model], #wrapwrap .o_record_cover_container[data-res-model]'; @@ -98,6 +100,12 @@ export class WysiwygAdapterComponent extends ComponentAdapter { }; }, () => []); + onWillUnmount(() => { + if (this.dummyWidgetEl) { + this.dummyWidgetEl.remove(); + document.body.classList.remove('editor_has_dummy_snippets'); + } + }); } /** * @override @@ -178,6 +186,38 @@ export class WysiwygAdapterComponent extends ComponentAdapter { }, ); } + leaveEditMode({ onLeave, forceLeave, onStay, reloadIframe = true } = {}) { + const leave = () => { + this.dummyWidgetEl = this._getDummmySnippetsEl(); + this.widget.el.parentElement.appendChild(this.dummyWidgetEl); + document.body.classList.add('editor_has_dummy_snippets'); + // The wysiwyg is destroyed to avoid listeners from the OdooEditor + // and the SnippetsMenu to be triggered when reloading the iframe. + this.widget.destroy(); + this.props.quitCallback({ onLeave, reloadIframe }); + }; + + if (!forceLeave && this._isDirty()) { + let leaving = false; + // The onStay/leave callbacks are not passed directly as + // primaryClick/secondaryClick props, so that closing the dialog + // with "esc" or the top right cross icon also executes onStay. + this.dialogs.add(WebsiteDialog, { + body: this.env._t("If you discard the current edits, all unsaved changes will be lost. You can cancel to return to edit mode."), + primaryClick: () => leaving = true, + }, { + onClose: () => { + if (leaving) { + leave(); + } else if (onStay) { + onStay(); + } + } + }); + } else { + leave(); + } + } //-------------------------------------------------------------------------- // Private @@ -277,7 +317,7 @@ export class WysiwygAdapterComponent extends ComponentAdapter { _addEditorMessages() { const $wrap = this.$editable.find('.oe_structure.oe_empty, [data-oe-type="html"]'); this.$editorMessageElement = $wrap.not('[data-editor-message]') - .attr('data-editor-message', _t('DRAG BUILDING BLOCKS HERE')); + .attr('data-editor-message', this.env._t('DRAG BUILDING BLOCKS HERE')); $wrap.filter(':empty').attr('contenteditable', false); } /** @@ -559,10 +599,10 @@ export class WysiwygAdapterComponent extends ComponentAdapter { * @private */ async _onSaveRequest(event) { - let callback = () => this.props.quitCallback(); + let callback = () => this.leaveEditMode({ forceLeave: true }); if (event.data.reload || event.data.reloadEditor) { this.widget.trigger_up('disable_loading_effects'); - this.props.willReload(this.widget.el); + this.props.willReload(this._getDummmySnippetsEl()); callback = async () => { if (event.data.onSuccess) { await event.data.onSuccess(); @@ -581,8 +621,11 @@ export class WysiwygAdapterComponent extends ComponentAdapter { callback = () => window.location = `/web#action=website.website_preview&website_id=${websiteId}&path=${currentPath}&enable_editor=1`; } else if (event.data.action) { callback = () => { - this.websiteService.leaveEditMode(); - this.action.doAction(event.data.action); + this.leaveEditMode({ + onLeave: () => this.action.doAction(event.data.action), + forceLeave: true, + reloadIframe: false, + }); }; } if (this._isDirty()) { @@ -610,24 +653,7 @@ export class WysiwygAdapterComponent extends ComponentAdapter { * @private */ _onCancelRequest(event) { - if (this._isDirty()) { - let discarding = false; - this.dialogs.add(WebsiteDialog, { - body: _t("If you discard the current edits, all unsaved changes will be lost. You can cancel to return to edit mode."), - primaryClick: () => { - discarding = true; - }, - }, { - onClose: () => { - if (discarding) { - return this.props.quitCallback(); - } - return event.data.onReject(); - } - }); - } else { - return this.props.quitCallback(); - } + this.leaveEditMode({ onStay: event.data.onReject }); } /** * Called when a snippet is about to be cloned in the page. Notifies the @@ -761,6 +787,21 @@ export class WysiwygAdapterComponent extends ComponentAdapter { const views = await this.switchableRelatedViews; event.data.onSuccess(views); } + /** + * This method returns a visual skeleton of the snippets menu, by making a + * copy of the Wysiwyg element. This is used when reloading the iframe or + * leaving the edit mode, so that the widget can be destroyed under the + * hood (ideally, the Wysiwyg would remove its listeners on the document, + * so that they are not triggered during a reload). + */ + _getDummmySnippetsEl() { + const dummySnippetsEl = this.widget.el.cloneNode(true); + dummySnippetsEl.querySelectorAll('#oe_manipulators, .d-none, .oe_snippet_body').forEach(el => el.remove()); + dummySnippetsEl.querySelectorAll('we-input input').forEach(input => { + input.setAttribute('value', input.closest('we-input').dataset.selectStyle || ''); + }); + return dummySnippetsEl; + } } WysiwygAdapterComponent.prototype.events = { 'widgets_start_request': '_onRootEventRequest', diff --git a/addons/website/static/src/js/content/website_root.js b/addons/website/static/src/js/content/website_root.js index 9453913ab85..9f8fc815788 100644 --- a/addons/website/static/src/js/content/website_root.js +++ b/addons/website/static/src/js/content/website_root.js @@ -39,19 +39,6 @@ export const WebsiteRoot = publicRootData.PublicRoot.extend(KeyboardNavigationMi */ start: function () { KeyboardNavigationMixin.start.call(this); - // Compatibility lang change ? - if (!this.$('.js_change_lang').length) { - var $links = this.$('.js_language_selector a:not([data-oe-id])'); - var m = $(_.min($links, function (l) { - return $(l).attr('href').length; - })).attr('href'); - $links.each(function () { - var $link = $(this); - var t = $link.attr('href'); - var l = (t === m) ? "default" : t.split('/')[1]; - $link.data('lang', l).addClass('js_change_lang'); - }); - } // Enable magnify on zommable img this.$('.zoomable img[data-zoom]').zoomOdoo(); @@ -178,7 +165,11 @@ export const WebsiteRoot = publicRootData.PublicRoot.extend(KeyboardNavigationMi */ _onLangChangeClick: function (ev) { ev.preventDefault(); - + // In edit mode, the client action redirects the iframe to the correct + // location with the chosen language. + if (document.body.classList.contains('editor_enable')) { + return; + } var $target = $(ev.currentTarget); // retrieve the hash before the redirect var redirect = { diff --git a/addons/website/static/src/services/website_service.js b/addons/website/static/src/services/website_service.js index a29495cfb8b..3b80493a05a 100644 --- a/addons/website/static/src/services/website_service.js +++ b/addons/website/static/src/services/website_service.js @@ -204,16 +204,6 @@ export const websiteService = { unblockIframe(processId) { bus.trigger('UNBLOCK', { processId }); }, - leaveEditMode() { - // FIXME this does not care about if the page is dirty or not. - - // TODO this should not be needed here, the one who was in - // charge of adding this class should be the one in charge of - // removing it. - document.body.classList.remove('editor_has_snippets'); - context.snippetsLoaded = false; - context.edition = false; - }, showLoader(props) { bus.trigger('SHOW-WEBSITE-LOADER', props); },