[REF] mass_mailing: adapt to new jabberwock editor

This commit is contained in:
Gorash
2020-10-14 10:29:10 +00:00
committed by David Monjoie
parent 95d0ca4b2c
commit 3dc721ab71
10 changed files with 371 additions and 1063 deletions
@@ -178,7 +178,9 @@ var BarcodeEvents = core.Class.extend(mixins.PropertiesMixin, {
// have no way of redispatching 'genuine' key events. Resent events
// don't trigger native event handlers of elements. So this means that
// our fake events will not appear in eg. an <input> element.
if ((this.element_is_editable(e.target) && !$(e.target).data('enableBarcode')) && e.target.getAttribute("barcode_events") !== "true")
// Don't catch events targeting a shadow DOM because we must keep the
// behavior specific to the content.
if (((this.element_is_editable(e.target) || e.target.shadowRoot) && !$(e.target).data('enableBarcode')) && e.target.getAttribute("barcode_events") !== "true")
return;
// Catch and buffer the event
@@ -1,10 +1,10 @@
odoo.define('mass_mailing.snippets.options', function (require) {
"use strict";
var options = require('web_editor.snippets.options');
var snippetOptions = require('web_editor.snippets.options');
// Snippet option for resizing image and column width inline like excel
options.registry.mass_mailing_sizing_x = options.Class.extend({
snippetOptions.registry.mass_mailing_sizing_x = snippetOptions.SnippetOptionWidget.extend({
/**
* @override
*/
@@ -79,7 +79,7 @@ options.registry.mass_mailing_sizing_x = options.Class.extend({
},
});
options.registry.mass_mailing_table_item = options.Class.extend({
snippetOptions.registry.mass_mailing_table_item = snippetOptions.SnippetOptionWidget.extend({
onClone: function (options) {
this._super.apply(this, arguments);
@@ -127,7 +127,7 @@ options.registry.mass_mailing_table_item = options.Class.extend({
// Adding compatibility for the outlook compliance of mailings.
// Commit of such compatibility : a14f89c8663c9cafecb1cc26918055e023ecbe42
options.registry.BackgroundImage = options.registry.BackgroundImage.extend({
snippetOptions.registry.BackgroundImage = snippetOptions.registry.BackgroundImage.extend({
start: function () {
this._super();
if (this.snippets && this.snippets.split('.')[0] === "mass_mailing") {
@@ -5,21 +5,28 @@ var config = require('web.config');
var core = require('web.core');
var FieldHtml = require('web_editor.field.html');
var fieldRegistry = require('web.field_registry');
var convertInline = require('web_editor.convertInline');
var QWeb = core.qweb;
var _t = core._t;
var MassMailingFieldHtml = FieldHtml.extend({
xmlDependencies: (FieldHtml.prototype.xmlDependencies || []).concat(["/mass_mailing/static/src/xml/mass_mailing.xml"]),
jsLibs: [
'/mass_mailing/static/src/js/mass_mailing_snippets.js',
],
assetLibs: ['web_editor.compiled_assets_wysiwyg'],
jsLibs: ['/mass_mailing/static/src/js/mass_mailing_snippets.js'],
custom_events: _.extend({}, FieldHtml.prototype.custom_events, {
snippets_loaded: '_onSnippetsLoaded',
}),
events: {
'click .o_we_show_themes_btn': '_onShowThemesClick',
'click .o_mail_theme_selector a': '_onChangeThemeClick',
'mouseenter .o_mail_theme_selector a': '_onChangeThemeMouseEnter',
'mouseleave .o_mail_theme_selector a': '_onChangeThemeMouseLeave',
'click .o_we_fullscreen_btn': '_onFullScreenClick',
},
/**
* @override
*/
@@ -41,103 +48,34 @@ var MassMailingFieldHtml = FieldHtml.extend({
*
* @override
*/
commitChanges: function () {
commitChanges: async function () {
var self = this;
if (config.isDebug() && this.mode === 'edit') {
var layoutInfo = $.summernote.core.dom.makeLayoutInfo(this.wysiwyg.$editor);
$.summernote.pluginEvents.codeview(undefined, undefined, layoutInfo, false);
}
if (this.mode === 'readonly' || !this.isRendered) {
return this._super();
}
var fieldName = this.nodeOptions['inline-field'];
if (this.$content.find('.o_basic_theme').length) {
this.$content.find('*').css('font-family', '');
await this._super();
if (this.mode === 'readonly' || !this.wysiwyg) {
return;
}
var $editable = this.wysiwyg.getEditable();
const isDirty = this._isDirty;
const changes = {};
changes[this.nodeOptions['inline-field']] = await this.wysiwyg.getValue('text/mail');
return this.wysiwyg.saveModifiedImages(this.$content).then(function () {
return self.wysiwyg.save().then(function (result) {
self._isDirty = result.isDirty;
convertInline.attachmentThumbnailToLinkImg($editable);
convertInline.fontToImg($editable);
convertInline.classToStyle($editable);
self.trigger_up('field_changed', {
dataPointID: self.dataPointID,
changes: _.object([fieldName], [self._unWrap($editable.html())])
});
if (self._isDirty && self.mode === 'edit') {
return self._doAction();
}
});
self.trigger_up('field_changed', {
dataPointID: self.dataPointID,
changes: changes,
});
},
/**
* The html_frame widget is opened in an iFrame that has its URL encoded
* with all the key/values returned by this method.
*
* Some fields can get very long values and we want to omit them for the URL building.
*
* @override
*/
getDatarecord: function () {
return _.omit(this._super(), [
'mailing_domain',
'contact_list_ids',
'body_html',
'attachment_ids'
]);
if (isDirty && self.mode === 'edit') {
return self._doAction();
}
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Returns true if must force the user to choose a theme.
*
* @private
* @returns {Boolean}
*/
_checkIfMustForceThemeChoice: function () {
var firstChoice = this._editableAreaIsEmpty();
this.$content.closest('body').toggleClass("o_force_mail_theme_choice", firstChoice);
return firstChoice;
},
/**
* Returns true if the editable area is empty.
*
* @private
* @param {JQuery} [$layout]
* @returns {Boolean}
*/
_editableAreaIsEmpty: function ($layout) {
$layout = $layout || this.$content.find(".o_layout");
var $mailWrapper = $layout.children(".o_mail_wrapper");
var $mailWrapperContent = $mailWrapper.find('.o_mail_wrapper_td');
if (!$mailWrapperContent.length) { // compatibility
$mailWrapperContent = $mailWrapper;
}
var value;
if ($mailWrapperContent.length > 0) {
value = $mailWrapperContent.html();
} else if ($layout.length) {
value = $layout.html();
} else {
value = this.wysiwyg.getValue();
}
var blankEditable = "<p><br></p>";
return value === "" || value === blankEditable;
},
/**
* @override
*/
_renderEdit: function () {
this._isFromInline = !!this.value;
if (!this.value) {
this.value = this.recordData[this.nodeOptions['inline-field']];
}
@@ -166,25 +104,6 @@ var MassMailingFieldHtml = FieldHtml.extend({
}
return $();
},
/**
* Returns the selected theme, if any.
*
* @private
* @param {Object} themesParams
* @returns {false|Object}
*/
_getSelectedTheme: function (themesParams) {
var $layout = this.$content.find(".o_layout");
var selectedTheme = false;
if ($layout.length !== 0) {
_.each(themesParams, function (themeParams) {
if ($layout.hasClass(themeParams.className)) {
selectedTheme = themeParams;
}
});
}
return selectedTheme;
},
/**
* Swap the previous theme's default images with the new ones.
* (Redefine the `src` attribute of all images in a $container, depending on the theme parameters.)
@@ -221,81 +140,187 @@ var MassMailingFieldHtml = FieldHtml.extend({
$img.attr("src", src);
});
},
_getContent: async function () {
return $(this.$('jw-shadow')[0].shadowRoot).find(':not(style,link)');
},
/**
* Switch themes or import first theme.
* Reload snippet when choose a template.
*
* @override
*/
_createWysiwygIntance: async function () {
await this._super();
const onCommitCheckSnippets = (params) => {
if (params.commandNames.includes('applyTemplate')) {
setTimeout(() => {
// use setTimeout to reload snippets after the redraw
this.wysiwyg.snippetsMenu.trigger('reload_snippet_dropzones');
});
}
};
this.wysiwyg.editor.dispatcher.registerCommandHook('@commit', onCommitCheckSnippets);
},
/**
* Add templates & themes.
*
* @override
*/
_getWysiwygOptions: async function () {
this.needShadow = true;
const options = await this._super();
const self = this;
// Get the snippets to have the templates and themes container.
const $snippets = $(await this._rpc({
model: 'ir.ui.view',
method: 'render_public_asset',
args: [options.snippets, {}],
kwargs: {
context: options.recordInfo.context,
},
}));
// Create templates and themes components.
const themes = [{
id: 'default',
label: _t('Default'),
render(editor) {
return editor.plugins.get(self.wysiwyg.JWEditorLib.Parser).parse('text/html',
'<div class="oe_structure"><t-placeholder/></div>');
},
}];
const components = [];
const templateConfigurations = {};
$snippets.find("#email_designer_themes").children().each(function () {
const $template = $(this);
const data = $template.data();
const templateId = 'template-' + data.name;
const themeId = 'theme-' + data.name;
const nowrap = !!$template.data('nowrap');
components.push({
id: templateId,
async render(editor) {
const valueAndTheme = self._getValueAndTheme($template.html());
const html = '<t-theme name="' + themeId + '">' + valueAndTheme.value + '</t-theme>';
return editor.plugins.get(self.wysiwyg.JWEditorLib.Parser).parse('text/html', html);
},
});
templateConfigurations[templateId] = {
componentId: templateId,
zoneId: 'editable',
label: data.name,
thumbnail: data.img + '_large.png',
thumbnailZoneId: 'container',
};
themes.push({
id: themeId,
data: data,
label: data.name,
render(editor) {
const parserPlugin = editor.plugins.get(self.wysiwyg.JWEditorLib.Parser);
if (nowrap) {
return parserPlugin.parse('text/html',
'<div class="o_layout oe_structure" contenteditable="true"><t-placeholder/></div>');
} else {
// This wrapper structure is the only way to have a responsive
// and centered fixed-width content column on all mail clients
return parserPlugin.parse('text/html',
'<div class="o_layout o_' + data.name + '_theme">' +
'<table class="o_mail_wrapper">' +
'<tr>' +
'<td class="o_mail_no_resize o_not_editable"></td>' +
'<td class="o_mail_no_options o_mail_wrapper_td oe_structure" contenteditable="true"><t-placeholder/></td>' +
'<td class="o_mail_no_resize o_not_editable"></td>' +
'</tr>' +
'</table>' +
'</div>');
}
},
});
});
// Add the templates and themes as options.
options.templates = {
components: components,
templateConfigurations: templateConfigurations,
};
options.themes = themes;
// Get the current theme.
const valueAndTheme = this._getValueAndTheme(options.value);
if (valueAndTheme.themeId) {
options.value = '<t-theme name="' + valueAndTheme.themeId + '">' + valueAndTheme.value + '</t-theme>';
} else if (options.value.length) {
options.value = '<t-theme>' + valueAndTheme.value + '</t-theme>';
}
return options;
},
/**
* Returns the selected theme, if any.
*
* @private
* @param {Boolean} firstChoice true if this is the first chosen theme (going from no theme to a theme)
* @param {Object} themeParams
* @param {string} value
* @returns {[string, string]} [value, themeId]
*/
_switchThemes: function (firstChoice, themeParams) {
if (!themeParams || this.switchThemeLast === themeParams) {
return;
}
this.switchThemeLast = themeParams;
this.$content.closest('body').removeClass(this._allClasses).addClass(themeParams.className);
var $old_layout = this.$content.find('.o_layout');
var $new_wrapper;
var $newWrapperContent;
if (themeParams.nowrap) {
$new_wrapper = $('<div/>', {
class: 'oe_structure'
});
$newWrapperContent = $new_wrapper;
} else {
// This wrapper structure is the only way to have a responsive
// and centered fixed-width content column on all mail clients
$new_wrapper = $('<table/>', {
class: 'o_mail_wrapper'
});
$newWrapperContent = $('<td/>', {
class: 'o_mail_no_options o_mail_wrapper_td oe_structure'
});
$new_wrapper.append($('<tr/>').append(
$('<td/>', {
class: 'o_mail_no_resize o_not_editable',
contenteditable: 'false'
}),
$newWrapperContent,
$('<td/>', {
class: 'o_mail_no_resize o_not_editable',
contenteditable: 'false'
})
));
}
var $newLayout = $('<div/>', {
class: 'o_layout ' + themeParams.className
}).append($new_wrapper);
var $contents;
if (firstChoice) {
$contents = themeParams.template;
} else if ($old_layout.length) {
$contents = ($old_layout.hasClass('oe_structure') ? $old_layout : $old_layout.find('.oe_structure').first()).contents();
} else {
$contents = this.$content.find('.o_editable').contents();
}
$newWrapperContent.append($contents);
this._switchImages(themeParams, $newWrapperContent);
this.$content.find('.o_editable').empty().append($newLayout);
$old_layout.remove();
if (firstChoice) {
$newWrapperContent.find('*').addBack()
.contents()
.filter(function () {
return this.nodeType === 3 && this.textContent.match(/\S/);
}).parent().addClass('o_default_snippet_text');
if (themeParams.name == 'basic') {
this.$content.focusIn();
_getValueAndTheme: function (value) {
const $value = $(value);
let $layout = $value.hasClass("o_layout") ? $value : $value.find(".o_layout");
let themeId;
if ($layout.length) {
let $contents = $layout.contents();
const classNameThemeId = [].find.call($layout[0].classList, className => className.includes('_theme'));
themeId = classNameThemeId && ('theme-' + classNameThemeId.slice(2, -6));
const $td = $contents.find('.o_mail_wrapper_td');
if ($td.length) {
$contents = $td.contents();
} else if ($layout.length) {
$contents = $layout.contents();
}
value = $contents.get().map(node => node.outerHTML || node.textContent).join('');
}
this.wysiwyg.trigger('reload_snippet_dropzones');
return {value: value, themeId: themeId};
},
/**
* @private
*/
_closeThemes: function () {
this.$el.find('#o_scroll, .o_snippet_search_filter').removeClass('d-none');
this.$el.find('.o_mail_theme_selector').addClass('d-none');
},
/**
* @private
*/
_previewUpdateTheme: function (themeId) {
const theme = this.wysiwyg.options.themes.find(theme => theme.id === themeId);
const $layout = $(this.el.querySelector('jw-shadow::shadow /deep/ .o_layout'));
$layout.attr('class', 'o_layout o_' + theme.data.name + '_theme');
const themeName = theme.data.name;
const imagesInfo = theme.data.imagesInfo;
$layout.find('img').each(function () {
const $img = $(this);
let src = $img.attr('src');
const isLogo = src.includes('logo.');
let moduleName = 'mass_mailing';
let format = 'jpg';
if (imagesInfo.logo && imagesInfo.logo.module && isLogo) {
moduleName = imagesInfo.logo.module;
} else if (imagesInfo.all && imagesInfo.all.module) {
moduleName = imagesInfo.all.module;
}
if (imagesInfo.logo && imagesInfo.logo.format && isLogo) {
format = imagesInfo.logo.format;
} else if (imagesInfo.all && imagesInfo.all.format) {
format = imagesInfo.all.format;
}
src = src.replace(
/^\/[^\/]+\/(.+)\/theme_[^/]+\/(.*)\.\w+$/,
'/' + moduleName + '/$1/theme_' + themeName + '/$2.' + format);
$img.attr('src', src);
});
},
//--------------------------------------------------------------------------
@@ -303,191 +328,101 @@ var MassMailingFieldHtml = FieldHtml.extend({
//--------------------------------------------------------------------------
/**
* @private
* @override
*/
_onLoadWysiwyg: function () {
if (this._isFromInline) {
this._fromInline();
}
if (this.snippetsLoaded) {
this._onSnippetsLoaded(this.snippetsLoaded);
}
this._super();
_onSnippetsLoaded: function () {
this.$el.find("#email_designer_themes").remove();
this.$el.find('#oe_snippets').append($(QWeb.render('mass_mailing.theme_selector', {themes: this.wysiwyg.options.themes})));
this.$el.find('#snippets_menu').append($('<button type="button" class="o_we_show_themes_btn"><span>' + _t('Select a theme') + '</span></button>'));
this.$el.find('#oe_snippets').before($('<button type="button" class="o_we_fullscreen_btn"><span class="fa fa-expand"></span></button>'));
},
/**
* @private
* @param {OdooEvent} ev
*/
_onSnippetsLoaded: function (ev) {
var self = this;
if (!this.$content) {
this.snippetsLoaded = ev;
return;
}
var $snippetsSideBar = ev.data;
var $themes = $snippetsSideBar.find("#email_designer_themes").children();
var $snippets = $snippetsSideBar.find(".oe_snippet");
var $snippets_menu = $snippetsSideBar.find("#snippets_menu");
_onChangeThemeClick: function (ev) {
ev.preventDefault();
const themeId = $(ev.target.closest('a')).data('id');
const theme = this.wysiwyg.options.themes.find(theme => theme.id === themeId);
const layoutPlugin = this.wysiwyg.editor.plugins.get(this.wysiwyg.JWEditorLib.Layout)
const domEngine = layoutPlugin.engines.dom;
const themeNode = domEngine.components.main[0].firstDescendant(node => node.themeName);
if (themeId !== themeNode.themeName) {
const changeTheme = () => {
themeNode.themeName = themeId;
if (config.device.isMobile) {
$snippetsSideBar.hide();
this.$content.attr('style', 'padding-left: 0px !important');
}
const themeName = theme.data.name;
const imagesInfo = theme.data.imagesInfo;
const imageNodes = themeNode.descendants(this.wysiwyg.JWEditorLib.ImageNode);
if ($themes.length === 0) {
return;
}
for (const imageNode of imageNodes) {
const attributes = imageNode.modifiers.get(this.wysiwyg.JWEditorLib.Attributes);
let src = attributes.get('src');
/**
* Initialize theme parameters.
*/
this._allClasses = "";
var themesParams = _.map($themes, function (theme) {
var $theme = $(theme);
var name = $theme.data("name");
var classname = "o_" + name + "_theme";
self._allClasses += " " + classname;
var imagesInfo = _.defaults($theme.data("imagesInfo") || {}, {
all: {}
});
_.each(imagesInfo, function (info) {
info = _.defaults(info, imagesInfo.all, {
module: "mass_mailing",
format: "jpg"
});
});
return {
name: name,
className: classname || "",
img: $theme.data("img") || "",
template: $theme.html().trim(),
nowrap: !!$theme.data('nowrap'),
get_image_info: function (filename) {
if (imagesInfo[filename]) {
return imagesInfo[filename];
const isLogo = src.includes('logo.');
let moduleName = 'mass_mailing';
let format = 'jpg';
if (imagesInfo.logo && imagesInfo.logo.module && isLogo) {
moduleName = imagesInfo.logo.module;
} else if (imagesInfo.all && imagesInfo.all.module) {
moduleName = imagesInfo.all.module;
}
return imagesInfo.all;
if (imagesInfo.logo && imagesInfo.logo.format && isLogo) {
format = imagesInfo.logo.format;
} else if (imagesInfo.all && imagesInfo.all.format) {
format = imagesInfo.all.format;
}
src = src.replace(
/^\/[^\/]+\/(.+)\/theme_[^/]+\/(.*)\.\w+$/,
'/' + moduleName + '/$1/theme_' + themeName + '/$2.' + format);
attributes.set('src', src);
}
};
});
$themes.parent().remove();
/**
* Create theme selection screen and check if it must be forced opened.
* Reforce it opened if the last snippet is removed.
*/
var $dropdown = $(core.qweb.render("mass_mailing.theme_selector", {
themes: themesParams
})).dropdown();
var firstChoice = this._checkIfMustForceThemeChoice();
/**
* Add proposition to install enterprise themes if not installed.
*/
var $mail_themes_upgrade = $dropdown.find(".o_mass_mailing_themes_upgrade");
$mail_themes_upgrade.on("click", function (e) {
e.stopImmediatePropagation();
e.preventDefault();
self.do_action("mass_mailing.action_mass_mailing_configuration");
});
/**
* Switch theme when a theme button is hovered. Confirm change if the theme button
* is pressed.
*/
var selectedTheme = false;
$dropdown.on("mouseenter", ".dropdown-item", function (e) {
if (firstChoice) {
return;
}
e.preventDefault();
var themeParams = themesParams[$(e.currentTarget).index()];
self._switchThemes(firstChoice, themeParams);
});
$dropdown.on("mouseleave", ".dropdown-item", function (e) {
self._switchThemes(false, selectedTheme);
});
$dropdown.on("click", '[data-toggle="dropdown"]', function (e) {
var $menu = $dropdown.find('.dropdown-menu');
var isVisible = $menu.hasClass('show');
if (isVisible) {
e.preventDefault();
e.stopImmediatePropagation();
$menu.removeClass('show');
}
});
$dropdown.on("click", ".dropdown-item", function (e) {
e.preventDefault();
e.stopImmediatePropagation();
var themeParams = themesParams[$(e.currentTarget).index()];
if (firstChoice) {
self._switchThemes(firstChoice, themeParams);
self.$content.closest('body').removeClass("o_force_mail_theme_choice");
firstChoice = false;
if ($mail_themes_upgrade.length) {
$dropdown.remove();
$snippets_menu.empty();
}
}
self._switchImages(themeParams, $snippets);
selectedTheme = themeParams;
// Notify form view
self.wysiwyg.getEditable().trigger('change');
$dropdown.find('.dropdown-menu').removeClass('show');
$dropdown.find('.dropdown-item.selected').removeClass('selected');
$dropdown.find('.dropdown-item:eq(' + themesParams.indexOf(selectedTheme) + ')').addClass('selected');
});
/**
* If the user opens the theme selection screen, indicates which one is active and
* saves the information...
* ... then when the user closes check if the user confirmed its choice and restore
* previous state if this is not the case.
*/
$dropdown.on("shown.bs.dropdown", function () {
selectedTheme = self._getSelectedTheme(themesParams);
$dropdown.find(".dropdown-item").removeClass("selected").filter(function () {
return ($(this).has(".o_thumb[style=\"" + "background-image: url(" + (selectedTheme && selectedTheme.img) + "_small.png)" + "\"]").length > 0);
}).addClass("selected");
});
$dropdown.on("hidden.bs.dropdown", function () {
self._switchThemes(firstChoice, selectedTheme);
});
/**
* On page load, check the selected theme and force switching to it (body needs the
* theme style for its edition toolbar).
*/
selectedTheme = this._getSelectedTheme(themesParams);
if (selectedTheme) {
this.$content.closest('body').addClass(selectedTheme.className);
$dropdown.find('.dropdown-item:eq(' + themesParams.indexOf(selectedTheme) + ')').addClass('selected');
this._switchImages(selectedTheme, $snippets);
} else if (this.$content.find('.o_layout').length) {
themesParams.push({
name: 'o_mass_mailing_no_theme',
className: 'o_mass_mailing_no_theme',
img: "",
template: this.$content.find('.o_layout').addClass('o_mass_mailing_no_theme').clone().find('oe_structure').empty().end().html().trim(),
nowrap: true,
get_image_info: function () {}
});
selectedTheme = this._getSelectedTheme(themesParams);
this.wysiwyg.editor.execCommand(changeTheme);
}
$dropdown.insertAfter($snippets_menu);
this._closeThemes();
},
/**
* @private
*/
_onChangeThemeMouseEnter: function (ev) {
const themeId = $(ev.target.closest('a')).data('id');
this._previewUpdateTheme(themeId);
},
/**
* @private
*/
_onChangeThemeMouseLeave: function (ev) {
const layoutPlugin = this.wysiwyg.editor.plugins.get(this.wysiwyg.JWEditorLib.Layout)
const domEngine = layoutPlugin.engines.dom;
const themeNode = domEngine.components.main[0].firstDescendant(node => node.themeName);
this._previewUpdateTheme(themeNode.themeName);
},
/**
* @private
*/
_onFullScreenClick: function (ev) {
if (this.el.classList.contains('jw-fullscreen')) {
document.body.classList.remove('jw-fullscreen');
this.el.classList.remove('jw-fullscreen');
} else {
document.body.classList.add('jw-fullscreen');
this.el.classList.add('jw-fullscreen');
}
},
/**
* @private
*/
_onShowThemesClick: function () {
this.$el.find('#o_scroll, .o_snippet_search_filter, .o_mail_theme_selector').toggleClass('d-none');
this.$el.one('click', '.o_we_add_snippet_btn, .o_we_customize_snippet_btn', () => this._closeThemes());
},
/**
* @override
* @param {MouseEvent} ev
*/
_onTranslate: function (ev) {
_onTranslate: function () {
this.trigger_up('translate', {
fieldName: this.nodeOptions['inline-field'],
id: this.dataPointID,
@@ -0,0 +1,50 @@
.o_field_widget.o_mail_body {
height: 55vh;
display: flex;
.o_mail_theme_selector {
overflow: auto;
> a {
.o_thumb {
display: none;
background-size: cover;
padding-top: 45%;
width: 90%;
margin: 10px auto;
&.logo {
display: block;
}
}
&:first-child {
display: none;
}
}
}
.o_we_fullscreen_btn {
position: absolute;
top: 0;
right: 12px;
}
}
.jw-fullscreen .jw-fullscreen {
z-index: 1;
}
.oe_structure {
width: 100%;
@media (max-width: map-get($grid-breakpoints, 'lg')) {
width: auto;
}
}
.o_basic_theme .o_main_sidebar {
display: none;
}
.oe_form_field_html .o_we_website_top_actions {
display: none;
}
@@ -1,210 +0,0 @@
.o_form_view .o_form_sheet .o_notebook .tab-content .tab-pane .o_mail_body {
// cancel padding of form_sheet
margin-top: -$o-sheet-cancel-tpadding;
margin-left: -$o-sheet-cancel-hpadding;
margin-right: -$o-sheet-cancel-hpadding;
margin-bottom: -$o-sheet-cancel-bpadding;
}
.o_mail_theme_selector {
> a {
@include o-position-absolute(0, 0, auto, 0);
height: $o-we-toolbar-height;
line-height: $o-we-toolbar-height;
border-radius: 0;
background-color: $o-we-sidebar-bg;
color: #212629;
display: flex;
justify-content: center;
align-items: center;
box-shadow: none !important;
&:hover, &:focus, &:active {
color: #4e525b;
}
i {
margin-right: 10px;
}
}
&.show > a {
color: white;
background-color: #212629;
}
.dropdown-menu {
position: absolute;
bottom: -2px;
top: -3px !important;
left: -5px !important;
right: -2px !important;
margin: 0;
border-radius: 0;
overflow: auto;
background-color: $o-we-sidebar-bg;
z-index: 1050;
&.show {
bottom: 31px !important;
top: -2px !important;
right: 3px !important;
}
.dropdown-item {
padding: 10px 10px;
&:first-child {
display: none;
}
.o_thumb {
display: none;
background-size: cover;
padding-top: 50%;
border: 1px solid $o-we-border-color;
&.logo {
display: block;
}
}
&:hover {
background-color: $o-we-sidebar-bg;
.o_thumb {
border: 1px solid black;
}
}
&.selected .o_thumb {
border: 2px solid $o-brand-odoo;
background-color: $o-we-sidebar-bg;
}
}
}
}
body.o_force_mail_theme_choice {
#oe_snippets {
width: 100%;
.o_mail_theme_selector {
.dropdown-toggle {
display: none;
}
.dropdown-menu {
display: block;
.dropdown-item {
margin: 0;
float: left;
clear: none;
width: 100%;
max-width: 25%;
transition: all 0.3s ease 0s;
&:first-child {
display: block;
}
.o_thumb {
display: none;
padding-top: 107%;
border: 1px solid #4e525b;
border-top: 1px solid $o-we-border-color;
box-shadow: 0 5px 10px rgba(black, 0.8);
will-change: transform;
backface-visibility: hidden;
transition: all 0.3s ease 0s;
&.small {
display: block;
}
@media screen and (min-width: 900px) {
&.small {
display: none;
}
&.large {
display: block;
}
}
}
&:hover {
background-color: #212629;
.o_thumb {
box-shadow: 0 5px 30px 1px rgba(black, 0.6);
}
}
&.o_mass_mailing_themes_upgrade .o_thumb {
position: relative;
display: block;
border: 1px dashed white;
opacity: 0.2;
> .fa {
@include o-position-absolute(0, 0, 0, 0);
text-align: center;
font-size: 50px;
color: white;
&::before {
vertical-align: middle;
}
&::after {
content: "";
display: inline-block;
height: 100%;
vertical-align: middle;
}
}
}
}
}
}
}
.note-editor {
display: none;
}
}
body.editor_enable.o_basic_theme.o_in_iframe {
padding-right: 0px !important;
#web_editor-top-edit .note-popover .popover {
right: 0 !important;
}
#oe_snippets {
display: none;
}
}
.note-editable .o_layout {
overflow: initial;
}
#web_editor-toolbars .popover .note-color {
.note-back-color-preview .dropdown-toggle,
.note-fore-color-preview .dropdown-toggle {
height: $o-we-toolbar-height - 1;
}
}
.oe_structure {
width: 100%;
@media (max-width: map-get($grid-breakpoints, 'lg')) {
width: auto;
margin-right: $o-we-sidebar-width;
}
}
:root {
font-size: 14px;
}
@@ -0,0 +1,13 @@
:root {
font-size: 14px;
}
.oe_structure {
width: 100%;
@media (max-width: map-get($grid-breakpoints, 'lg')) {
width: auto;
margin-left: $o-we-sidebar-width;
}
}
.note-editable {
min-height: 400px;
}
@@ -1,22 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<div t-name="mass_mailing.theme_selector" class="o_mail_theme_selector">
<a role="button" href="#" class="btn btn-sm dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-paint-brush"/> Change Style
</a>
<div class="dropdown-menu" role="menu">
<t t-foreach="themes" t-as="theme">
<a t-att-id="theme.name" role="menuitem" href="#" class="dropdown-item">
<div class="o_thumb small" t-attf-style="background-image: url(#{theme.img}_small.png)"/>
<div class="o_thumb large" t-attf-style="background-image: url(#{theme.img}_large.png)"/>
<div class="o_thumb logo" t-attf-style="background-image: url(#{theme.img}_logo.png)"/>
</a>
</t>
<t t-if="themes.length === 1">
<a role="menuitem" href="#" class="dropdown-item o_mass_mailing_themes_upgrade">
<div class="o_thumb"><i class="fa fa-plus" role="img" aria-label="Upgrade theme" title="Upgrade theme"/></div>
</a>
</t>
</div>
<div t-name="mass_mailing.theme_selector" class="o_mail_theme_selector d-none">
<t t-foreach="themes" t-as="theme" t-if="theme.data">
<a t-att-data-id="theme.id" t-attf-class="o_{{theme.data.name}}_theme" role="menuitem" href="#">
<div class="o_thumb small" t-attf-style="background-image: url(#{theme.data.img}_small.png)"/>
<div class="o_thumb large" t-attf-style="background-image: url(#{theme.data.img}_large.png)"/>
<div class="o_thumb logo" t-attf-style="background-image: url(#{theme.data.img}_logo.png)"/>
</a>
</t>
<t t-if="themes.length === 1">
<a role="menuitem" href="#" class="dropdown-item o_mass_mailing_themes_upgrade">
<div class="o_thumb"><i class="fa fa-plus" role="img" aria-label="Upgrade theme" title="Upgrade theme"/></div>
</a>
</t>
</div>
</templates>
@@ -3,11 +3,9 @@ odoo.define('mass_mailing.field_html_tests', function (require) {
var ajax = require('web.ajax');
var FormView = require('web.FormView');
var FieldHtml = require('web_editor.field.html');
var MassMailingFieldHtml = require('mass_mailing.FieldHtml');
var testUtils = require('web.test_utils');
var weTestUtils = require('web_editor.test_utils');
var Wysiwyg = require('web_editor.wysiwyg');
const loader = require('web_editor.loader');
QUnit.module('mass_mailing', {}, function () {
@@ -37,21 +35,19 @@ QUnit.module('field html', {
}],
},
});
testUtils.mock.patch(ajax, {
loadAsset: function (xmlId) {
if (xmlId === 'web_editor.compiled_assets_wysiwyg') {
return Promise.resolve({
jsLibs: [],
cssLibs: [],
cssContents: []
});
}
if (xmlId === 'template.assets') {
return Promise.resolve({
cssLibs: [],
cssContents: ['.field_body {background-color: red;}']
});
}
if (xmlId === 'template.assets_all_style') {
return Promise.resolve({
cssLibs: $('link[href]:not([type="image/x-icon"])').map(function () {
return $(this).attr('href');
}).get(),
cssContents: ['.field_body {background-color: red;}']
cssContents: ['body {background-color: red;}']
});
}
throw 'Wrong template';
@@ -89,7 +85,7 @@ QUnit.test('save arch and html', async function (assert) {
var $fieldReadonly = form.$('.oe_form_field[name="body_html"]');
var $fieldEdit = form.$('.oe_form_field[name="body_arch"]');
assert.strictEqual($fieldReadonly.css('display'), 'block', "should display the readonly mode");
assert.strictEqual($fieldReadonly.css('display'), 'flex', "should display the readonly mode");
assert.strictEqual($fieldEdit.css('display'), 'none', "should hide the edit mode");
await testUtils.form.clickEdit(form);
@@ -98,7 +94,7 @@ QUnit.test('save arch and html', async function (assert) {
$fieldEdit = form.$('.oe_form_field[name="body_arch"]');
assert.strictEqual($fieldReadonly.css('display'), 'none', "should hide the readonly mode");
assert.strictEqual($fieldEdit.css('display'), 'block', "should display the edit mode");
assert.strictEqual($fieldEdit.css('display'), 'flex', "should display the edit mode");
form.destroy();
});
@@ -2,9 +2,6 @@
<odoo>
<!-- Snippets & Themes Menu -->
<template id="email_designer_snippets" inherit_id="web_editor.snippets" primary="True" groups="base.group_user">
<xpath expr="//div[@id='snippets_menu']" position="inside">
<button type="button" disabled="disabled">Select a template</button>
</xpath>
<xpath expr="//t[@id='default_snippets']" position="replace">
<t id="default_snippets">
<t t-set="company_id" t-value="res_company"/>
@@ -1,470 +0,0 @@
odoo.define('web_editor.convertInline', function (require) {
'use strict';
var FieldHtml = require('web_editor.field.html');
/**
* Returns the css rules which applies on an element, tweaked so that they are
* browser/mail client ok.
*
* @param {DOMElement} a
* @returns {Object} css property name -> css property value
*/
function getMatchedCSSRules(a) {
var i, r, k;
var doc = a.ownerDocument;
var rulesCache = a.ownerDocument._rulesCache || (a.ownerDocument._rulesCache = []);
if (!rulesCache.length) {
var sheets = doc.styleSheets;
for (i = sheets.length-1 ; i >= 0 ; i--) {
var rules;
// try...catch because browser may not able to enumerate rules for cross-domain sheets
try {
rules = sheets[i].rules || sheets[i].cssRules;
} catch (e) {
console.warn("Can't read the css rules of: " + sheets[i].href, e);
continue;
}
if (rules) {
for (r = rules.length-1; r >= 0; r--) {
var selectorText = rules[r].selectorText;
if (selectorText &&
rules[r].cssText &&
selectorText !== '*' &&
selectorText.indexOf(':hover') === -1 &&
selectorText.indexOf(':before') === -1 &&
selectorText.indexOf(':after') === -1 &&
selectorText.indexOf(':active') === -1 &&
selectorText.indexOf(':link') === -1 &&
selectorText.indexOf('::') === -1 &&
selectorText.indexOf("'") === -1) {
var st = selectorText.split(/\s*,\s*/);
for (k = 0 ; k < st.length ; k++) {
rulesCache.push({ 'selector': st[k], 'style': rules[r].style });
}
}
}
}
}
rulesCache.reverse();
}
var css = [];
var style;
a.matches = a.matches || a.webkitMatchesSelector || a.mozMatchesSelector || a.msMatchesSelector || a.oMatchesSelector;
for (r = 0; r < rulesCache.length; r++) {
if (a.matches(rulesCache[r].selector)) {
style = rulesCache[r].style;
if (style.parentRule) {
var style_obj = {};
var len;
for (k = 0, len = style.length ; k < len ; k++) {
if (style[k].indexOf('animation') !== -1) {
continue;
}
style_obj[style[k]] = style[style[k].replace(/-(.)/g, function (a, b) { return b.toUpperCase(); })];
if (new RegExp(style[k] + '\s*:[^:;]+!important' ).test(style.cssText)) {
style_obj[style[k]] += ' !important';
}
}
rulesCache[r].style = style = style_obj;
}
css.push([rulesCache[r].selector, style]);
}
}
function specificity(selector) {
// http://www.w3.org/TR/css3-selectors/#specificity
var a = 0;
selector = selector.replace(/#[a-z0-9_-]+/gi, function () { a++; return ''; });
var b = 0;
selector = selector.replace(/(\.[a-z0-9_-]+)|(\[.*?\])/gi, function () { b++; return ''; });
var c = 0;
selector = selector.replace(/(^|\s+|:+)[a-z0-9_-]+/gi, function (a) { if (a.indexOf(':not(')===-1) c++; return ''; });
return a*100 + b*10 + c;
}
css.sort(function (a, b) { return specificity(a[0]) - specificity(b[0]); });
style = {};
_.each(css, function (v,k) {
_.each(v[1], function (v,k) {
if (v && _.isString(v) && k.indexOf('-webkit') === -1 && (!style[k] || style[k].indexOf('important') === -1 || v.indexOf('important') !== -1)) {
style[k] = v;
}
});
});
_.each(style, function (v,k) {
if (v.indexOf('important') !== -1) {
style[k] = v.slice(0, v.length-11);
}
});
if (style.display === 'block') {
delete style.display;
}
// The css generates all the attributes separately and not in simplified form.
// In order to have a better compatibility (outlook for example) we simplify the css tags.
// e.g. border-left-style: none; border-bottom-s .... will be simplified in border-style = none
_.each([
{property: 'margin'},
{property: 'padding'},
{property: 'border', propertyEnd: '-style', defaultValue: 'none'},
], function (propertyInfo) {
var p = propertyInfo.property;
var e = propertyInfo.propertyEnd || '';
var defVal = propertyInfo.defaultValue || 0;
if (style[p+'-top'+e] || style[p+'-right'+e] || style[p+'-bottom'+e] || style[p+'-left'+e]) {
if (style[p+'-top'+e] === style[p+'-right'+e] && style[p+'-top'+e] === style[p+'-bottom'+e] && style[p+'-top'+e] === style[p+'-left'+e]) {
// keep => property: [top/right/bottom/left value];
style[p+e] = style[p+'-top'+e];
}
else {
// keep => property: [top value] [right value] [bottom value] [left value];
style[p+e] = (style[p+'-top'+e] || defVal) + ' ' + (style[p+'-right'+e] || defVal) + ' ' + (style[p+'-bottom'+e] || defVal) + ' ' + (style[p+'-left'+e] || defVal);
if (style[p+e].indexOf('inherit') !== -1 || style[p+e].indexOf('initial') !== -1) {
// keep => property-top: [top value]; property-right: [right value]; property-bottom: [bottom value]; property-left: [left value];
delete style[p+e];
return;
}
}
delete style[p+'-top'+e];
delete style[p+'-right'+e];
delete style[p+'-bottom'+e];
delete style[p+'-left'+e];
}
});
if (style['border-bottom-left-radius']) {
style['border-radius'] = style['border-bottom-left-radius'];
delete style['border-bottom-left-radius'];
delete style['border-bottom-right-radius'];
delete style['border-top-left-radius'];
delete style['border-top-right-radius'];
}
// if the border styling is initial we remove it to simplify the css tags for compatibility.
// Also, since we do not send a css style tag, the initial value of the border is useless.
_.each(_.keys(style), function (k) {
if (k.indexOf('border') !== -1 && style[k] === 'initial') {
delete style[k];
}
});
// text-decoration rule is decomposed in -line, -color and -style. This is
// however not supported by many browser/mail clients and the editor does
// not allow to change -color and -style rule anyway
if (style['text-decoration-line']) {
style['text-decoration'] = style['text-decoration-line'];
delete style['text-decoration-line'];
delete style['text-decoration-color'];
delete style['text-decoration-style'];
}
// text-align inheritance does not seem to get past <td> elements on some
// mail clients
if (style['text-align'] === 'inherit') {
var $el = $(a).parent();
do {
var align = $el.css('text-align');
if (_.indexOf(['left', 'right', 'center', 'justify'], align) >= 0) {
style['text-align'] = align;
break;
}
$el = $el.parent();
} while ($el.length && !$el.is('html'));
}
return style;
}
/**
* Converts font icons to images.
*
* @param {jQuery} $editable - the element in which the font icons have to be
* converted to images
*/
function fontToImg($editable) {
var fonts = odoo.__DEBUG__.services["wysiwyg.fonts"];
$editable.find('.fa').each(function () {
var $font = $(this);
var icon, content;
_.find(fonts.fontIcons, function (font) {
return _.find(fonts.getCssSelectors(font.parser), function (data) {
if ($font.is(data.selector.replace(/::?before/g, ''))) {
icon = data.names[0].split('-').shift();
content = data.css.match(/content:\s*['"]?(.)['"]?/)[1];
return true;
}
});
});
if (content) {
var color = $font.css('color').replace(/\s/g, '');
$font.replaceWith($('<img/>', {
src: _.str.sprintf('/web_editor/font_to_img/%s/%s/%s', content.charCodeAt(0), window.encodeURI(color), Math.max(1, Math.round($font.height()))),
'data-class': $font.attr('class'),
'data-style': $font.attr('style'),
class: $font.attr('class').replace(new RegExp('(^|\\s+)' + icon + '(-[^\\s]+)?', 'gi'), ''), // remove inline font-awsome style
style: $font.attr('style'),
}).css({height: 'auto', width: 'auto'}));
} else {
$font.remove();
}
});
}
/**
* Converts images which were the result of a font icon convertion to a font
* icon again.
*
* @param {jQuery} $editable - the element in which the images will be converted
* back to font icons
*/
function imgToFont($editable) {
$editable.find('img[src*="/web_editor/font_to_img/"]').each(function () {
var $img = $(this);
$img.replaceWith($('<span/>', {
class: $img.data('class'),
style: $img.data('style')
}));
});
}
/*
* Utility function to apply function over descendants elements
*
* This is needed until the following issue of jQuery is solved:
* https://github.com./jquery/sizzle/issues/403
*
* @param {Element} node The root Element node
* @param {Function} func The function applied over descendants
*/
function applyOverDescendants(node, func) {
node = node.firstChild;
while (node) {
if (node.nodeType === 1) {
func(node);
applyOverDescendants(node, func);
}
var $node = $(node);
if (node.nodeName === 'A' && $node.hasClass('btn') && !$node.children().length && $(node).parents('.o_outlook_hack').length) {
node = $(node).parents('.o_outlook_hack')[0];
}
else if (node.nodeName === 'IMG' && $node.parent('p').hasClass('o_outlook_hack')) {
node = $node.parent()[0];
}
node = node.nextSibling;
}
}
/**
* Converts css style to inline style (leave the classes on elements but forces
* the style they give as inline style).
*
* @param {jQuery} $editable
*/
function classToStyle($editable) {
applyOverDescendants($editable[0], function (node) {
var $target = $(node);
var css = getMatchedCSSRules(node);
var style = $target.attr('style') || '';
_.each(css, function (v,k) {
if (!(new RegExp('(^|;)\s*' + k).test(style))) {
style = k+':'+v+';'+style;
}
});
if (_.isEmpty(style)) {
$target.removeAttr('style');
} else {
$target.attr('style', style);
}
// Apple Mail
if (node.nodeName === 'TD' && !node.childNodes.length) {
node.innerHTML = '&nbsp;';
}
// Outlook
if (node.nodeName === 'A' && $target.hasClass('btn') && !$target.hasClass('btn-link') && !$target.children().length) {
var $hack = $('<table class="o_outlook_hack" style="display: inline-table;vertical-align:middle"><tr><td></td></tr></table>');
$hack.find('td')
.attr('height', $target.outerHeight())
.css({
'text-align': $target.parent().css('text-align'),
'margin': $target.css('padding'),
'border-radius': $target.css('border-radius'),
'background-color': $target.css('background-color'),
});
$target.after($hack);
$target.appendTo($hack.find('td'));
// the space add a line when it's a table but it's invisible when it's a link
node = $hack[0].previousSibling;
if (node && node.nodeType === Node.TEXT_NODE && !node.textContent.match(/\S/)) {
$(node).remove();
}
node = $hack[0].nextSibling;
if (node && node.nodeType === Node.TEXT_NODE && !node.textContent.match(/\S/)) {
$(node).remove();
}
}
else if (node.nodeName === 'IMG' && $target.is('.mx-auto.d-block')) {
$target.wrap('<p class="o_outlook_hack" style="text-align:center;margin:0"/>');
}
});
}
/**
* Removes the inline style which is not necessary (because, for example, a
* class on an element will induce the same style).
*
* @param {jQuery} $editable
*/
function styleToClass($editable) {
// Outlook revert
$editable.find('.o_outlook_hack').each(function () {
$(this).after($('a,img', this));
}).remove();
var $c = $('<span/>').appendTo($editable[0].ownerDocument.body);
applyOverDescendants($editable[0], function (node) {
var $target = $(node);
var css = getMatchedCSSRules(node);
var style = '';
_.each(css, function (v,k) {
if (!(new RegExp('(^|;)\s*' + k).test(style))) {
style = k+':'+v+';'+style;
}
});
css = ($c.attr('style', style).attr('style') || '').split(/\s*;\s*/);
style = ($target.attr('style') || '').replace(/\s*:\s*/, ':').replace(/\s*;\s*/, ';');
_.each(css, function (v) {
style = style.replace(v, '');
});
style = style.replace(/;+(\s;)*/g, ';').replace(/^;/g, '');
if (style !== '') {
$target.attr('style', style);
} else {
$target.removeAttr('style');
}
});
$c.remove();
}
/**
* Converts css display for attachment link to real image.
* Without this post process, the display depends on the css and the picture
* does not appear when we use the html without css (to send by email for e.g.)
*
* @param {jQuery} $editable
*/
function attachmentThumbnailToLinkImg($editable) {
$editable.find('a[href*="/web/content/"][data-mimetype]').filter(':empty, :containsExact( )').each(function () {
var $link = $(this);
var $img = $('<img/>')
.attr('src', $link.css('background-image').replace(/(^url\(['"])|(['"]\)$)/g, ''))
.css('height', Math.max(1, $link.height()) + 'px')
.css('width', Math.max(1, $link.width()) + 'px');
$link.prepend($img);
});
}
/**
* Revert attachmentThumbnailToLinkImg changes
*
* @see attachmentThumbnailToLinkImg
* @param {jQuery} $editable
*/
function linkImgToAttachmentThumbnail($editable) {
$editable.find('a[href*="/web/content/"][data-mimetype] > img').remove();
}
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
FieldHtml.include({
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @override
*/
commitChanges: function () {
if (this.nodeOptions['style-inline'] && this.mode === "edit") {
this._toInline();
}
return this._super();
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Converts CSS dependencies to CSS-independent HTML.
* - CSS display for attachment link -> real image
* - Font icons -> images
* - CSS styles -> inline styles
*
* @private
*/
_toInline: function () {
var $editable = this.wysiwyg.getEditable();
var html = this.wysiwyg.getValue();
$editable.html(html);
attachmentThumbnailToLinkImg($editable);
fontToImg($editable);
classToStyle($editable);
this.wysiwyg.setValue($editable.html(), {
notifyChange: false,
});
},
/**
* Revert _toInline changes.
*
* @private
*/
_fromInline: function () {
var $editable = this.wysiwyg.getEditable();
var html = this.wysiwyg.getValue();
$editable.html(html);
styleToClass($editable);
imgToFont($editable);
linkImgToAttachmentThumbnail($editable);
this.wysiwyg.setValue($editable.html(), {
notifyChange: false,
});
},
//--------------------------------------------------------------------------
// Handler
//--------------------------------------------------------------------------
/**
* @override
*/
_onLoadWysiwyg: function () {
if (this.nodeOptions['style-inline'] && this.mode === "edit") {
this._fromInline();
}
this._super();
},
});
return {
fontToImg: fontToImg,
imgToFont: imgToFont,
classToStyle: classToStyle,
styleToClass: styleToClass,
attachmentThumbnailToLinkImg: attachmentThumbnailToLinkImg,
linkImgToAttachmentThumbnail: linkImgToAttachmentThumbnail,
};
});