[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) <dmo@odoo.com>
Signed-off-by: Antoine Guenet (age) <age@odoo.com>
This commit is contained in:
Antoine Guenet
2023-07-24 11:28:51 +02:00
parent 5ea4baa612
commit b68e06e20a
3 changed files with 95 additions and 4 deletions
@@ -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)));
}
}
}
@@ -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 (
'#' +
@@ -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();
});
});