[FIX] mass_mailing, web_editor: add missing toolbar in basic theme

Short read version:

This commit gives the Wysiwyg class the attribution of switching between
the two types of text editor toolbar (floating or part of the sidebar),
instead of the SnippetsMenu class, as Wysiwyg owns the Toolbar instance
and has the methods to configure it.

The switching between toolbars is now done by having one instance of each
type (and properly displaying/hiding them), rather than moving the toolbar
element of place in the DOM and reconfiguring it (button handlers, CSS
classes, displayed/hidded buttons, etc), so that the configuration steps
of each toolbar can be done only once.

The floating toolbar is needed for mailings based on the "basic" theme,
where no sidebar/SnippetsMenu is available. Note that it will be only
instantiated if needed.

Long read version:

The 'basic' theme in mass_mailing has no sidebar (SnippetsMenu), so the
floating toolbar should be available instead, in order to give users access
to the text editor's features.

Commit [1] added the `setFolded` method to the SnippetsMenu class, which folds
(hides) the snippets menu and recovers the floating toolbar from the
sidebar container and vice-versa. While the recovered toolbar works well when
`isMobile` is `true` (although with some minor issues described
below), the one for the non-mobile case does not (as it must be properly
reconfigured using the document's Jquery, and not the iframe's, among other
details).

This commit proposes a different approach to the toolbar recovery issue:
if the floating toolbar is needed (mobile or basic theme), a new Toolbar
instance is instantiated and properly configured. That way the Wysiwyg
instance will hold two instances of the toolbar: the one embedded in the
sidebar (snippets menu) and the floating one. This allows for easy
switching between the two types of toolbar, without the need to run the
configuration steps for the toolbar on every switch.

Such toolbar switching is needed in mass_mailing, where a single instance of
the wysiwyg is kept while the user can switch between different records of
mailings. Some of these records could be based on the 'basic' theme
(thus, no sidebar), and other ones based on other themes that do offer
the snippets menu.

After this commit, the toolbar switching is handled by the Wysiwyg class
instead of SnippetsMenu, as Wysiwyg owns the Toolbar instance(s) and has
the methods to configure it.
The `folded` property of SnippetsMenu and the `setFolded` method are
leveraged by this commit, keeping its original purpose and meaning (except
for the toolbar handling).

Before this commit the floating toolbar for the mobile case had the
following issues, that are fixed by this commit:
- The "table" button was (incorrectly) displayed;
- The default colors of the color picker (the top row) were undefined;
- The 'Link' powerbox command and the toolbar icon should open the Dialog,
and not the LinkTools (part of the sidebar).

Finally, this commit removes the SCSS rules that hide the SnippetsMenu
when the theme in "basic", as its display is already handled by
SnippetsMenu's `setFolded` method, introduced by [1].

[1] https://github.com/odoo/odoo/pull/105472/commits/c589939eeaff928997bf6f9735b19710036f9ad6

task-3133259
opw-3165111

Part-of: odoo/odoo#117166
This commit is contained in:
Rodolpho Lima
2023-04-13 16:40:08 +02:00
parent a27fbf4aaf
commit 164df578fc
6 changed files with 119 additions and 77 deletions
@@ -46,11 +46,11 @@ export class MassMailingHtmlField extends HtmlField {
return {
...super.wysiwygOptions,
onIframeUpdated: () => this.onIframeUpdated(),
foldSnippets: device.isMobile,
snippets: 'mass_mailing.email_designer_snippets',
resizable: false,
defaultDataForLinkTools: { isNewWindow: true },
toolbarTemplate: device.isMobile ? 'web_editor.toolbar' : 'mass_mailing.web_editor_toolbar',
toolbarTemplate: 'mass_mailing.web_editor_toolbar',
onWysiwygBlur: () => this.wysiwyg.odooEditor.toolbarHide(),
...this.props.wysiwygOptions,
};
}
@@ -337,7 +337,7 @@ export class MassMailingHtmlField extends HtmlField {
if (editableAreaIsEmpty) {
// unfold to prevent toolbar from going over the menu
this.wysiwyg.snippetsMenu.setFolded(false);
this.wysiwyg.setSnippetsMenuFolded(false);
$themeSelectorNew.appendTo(this.wysiwyg.$iframeBody);
}
@@ -353,9 +353,8 @@ export class MassMailingHtmlField extends HtmlField {
this.wysiwyg.$iframeBody.closest('body').removeClass("o_force_mail_theme_choice");
$themeSelectorNew.remove();
if (device.isMobile) {
this.wysiwyg.snippetsMenu.setFolded(true);
}
this.wysiwyg.setSnippetsMenuFolded(device.isMobile || themeName === 'basic');
this._switchImages(themeParams, $snippets);
@@ -424,6 +423,8 @@ export class MassMailingHtmlField extends HtmlField {
selectedTheme = this._getSelectedTheme(themesParams);
}
this.wysiwyg.setSnippetsMenuFolded(device.isMobile || (selectedTheme && selectedTheme.name === 'basic'));
this.wysiwyg.$iframeBody.find('.iframe-utils-zone').removeClass('d-none');
if (this.env.mailingFilterTemplates && this.wysiwyg) {
this._hideIrrelevantTemplates();
+60 -2
View File
@@ -3,6 +3,7 @@
import Wysiwyg from "web_editor.wysiwyg";
import MassMailingSnippetsMenu from "mass_mailing.snippets.editor";
import {closestElement} from "@web_editor/js/editor/odoo-editor/src/OdooEditor";
import Toolbar from "web_editor.toolbar";
const MassMailingWysiwyg = Wysiwyg.extend({
//--------------------------------------------------------------------------
@@ -17,15 +18,72 @@ const MassMailingWysiwyg = Wysiwyg.extend({
e.preventDefault();
}
});
this.snippetsMenuToolbar = this.toolbar;
return res;
},
toggleLinkTools(options = {}) {
this._super({
...options,
// Always open the dialog for the basic theme as it has no sidebar.
forceDialog: options.forceDialog || this.$iframeBody.find('.o_basic_theme').length
// Always open the dialog when the sidebar is folded.
forceDialog: options.forceDialog || this.snippetsMenu.folded
});
if (this.snippetsMenu.folded) {
// Hide toolbar and avoid it being re-displayed after getDeepRange.
this.odooEditor.document.getSelection().collapseToEnd();
}
},
/**
* Sets SnippetsMenu fold state and switches toolbar.
* Instantiates a new floating Toolbar if needed.
*
* @param {Boolean} fold
*/
setSnippetsMenuFolded: async function (fold = true) {
if (fold) {
this.snippetsMenu.setFolded(true);
if (!this.floatingToolbar) {
// Instantiate and configure new toolbar.
this.floatingToolbar = new Toolbar(this, 'web_editor.toolbar');
this.toolbar = this.floatingToolbar;
await this.toolbar.appendTo(document.createElement('void'));
this._configureToolbar({ snippets: false });
this._updateEditorUI();
this.setCSSVariables(this.toolbar.el);
this.odooEditor.setupToolbar(this.toolbar.el);
if (this.odooEditor.isMobile) {
document.body.querySelector('.o_mail_body').prepend(this.toolbar.el);
} else {
document.body.append(this.toolbar.el);
}
} else {
this.toolbar = this.floatingToolbar;
}
this.toolbar.el.classList.remove('d-none');
this.odooEditor.autohideToolbar = true;
this.odooEditor.toolbarHide();
} else {
this.snippetsMenu.setFolded(false);
this.toolbar = this.snippetsMenuToolbar;
this.odooEditor.autohideToolbar = false;
if (this.floatingToolbar) {
this.floatingToolbar.el.classList.add('d-none');
}
}
this.odooEditor.toolbar = this.toolbar.el;
},
/**
* @override
*/
openMediaDialog: function() {
this._super(...arguments);
// Opening the dialog in the outer document does not trigger the selectionChange
// (that would normally hide the toolbar) in the iframe.
if (this.snippetsMenu.folded) {
this.odooEditor.toolbarHide();
}
},
//--------------------------------------------------------------------------
@@ -302,10 +302,6 @@ body.editor_enable.o_basic_theme.o_in_iframe {
#web_editor-top-edit .note-popover .popover {
right: 0 !important;
}
#oe_snippets {
display: none;
}
}
.note-editable .o_layout {
@@ -627,30 +627,7 @@ export class OdooEditor extends EventTarget {
// -------
if (this.options.toolbar) {
this.toolbar = this.options.toolbar;
this.bindExecCommand(this.toolbar);
// Ensure anchors in the toolbar don't trigger a hash change.
const toolbarAnchors = this.toolbar.querySelectorAll('a');
toolbarAnchors.forEach(a => a.addEventListener('click', e => e.preventDefault()));
for (const colorLabel of this.toolbar.querySelectorAll('label')) {
colorLabel.addEventListener('mousedown', ev => {
// Hack to prevent loss of focus (done by preventDefault) while still opening
// color picker dialog (which is also prevented by preventDefault on chrome,
// except when click detail is 2, which happens on a double-click but isn't
// triggered by a dblclick event)
if (ev.detail < 2) {
ev.preventDefault();
ev.currentTarget.dispatchEvent(new MouseEvent('click', { detail: 2 }));
}
});
colorLabel.addEventListener('input', ev => {
this.document.execCommand(ev.target.name, false, ev.target.value);
this.updateColorpickerLabels();
});
}
if (this.isMobile) {
this.editable.before(this.toolbar);
}
this.setupToolbar(this.options.toolbar);
}
// placeholder hint
if (editable.textContent === '' && this.options.placeholder) {
@@ -677,6 +654,31 @@ export class OdooEditor extends EventTarget {
this._columnUi.remove();
}
setupToolbar(toolbar) {
this.toolbar = toolbar;
this.autohideToolbar = this.options.autohideToolbar;
this.bindExecCommand(this.toolbar);
// Ensure anchors in the toolbar don't trigger a hash change.
const toolbarAnchors = this.toolbar.querySelectorAll('a');
toolbarAnchors.forEach(a => a.addEventListener('click', e => e.preventDefault()));
for (const colorLabel of this.toolbar.querySelectorAll('label')) {
colorLabel.addEventListener('mousedown', ev => {
// Hack to prevent loss of focus (done by preventDefault) while still opening
// color picker dialog (which is also prevented by preventDefault on chrome,
// except when click detail is 2, which happens on a double-click but isn't
// triggered by a dblclick event)
if (ev.detail < 2) {
ev.preventDefault();
ev.currentTarget.dispatchEvent(new MouseEvent('click', { detail: 2 }));
}
});
colorLabel.addEventListener('input', ev => {
this.document.execCommand(ev.target.name, false, ev.target.value);
this.updateColorpickerLabels();
});
}
}
resetContent(value) {
value = value || '<p><br></p>';
this.editable.innerHTML = value;
@@ -2731,8 +2733,10 @@ export class OdooEditor extends EventTarget {
* @param {boolean} [show]
*/
_updateToolbar(show) {
if (!this.options.toolbar) return;
if (!this.options.autohideToolbar && this.toolbar.style.visibility !== 'visible') {
if (!this.toolbar) {
return;
}
if (!this.autohideToolbar && this.toolbar.style.visibility !== 'visible') {
this.toolbar.style.visibility = 'visible';
}
@@ -2751,7 +2755,7 @@ export class OdooEditor extends EventTarget {
}
}
}
if (this.options.autohideToolbar && !this.toolbar.contains(sel.anchorNode)) {
if (this.autohideToolbar && !this.toolbar.contains(sel.anchorNode)) {
if (!this.isMobile) {
if (show !== undefined) {
this.toolbar.style.visibility = show ? 'visible' : 'hidden';
@@ -2883,13 +2887,13 @@ export class OdooEditor extends EventTarget {
undoButton && undoButton.classList.toggle('disabled', !this.historyCanUndo());
const redoButton = this.toolbar.querySelector('#redo');
redoButton && redoButton.classList.toggle('disabled', !this.historyCanRedo());
if (this.options.autohideToolbar && !this.isMobile && !this.toolbar.contains(sel.anchorNode)) {
if (this.autohideToolbar && !this.isMobile && !this.toolbar.contains(sel.anchorNode)) {
this._positionToolbar();
}
}
updateToolbarPosition() {
if (
this.options.autohideToolbar &&
this.autohideToolbar &&
!this.isMobile &&
getComputedStyle(this.toolbar).visibility === 'visible'
) {
@@ -1850,12 +1850,9 @@ var SnippetsMenu = Widget.extend({
this.$el = this.window.$(this.$el);
this.$el.data('snippetMenu', this);
this.folded = !!this.options.foldSnippets;
this.customizePanel = document.createElement('div');
this.customizePanel.classList.add('o_we_customize_panel', 'd-none');
// adds toolbar if not folded
this.setFolded(this.folded);
this._addToolbar();
this._checkEditorToolbarVisibilityCallback = this._checkEditorToolbarVisibility.bind(this);
$(this.options.wysiwyg.odooEditor.document.body).on('click', this._checkEditorToolbarVisibilityCallback);
@@ -2152,8 +2149,6 @@ var SnippetsMenu = Widget.extend({
this.el.classList.toggle('d-none', foldState);
this.el.ownerDocument.body.classList.toggle('editor_has_snippets', !foldState);
this.folded = !!foldState;
// "add" toolbar to set it inside the snippet menu/in the body
this._addToolbar();
},
/**
* Get the editable area.
@@ -4157,7 +4152,6 @@ var SnippetsMenu = Widget.extend({
},
_addToolbar(toolbarMode = "text") {
if (this.folded) {
this._addToolbarToOriginalPosition();
return;
}
let titleText = _t("Inline Text");
@@ -4174,35 +4168,24 @@ var SnippetsMenu = Widget.extend({
}
this.options.wysiwyg.toolbar.el.classList.remove('oe-floating');
if (!this._$toolbarContainer) {
// Create toolbar custom container.
this._$toolbarContainer = $('<WE-CUSTOMIZEBLOCK-OPTIONS id="o_we_editor_toolbar_container"/>');
const $title = $("<we-title><span>" + titleText + "</span></we-title>");
this._$toolbarContainer.append($title);
this._$toolbarContainer.append(this.options.wysiwyg.toolbar.$el);
$(this.customizePanel).append(this._$toolbarContainer);
// Create table-options custom container.
const $customizeTableBlock = $(QWeb.render('web_editor.toolbar.table-options'));
this.options.wysiwyg.odooEditor.bindExecCommand($customizeTableBlock[0]);
$(this.customizePanel).append($customizeTableBlock);
this._$removeFormatButton = this.options.wysiwyg.toolbar.$el.find('#removeFormat');
$title.append(this._$removeFormatButton);
this._$toolbarContainer.append(this.options.wysiwyg.toolbar.$el);
}
// Create toolbar custom container.
this._$toolbarContainer = $('<WE-CUSTOMIZEBLOCK-OPTIONS id="o_we_editor_toolbar_container"/>');
const $title = $("<we-title><span>" + titleText + "</span></we-title>");
this._$toolbarContainer.append($title);
this._$toolbarContainer.append(this.options.wysiwyg.toolbar.$el);
$(this.customizePanel).append(this._$toolbarContainer);
// Create table-options custom container.
const $customizeTableBlock = $(QWeb.render('web_editor.toolbar.table-options'));
this.options.wysiwyg.odooEditor.bindExecCommand($customizeTableBlock[0]);
$(this.customizePanel).append($customizeTableBlock);
this._$removeFormatButton = this.options.wysiwyg.toolbar.$el.find('#removeFormat');
$title.append(this._$removeFormatButton);
this._$toolbarContainer.append(this.options.wysiwyg.toolbar.$el);
this._checkEditorToolbarVisibility();
},
_addToolbarToOriginalPosition: function () {
const toolbar = this.options.wysiwyg.toolbar.el;
toolbar.classList.add('oe-floating');
if (this.options.wysiwyg.odooEditor.isMobile) {
const editorEditable = this.options.wysiwyg.odooEditor.editable;
editorEditable.before(toolbar);
} else if (this.options.autohideToolbar) {
document.body.appendChild(toolbar);
}
},
/**
* Update editor UI visibility based on the current range.
*/
@@ -1516,7 +1516,7 @@ const Wysiwyg = Widget.extend({
break;
}
};
if (!this.options.snippets) {
if (!options.snippets) {
$toolbar.find('#justify, #media-insert').remove();
}
$toolbar.find('#media-insert, #media-replace, #media-description').click(openTools);
@@ -1874,7 +1874,7 @@ const Wysiwyg = Widget.extend({
].join(',')).toggleClass('d-none', !isInMedia);
// The image replace button is in the image options when the sidebar
// exists.
if (this.snippetsMenu && $target.is('img')) {
if (this.snippetsMenu && !this.snippetsMenu.folded && $target.is('img')) {
this.toolbar.$el.find('#media-replace').toggleClass('d-none', true);
}
// Only show the image-transform, image-crop and media-description
@@ -1931,7 +1931,7 @@ const Wysiwyg = Widget.extend({
// Always hide the unlink button on media.
this.toolbar.$el.find('#unlink').toggleClass('d-none', true);
// Show the floatingtoolbar on the topleft of the media.
if (this.options.autohideToolbar) {
if (this.odooEditor.autohideToolbar && !this.odooEditor.isMobile) {
const imagePosition = this.lastMediaClicked.getBoundingClientRect();
this.toolbar.$el.css({
visibility: 'visible',
@@ -2461,7 +2461,7 @@ const Wysiwyg = Widget.extend({
}
},
_onSelectionChange() {
if (this.options.autohideToolbar) {
if (this.odooEditor.autohideToolbar) {
const isVisible = this.linkPopover && this.linkPopover.el.offsetParent;
if (isVisible && !this.odooEditor.document.getSelection().isCollapsed) {
this.linkPopover.hide();