[FIX] web_editor, website: leave edit mode from any website component
This commit reworks the way the edit mode is left, from the client action. It does 2 things: 1/ It keeps the SnippetsMenu as long as the iframe is reloading. Once everything is ready, the SnippetsMenu is removed with a transition. 2/ It prompts the "Discard your changes" modal when switching language in edit mode. 1/ Before this commit, the WysiwygAdapter was dismounted before reloading the iframe. It was simpler this way, because the OdooEditor has set some listeners on the editable document, which will crash during a reload of the iframe. Now, in a leaveEditMode method, the WysiwygAdapter will make a copy of the snippet's menu html element, and display that skeleton while the editor is destroyed. It will also check if the editable is dirty and prompt a modal to the user if needed, as it was before. Some of the code to display that "fake" snippets menu is shared with the Editor component which uses it to unmount and remount the WysiwygAdapter while reloading the iframe (in a _getDummySnippetsEl method). 2/ With that, the quit method, passed as a prop to the WysiwygAdapter, and played in the new leaveEditMode method, is updated with two parameters: an onLeave function that will be played when the component is unmounted, and a reloadIframe boolean. As the WysiwygAdapter listens to a new 'LEAVE-EDIT-MODE' event using these parameters, it allows for any website component to request leaving the edit mode, with a reload or not, and execute an action after that. This fixes [1], which was leaving the edit mode without going through the WysiwygAdapter, and so, without checking if the editable was dirty or not (i.e. without prompting the "Discard your changes" modal). Related to https://github.com/odoo/odoo/commit/31cc10b91dc7762e23b4bde9b945be0c4ce3fe3b [1]: https://github.com/odoo/odoo/commit/11429329b8dea2dd0b2496dc8c1c8627751cccff task-2687506 Part-of: odoo/odoo#97408
This commit is contained in:
@@ -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');
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<void>}
|
||||
*/
|
||||
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<void>}
|
||||
*/
|
||||
async destroyAfterTransition() {
|
||||
destroyAfterTransition() {
|
||||
this.state.showWysiwyg = false;
|
||||
await this.props.reloadIframe();
|
||||
this.websiteContext.edition = false;
|
||||
this.websiteService.unblockIframe();
|
||||
}
|
||||
}
|
||||
WebsiteEditorComponent.components = { WysiwygAdapterComponent };
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user