diff --git a/addons/mass_mailing/tests/test_mailing_ui.py b/addons/mass_mailing/tests/test_mailing_ui.py
index 22b6ab5e224..c1c240e4aac 100644
--- a/addons/mass_mailing/tests/test_mailing_ui.py
+++ b/addons/mass_mailing/tests/test_mailing_ui.py
@@ -20,4 +20,4 @@ class TestUi(HttpCaseWithUserDemo):
# for email client compatibility should be saved in body_html. This
# ensures both fields have different values.
self.assertEqual(mail.body_arch, '
')
- self.assertEqual(mail.body_html, '
')
+ self.assertEqual(mail.body_html, '
')
diff --git a/addons/web_editor/static/src/js/backend/convert_inline.js b/addons/web_editor/static/src/js/backend/convert_inline.js
index a6adb8823fa..f2bb38b01c0 100644
--- a/addons/web_editor/static/src/js/backend/convert_inline.js
+++ b/addons/web_editor/static/src/js/backend/convert_inline.js
@@ -9,6 +9,7 @@ import { isBlock, rgbToHex } from '../../../lib/odoo-editor/src/utils/utils';
//--------------------------------------------------------------------------
const RE_COL_MATCH = /(^| )col(-[\w\d]+)*( |$)/;
+const RE_COMMAS_OUTSIDE_PARENTHESES = /,(?![^(]*?\))/g;
const RE_OFFSET_MATCH = /(^| )offset(-[\w\d]+)*( |$)/;
const RE_PADDING = /([\d.]+)/;
const RE_WHITESPACE = /[\s\u200b]*/;
@@ -290,9 +291,31 @@ function cardToTable($editable) {
*/
function classToStyle($editable, cssRules) {
const writes = [];
+ const nodeToRules = new Map();
+ const rulesToProcess = [];
+ for (const rule of cssRules) {
+ const nodes = $editable[0].querySelectorAll(rule.selector);
+ if (nodes.length) {
+ rulesToProcess.push(rule);
+ }
+ for (const node of nodes) {
+ const nodeRules = nodeToRules.get(node);
+ if (!nodeRules) {
+ nodeToRules.set(node, [rule]);
+ } else {
+ nodeRules.push(rule);
+ }
+ }
+ }
+ _computeStyleAndSpecificityOnRules(rulesToProcess);
+ for (const rules of nodeToRules.values()) {
+ rules.sort((a, b) => a.specificity - b.specificity);
+ }
+
_applyOverDescendants($editable[0], function (node) {
const $target = $(node);
- const css = _getMatchedCSSRules(node, cssRules);
+ const nodeRules = nodeToRules.get(node);
+ const css = nodeRules ? _getMatchedCSSRules(node, nodeRules) : {};
// Flexbox
for (const styleName of node.style) {
if (styleName.includes('flex') || `${node.style[styleName]}`.includes('flex')) {
@@ -618,17 +641,15 @@ function getCSSRules(doc) {
}
for (const subRule of subRules) {
const selectorText = subRule.selectorText || '';
- for (const selector of selectorText.split(',')) {
+ // Split selectors, making sure not to split at commas in parentheses.
+ for (const selector of selectorText.split(RE_COMMAS_OUTSIDE_PARENTHESES)) {
if (selector && !SELECTORS_IGNORE.test(selector)) {
- const style = _normalizeStyle(subRule.style);
- if (Object.keys(style).length) {
- cssRules.push({ selector, style, specificity: _computeSpecificity(selector) });
- if (selector === 'body') {
- // The top element of a mailing has the class
- // 'o_layout'. Give it the body's styles so they can
- // trickle down.
- cssRules.push({ selector: '.o_layout', style, specificity: 1 });
- }
+ cssRules.push({ selector: selector.trim(), rawRule: subRule });
+ if (selector === 'body') {
+ // The top element of a mailing has the class
+ // 'o_layout'. Give it the body's styles so they can
+ // trickle down.
+ cssRules.push({ selector: '.o_layout', rawRule: subRule, specificity: 1 });
}
}
}
@@ -789,6 +810,24 @@ function _computeSpecificity(selector) {
selector = selector.replace(/(^|\s+|:+)[a-z0-9_-]+/gi, a => { if (!a.includes(':not(')) c++; return ''; });
return (a * 100) + (b * 10) + c;
}
+/**
+ * Take all the rules and modify them to contain information on their
+ * specificity and to have normalized style.
+ *
+ * @see _computeSpecificity
+ * @see _normalizeStyle
+ * @param {Object} cssRules
+ */
+function _computeStyleAndSpecificityOnRules(cssRules) {
+ for (const cssRule of cssRules) {
+ if (!cssRule.style && cssRule.rawRule.style) {
+ const style = _normalizeStyle(cssRule.rawRule.style);
+ if (Object.keys(style).length) {
+ Object.assign(cssRule, { style, specificity: _computeSpecificity(cssRule.selector) });
+ }
+ }
+ }
+}
/**
* Return an array of twelve table cells as JQuery elements.
*
@@ -861,13 +900,7 @@ function _getColumnSize(column) {
*/
function _getMatchedCSSRules(node, cssRules) {
node.matches = node.matches || node.webkitMatchesSelector || node.mozMatchesSelector || node.msMatchesSelector || node.oMatchesSelector;
- const css = [];
- cssRules.sort((a, b) => a.specificity - b.specificity);
- for (const rule of cssRules) {
- if (node.matches(rule.selector)) {
- css.push([rule.selector, rule.style, rule.specificity]);
- }
- }
+ const styles = cssRules.map((rule) => rule.style).filter(Boolean);
// Add inline styles at the highest specificity.
if (node.style.length) {
@@ -875,29 +908,29 @@ function _getMatchedCSSRules(node, cssRules) {
for (const styleName of node.style) {
inlineStyles[styleName] = node.style[styleName];
}
- css.push([node, inlineStyles]);
+ styles.push(inlineStyles);
}
- const style = {};
- for (const cssValue of css) {
- for (const [key, value] of Object.entries(cssValue[1])) {
- if (!style[key] || !style[key].includes('important') || value.includes('important')) {
- style[key] = value;
+ const processedStyle = {};
+ for (const style of styles) {
+ for (const [key, value] of Object.entries(style)) {
+ if (!processedStyle[key] || !processedStyle[key].includes('important') || value.includes('important')) {
+ processedStyle[key] = value;
}
- };
- };
+ }
+ }
- for (const [key, value] of Object.entries(style)) {
+ for (const [key, value] of Object.entries(processedStyle)) {
if (value.endsWith('important')) {
- style[key] = value.replace(/\s*!important\s*$/, '');
+ processedStyle[key] = value.replace(/\s*!important\s*$/, '');
}
};
- if (style.display === 'block' && !(node.classList && node.classList.contains('btn-block'))) {
- delete style.display;
+ if (processedStyle.display === 'block' && !(node.classList && node.classList.contains('btn-block'))) {
+ delete processedStyle.display;
}
- if (!style['box-sizing']) {
- style['box-sizing'] = 'border-box'; // This is by default with Bootstrap.
+ if (!processedStyle['box-sizing']) {
+ processedStyle['box-sizing'] = 'border-box'; // This is by default with Bootstrap.
}
// The css generates all the attributes separately and not in simplified
@@ -911,55 +944,55 @@ function _getMatchedCSSRules(node, cssRules) {
]) {
const positions = ['top', 'right', 'bottom', 'left'];
const positionalKeys = positions.map(position => `${info.name}-${position}${info.suffix || ''}`);
- const hasStyles = positionalKeys.some(key => style[key]);
- const inherits = positionalKeys.some(key => ['inherit', 'initial'].includes((style[key] || '').trim()));
+ const hasStyles = positionalKeys.some(key => processedStyle[key]);
+ const inherits = positionalKeys.some(key => ['inherit', 'initial'].includes((processedStyle[key] || '').trim()));
if (hasStyles && !inherits) {
const propertyName = `${info.name}${info.suffix || ''}`;
- style[propertyName] = positionalKeys.every(key => style[positionalKeys[0]] === style[key])
- ? style[propertyName] = style[positionalKeys[0]] // top = right = bottom = left => property: [top];
- : positionalKeys.map(key => style[key] || (info.defaultValue || 0)).join(' '); // property: [top] [right] [bottom] [left];
+ processedStyle[propertyName] = positionalKeys.every(key => processedStyle[positionalKeys[0]] === processedStyle[key])
+ ? processedStyle[propertyName] = processedStyle[positionalKeys[0]] // top = right = bottom = left => property: [top];
+ : positionalKeys.map(key => processedStyle[key] || (info.defaultValue || 0)).join(' '); // property: [top] [right] [bottom] [left];
for (const prop of positionalKeys) {
- delete style[prop];
+ delete processedStyle[prop];
}
}
};
- 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 (processedStyle['border-bottom-left-radius']) {
+ processedStyle['border-radius'] = processedStyle['border-bottom-left-radius'];
+ delete processedStyle['border-bottom-left-radius'];
+ delete processedStyle['border-bottom-right-radius'];
+ delete processedStyle['border-top-left-radius'];
+ delete processedStyle['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.
- for (const styleName in style) {
- if (styleName.includes('border') && style[styleName] === 'initial') {
- delete style[styleName];
+ for (const styleName in processedStyle) {
+ if (styleName.includes('border') && processedStyle[styleName] === 'initial') {
+ delete processedStyle[styleName];
}
};
// 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'];
- delete style['text-decoration-thickness'];
+ if (processedStyle['text-decoration-line']) {
+ processedStyle['text-decoration'] = processedStyle['text-decoration-line'];
+ delete processedStyle['text-decoration-line'];
+ delete processedStyle['text-decoration-color'];
+ delete processedStyle['text-decoration-style'];
+ delete processedStyle['text-decoration-thickness'];
}
// flexboxes are not supported in Windows Outlook
- for (const styleName in style) {
- if (styleName.includes('flex') || `${style[styleName]}`.includes('flex')) {
- delete style[styleName];
+ for (const styleName in processedStyle) {
+ if (styleName.includes('flex') || `${processedStyle[styleName]}`.includes('flex')) {
+ delete processedStyle[styleName];
}
}
- return style;
+ return processedStyle;
}
/**
* Take a css style declaration return a "normalized" version of it (as a