From b68e06e20a7e67668c37ab9473c2ab25428b00fc Mon Sep 17 00:00:00 2001 From: Antoine Guenet Date: Mon, 10 Jul 2023 12:40:06 +0000 Subject: [PATCH] [FIX] web_editor: properly convert rgba colors to hexadecimal We currently convert rgb colors to hexadecimal, but not rgba colors. Unfortunately since many Bootstrap border colors are defined as rgba, this meant that these were not properly converted for Outlook (which doesn't support rgba). To convert rgba while losing the transparency information we need to "flatten" it by taking in consideration the background color. closes odoo/odoo#129153 X-original-commit: dc56506cffa5a13508ca24018d4f0a25c5353cca Signed-off-by: David Monjoie (dmo) Signed-off-by: Antoine Guenet (age) --- .../static/src/js/backend/convert_inline.js | 6 +-- .../js/editor/odoo-editor/src/utils/utils.js | 42 ++++++++++++++- .../odoo-editor/test/spec/color.test.js | 51 +++++++++++++++++++ 3 files changed, 95 insertions(+), 4 deletions(-) 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 f501e7fc0c7..1fe6797afc1 100644 --- a/addons/web_editor/static/src/js/backend/convert_inline.js +++ b/addons/web_editor/static/src/js/backend/convert_inline.js @@ -1136,9 +1136,9 @@ function listGroupToTable(editable) { function normalizeColors($editable) { const editable = $editable.get(0); for (const node of editable.querySelectorAll('[style*="rgb"]')) { - const rgbMatch = node.getAttribute('style').match(/rgb?\(([\d\.]*,?\s?){3,4}\)/g); - for (const rgb of rgbMatch || []) { - node.setAttribute('style', node.getAttribute('style').replace(rgb, rgbToHex(rgb))); + const rgbaMatch = node.getAttribute('style').match(/rgba?\(([\d\.]+\s*,?\s*){3,4}\)/g); + for (const rgb of rgbaMatch || []) { + node.setAttribute('style', node.getAttribute('style').replace(rgb, rgbToHex(rgb, node))); } } } diff --git a/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/utils.js b/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/utils.js index 8f95ae89971..032a88b761a 100644 --- a/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/utils.js +++ b/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/utils.js @@ -2456,9 +2456,49 @@ export function enforceWhitespace(el, offset, direction, rule) { } } -export function rgbToHex(rgb = '') { +/** + * Takes a color (rgb, rgba or hex) and returns its hex representation. If the + * color is given in rgba, the background color of the node whose color we're + * converting is used in conjunction with the alpha to compute the resulting + * color (using the formula: `alpha*color + (1 - alpha)*background` for each + * channel). + * + * @param {string} rgb + * @param {HTMLElement} [node] + * @returns {string} hexadecimal color (#RRGGBB) + */ +export function rgbToHex(rgb = '', node = null) { if (rgb.startsWith('#')) { return rgb; + } else if (rgb.startsWith('rgba')) { + const values = rgb.match(/[\d\.]{1,5}/g) || []; + const alpha = parseFloat(values.pop()); + // Retrieve the background color. + let bgRgbValues = []; + if (node) { + let bgColor = getComputedStyle(node).backgroundColor; + if (bgColor.startsWith('rgba')) { + // The background color is itself rgba so we need to compute + // the resulting color using the background color of its + // parent. + bgColor = rgbToHex(bgColor, node.parentElement); + } + if (bgColor && bgColor.startsWith('#')) { + bgRgbValues = (bgColor.match(/[\da-f]{2}/gi) || []).map(val => parseInt(val, 16)); + } else if (bgColor && bgColor.startsWith('rgb')) { + bgRgbValues = (bgColor.match(/[\d\.]{1,5}/g) || []).map(val => parseInt(val)); + } + } + bgRgbValues = bgRgbValues.length ? bgRgbValues : [255, 255, 255]; // Default to white. + + return ( + '#' + + values.map((value, index) => { + const converted = Math.floor(alpha * parseInt(value) + (1 - alpha) * bgRgbValues[index]); + const hex = parseInt(converted).toString(16); + return hex.length === 1 ? '0' + hex : hex; + }).join('') + ); } else { return ( '#' + diff --git a/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/color.test.js b/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/color.test.js index f9967cdf21c..c31cd4c4d9e 100644 --- a/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/color.test.js +++ b/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/color.test.js @@ -1,6 +1,7 @@ /** @odoo-module */ import { BasicEditor, testEditor, unformat } from '../utils.js'; +import { rgbToHex } from '../../src/utils/utils.js'; const setColor = (color, mode) => { return async editor => { @@ -106,3 +107,53 @@ describe('applyColor', () => { }); }); }); +describe('rgbToHex', () => { + it('should convert an rgb color to hexadecimal', async () => { + window.chai.expect(rgbToHex('rgb(0, 0, 255)')).to.be.equal('#0000ff'); + window.chai.expect(rgbToHex('rgb(0,0,255)')).to.be.equal('#0000ff'); + }); + it('should convert an rgba color to hexadecimal (background is hexadecimal)', async () => { + const parent = document.createElement('div'); + const node = document.createElement('div'); + parent.style.backgroundColor = '#ff0000'; // red, should be irrelevant + node.style.backgroundColor = '#0000ff'; // blue + parent.append(node); + document.body.append(parent); + // white with 50% opacity over blue = light blue + window.chai.expect(rgbToHex('rgba(255, 255, 255, 0.5)', node)).to.be.equal('#7f7fff'); + parent.remove(); + }); + it('should convert an rgba color to hexadecimal (background is color name)', async () => { + const parent = document.createElement('div'); + const node = document.createElement('div'); + parent.style.backgroundColor = '#ff0000'; // red, should be irrelevant + node.style.backgroundColor = 'blue'; // blue + parent.append(node); + document.body.append(parent); + // white with 50% opacity over blue = light blue + window.chai.expect(rgbToHex('rgba(255, 255, 255, 0.5)', node)).to.be.equal('#7f7fff'); + parent.remove(); + }); + it('should convert an rgba color to hexadecimal (background is rgb)', async () => { + const parent = document.createElement('div'); + const node = document.createElement('div'); + parent.style.backgroundColor = '#ff0000'; // red, should be irrelevant + node.style.backgroundColor = 'rgb(0, 0, 255)'; // blue + parent.append(node); + document.body.append(parent); + // white with 50% opacity over blue = light blue + window.chai.expect(rgbToHex('rgba(255, 255, 255, 0.5)', node)).to.be.equal('#7f7fff'); + parent.remove(); + }); + it('should convert an rgba color to hexadecimal (background is rgba)', async () => { + const parent = document.createElement('div'); + const node = document.createElement('div'); + parent.style.backgroundColor = 'rgb(255, 0, 0)'; // red + node.style.backgroundColor = 'rgba(0, 0, 255, 0.5)'; // blue + parent.append(node); + document.body.append(parent); + // white with 50% opacity over blue with 50% opacity over red = light purple + window.chai.expect(rgbToHex('rgba(255, 255, 255, 0.5)', node)).to.be.equal('#bf7fbf'); + parent.remove(); + }); +});