',
], type='http', auth="none")
- def export_icon_to_png(self, icon, color='#000', size=100, alpha=255, font='/web/static/lib/fontawesome/fonts/fontawesome-webfont.ttf'):
+ def export_icon_to_png(self, icon, color='#000', bg=None, size=100, alpha=255, font='/web/static/lib/fontawesome/fonts/fontawesome-webfont.ttf'):
""" This method converts an unicode character to an image (using Font
Awesome font by default) and is used only for mass mailing because
custom fonts are not supported in mail.
:param icon : decimal encoding of unicode character
:param color : RGB code of the color
+ :param bg : RGB code of the background color
:param size : Pixels in integer
:param alpha : transparency of the image from 0 to 255
:param font : font path
@@ -57,6 +61,11 @@ class Web_Editor(http.Controller):
# if received character is not a number, keep old behaviour (icon is character)
icon = chr(int(icon)) if icon.isdigit() else icon
+ # Background standardization
+ if bg is not None and bg.startswith('rgba'):
+ bg = bg.replace('rgba', 'rgb')
+ bg = ','.join(bg.split(',')[:-1])+')'
+
# Determine the dimensions of the icon
image = Image.new("RGBA", (size, size), color=(0, 0, 0, 0))
draw = ImageDraw.Draw(image)
@@ -78,8 +87,8 @@ class Web_Editor(http.Controller):
iconimage.putalpha(imagemask)
# Create output image
- outimage = Image.new("RGBA", (boxw, size), (0, 0, 0, 0))
- outimage.paste(iconimage, (left, top))
+ outimage = Image.new("RGBA", (boxw, size), bg or (0, 0, 0, 0))
+ outimage.paste(iconimage, (left, top), iconimage)
# output image
output = io.BytesIO()
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 ff285255286..7ad783af1b5 100644
--- a/addons/web_editor/static/src/js/backend/convert_inline.js
+++ b/addons/web_editor/static/src/js/backend/convert_inline.js
@@ -4,6 +4,21 @@ odoo.define('web_editor.convertInline', function (require) {
var FieldHtml = require('web_editor.field.html');
const SELECTORS_IGNORE = /(^\*$|:hover|:before|:after|:active|:link|::|'|\([^(),]+[,(])/;
+
+// Note: duplicated from odoo-editor utils.
+// DOES NOT SUPPORT RGBA (Outlook doesn't support transparency)
+function rgbToHex(rgb = '') {
+ return (
+ '#' +
+ (rgb.match(/\d{1,3}/g) || [])
+ .map(x => {
+ x = parseInt(x).toString(16);
+ return x.length === 1 ? '0' + x : x;
+ })
+ .join('')
+ );
+}
+
/**
* Returns the css rules which applies on an element, tweaked so that they are
* browser/mail client ok.
@@ -12,7 +27,7 @@ const SELECTORS_IGNORE = /(^\*$|:hover|:before|:after|:active|:link|::|'|\([^(),
* @returns {Object} css property name -> css property value
*/
function getMatchedCSSRules(a) {
- var i, r, k;
+ var i, r, k, l;
var doc = a.ownerDocument;
var rulesCache = a.ownerDocument._rulesCache || (a.ownerDocument._rulesCache = []);
@@ -29,11 +44,37 @@ function getMatchedCSSRules(a) {
}
if (rules) {
for (r = rules.length-1; r >= 0; r--) {
+ const conditionText = rules[r].conditionText;
+ const minWidthMatch = conditionText && conditionText.match(/\(min-width *: *(\d+)/);
+ const minWidth = minWidthMatch && +(minWidthMatch[1] || '0');
+ if (minWidth && minWidth >= 1200) {
+ // Large min-width media queries should be included.
+ // eg., .container has a default max-width for all
+ // screens.
+ let mediaRules;
+ try {
+ mediaRules = rules[r].rules || rules[r].cssRules;
+ } catch (e) {
+ console.log(`Can't read the css rules of: ${sheets[i].href} (${conditionText})`, e);
+ continue;
+ }
+ if (mediaRules) {
+ for (k = mediaRules.length-1; k >= 0; k--) {
+ var selectorText = mediaRules[k].selectorText;
+ if (selectorText && !SELECTORS_IGNORE.test(selectorText)) {
+ var st = selectorText.split(/\s*,\s*/);
+ for (l = 0 ; l < st.length ; l++) {
+ rulesCache.push({ 'selector': st[l], 'style': mediaRules[k].style });
+ }
+ }
+ }
+ }
+ }
var selectorText = rules[r].selectorText;
if (selectorText && !SELECTORS_IGNORE.test(selectorText)) {
var st = selectorText.split(/\s*,\s*/);
- for (k = 0 ; k < st.length ; k++) {
- rulesCache.push({ 'selector': st[k], 'style': rules[r].style });
+ for (l = 0 ; l < st.length ; l++) {
+ rulesCache.push({ 'selector': st[l], 'style': rules[r].style });
}
}
}
@@ -93,9 +134,12 @@ function getMatchedCSSRules(a) {
}
});
- if (style.display === 'block') {
+ if (style.display === 'block' && !(a.classList && a.classList.contains('btn-block'))) {
delete style.display;
}
+ if (!style['box-sizing']) {
+ style['box-sizing'] = 'border-box'; // This is by default with Bootstrap.
+ }
// 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.
@@ -157,18 +201,34 @@ function getMatchedCSSRules(a) {
delete style['text-decoration-thickness'];
}
- // text-align inheritance does not seem to get past | 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;
+ // color and text-align inheritance do not seem to get past | elements on
+ // some mail clients. TODO: This is hacky as it applies a color/text-align
+ // style to all descendants of nodes with a color style. We can probably do
+ // this more elegantly.
+ if (style.color || style['text-align']) {
+ function _styleDescendants(node, styleName) {
+ const camelCased = styleName.replace(/-(\w)/g, match => match[1].toUpperCase());
+ node.style[camelCased] = style[styleName];
+ for (const child of $(node).children()) {
+ const childStyle = $(child).css(styleName);
+ if (childStyle && childStyle !== 'inherit') {
+ _styleDescendants(child, styleName);
+ }
}
- $el = $el.parent();
- } while ($el.length && !$el.is('html'));
+ }
+ if (style.color) {
+ _styleDescendants(a, 'color');
+ }
+ if (style['text-align']) {
+ _styleDescendants(a, 'text-align');
+ }
+ }
+
+ // flexboxes are not supported in Windows Outlook
+ for (const styleName in style) {
+ if (styleName.includes('flex') || `${style[styleName]}`.includes('flex')) {
+ delete style[styleName];
+ }
}
return style;
@@ -197,13 +257,45 @@ function fontToImg($editable) {
});
if (content) {
var color = $font.css('color').replace(/\s/g, '');
- $font.replaceWith($(' ', {
- src: _.str.sprintf('/web_editor/font_to_img/%s/%s/%s', content.charCodeAt(0), window.encodeURI(color), Math.max(1, Math.round($font.height()))),
+ let $backgroundColoredElement = $font;
+ let bg, isTransparent;
+ do {
+ bg = $backgroundColoredElement.css('background-color').replace(/\s/g, '');
+ isTransparent = bg === 'transparent' || bg === 'rgba(0,0,0,0)';
+ $backgroundColoredElement = $backgroundColoredElement.parent();
+ } while (isTransparent && $backgroundColoredElement[0]);
+ if (bg === 'rgba(0,0,0,0)' && isTransparent) {
+ // default on white rather than black background since opacity
+ // is not supported.
+ bg = 'rgb(255,255,255)';
+ }
+ const style = $font.attr('style');
+ const width = $font.width();
+ const height = $font.height();
+ const lineHeight = $font.css('line-height');
+ // Compute the padding.
+ // First get the dimensions of the icon itself (::before)
+ $font.css({height: 'fit-content', width: 'fit-content', 'line-height': 'normal'});
+ const hPadding = width && (width - $font.width()) / 2;
+ const vPadding = height && (height - $font.height()) / 2;
+ let padding = '';
+ if (hPadding || vPadding) {
+ padding = vPadding ? vPadding + 'px ' : '0 ';
+ padding += hPadding ? hPadding + 'px' : '0';
+ }
+ const $img = $(' ').attr({
+ width, height,
+ src: `/web_editor/font_to_img/${content.charCodeAt(0)}/${window.encodeURI(color)}/${window.encodeURI(bg)}/${Math.max(1, $font.height())}`,
'data-class': $font.attr('class'),
- 'data-style': $font.attr('style'),
+ 'data-style': 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'}));
+ style,
+ }).css({
+ 'box-sizing': 'border-box', // keep the fontawesome's dimensions
+ 'line-height': lineHeight,
+ padding, width: width + 'px', height: height + 'px',
+ });
+ $font.replaceWith($img);
} else {
$font.remove();
}
@@ -237,6 +329,312 @@ function applyOverDescendants(node, func) {
}
}
+const reColMatch = /(^| )col(-[\w\d]+)*( |$)/;
+const reOffsetMatch = /(^| )offset(-[\w\d]+)*( |$)/;
+function _getColumnSize(column) {
+ const colMatch = column.className.match(reColMatch);
+ const colOptions = colMatch[2] && colMatch[2].substr(1).split('-');
+ const colSize = colOptions && (colOptions.length === 2 ? +colOptions[1] : +colOptions[0]) || 0;
+ const offsetMatch = column.className.match(reOffsetMatch);
+ const offsetOptions = offsetMatch && offsetMatch[2] && offsetMatch[2].substr(1).split('-');
+ const offsetSize = offsetOptions && (offsetOptions.length === 2 ? +offsetOptions[1] : +offsetOptions[0]) || 0;
+ return colSize + offsetSize;
+}
+
+
+// Attributes all tables should have in a mailing.
+const tableAttributes = {
+ cellspacing: 0,
+ cellpadding: 0,
+ border: 0,
+ width: '100%',
+ align: 'center',
+ role: 'presentation',
+};
+// Cancel tables default styles.
+const tableStyles = {
+ 'border-collapse': 'collapse',
+ 'text-align': 'inherit',
+};
+function _createTable(attributes = []) {
+ const $table = $('');
+ $table.attr(tableAttributes).css(tableStyles);
+ $table[0].style.setProperty('width', '100%', 'important');
+ for (const attr of attributes) {
+ if (attr.name !== 'width' && attr.value !== '100%') {
+ $table.attr(attr.name, attr.value);
+ }
+ }
+ return $table;
+}
+function _createColumnGrid() {
+ return new Array(12).fill().map(() => $(' | | '));
+}
+function _applyColspanToGridElement($gridElement, colspan) {
+ $gridElement.attr('colspan', colspan);
+ const width = Math.round(+$gridElement.attr('colspan') * 100 / 12) + '%';
+ $gridElement.attr('width', width);
+ $gridElement.css('width', width);
+}
+
+/**
+ * Converts bootstrap rows and columns to actual tables.
+ *
+ * Note: Because of the limited support of media queries in emails, this doesn't
+ * support the mixing and matching of column options (e.g., "col-4 col-sm-6" and
+ * "col col-4" aren't supported).
+ *
+ * @param {jQuery} $editable
+ */
+function bootstrapToTable($editable) {
+ // First give all rows in columns a separate container parent.
+ $editable.find('.row').filter((i, row) => reColMatch.test(row.parentElement.className)).wrap('');
+
+ // Now convert all containers with rows to tables.
+ for (const container of $editable.find('.container:has(.row), .container-fluid:has(.row), .o_fake_table:has(.row)')) {
+ const $container = $(container);
+
+
+ // TABLE
+ const $table = _createTable(container.attributes);
+ for (const child of [...container.childNodes]) {
+ $table.append(child);
+ }
+ $table.removeClass('container container-fluid o_fake_table');
+ $container.before($table);
+ $container.remove();
+
+
+ // ROWS
+ const $bootstrapRows = $table.children().filter('.row');
+ for (const bootstrapRow of $bootstrapRows) {
+ const $bootstrapRow = $(bootstrapRow);
+ const $row = $('
');
+ for (const attr of bootstrapRow.attributes) {
+ $row.attr(attr.name, attr.value);
+ }
+ $row.removeClass('row');
+ for (const child of [...bootstrapRow.childNodes]) {
+ $row.append(child);
+ }
+ $bootstrapRow.before($row);
+ $bootstrapRow.remove();
+
+
+ // COLUMNS
+ const $bootstrapColumns = $row.children().filter((i, column) => column.className && column.className.match(reColMatch));
+
+ // 1. Replace generic "col" classes with specific "col-n", computed
+ // by sharing the available space between them.
+ const $flexColumns = $bootstrapColumns.filter((i, column) => !/\d/.test(column.className.match(reColMatch)[0] || '0'));
+ const colTotalSize = $bootstrapColumns.toArray().map(child => _getColumnSize(child)).reduce((a, b) => a + b);
+ const colSize = Math.round((12 - colTotalSize) / $flexColumns.length);
+ for (const flexColumn of $flexColumns) {
+ flexColumn.classList.remove(flexColumn.className.match(reColMatch)[0].trim());
+ flexColumn.classList.add(`col-${colSize}`);
+ }
+
+ // 2. Create and fill up the row(s) with grid(s).
+ let grid = _createColumnGrid();
+ let gridIndex = 0;
+ let $currentRow = $($row[0].cloneNode());
+ $row.after($currentRow);
+ let $currentCol;
+ let columnIndex = 0;
+ for (const bootstrapColumn of $bootstrapColumns) {
+ const columnSize = _getColumnSize(bootstrapColumn);
+ if (gridIndex + columnSize < 12) {
+ $currentCol = grid[gridIndex];
+ _applyColspanToGridElement($currentCol, columnSize);
+ gridIndex += columnSize;
+ if (columnIndex === $bootstrapColumns.length - 1) {
+ // We handled all the columns but there is still space
+ // in the row. Insert the columns and fill the row.
+ grid[gridIndex].attr('colspan', 12 - gridIndex);
+ $currentRow.append(...grid.filter(td => td.attr('colspan')));
+ }
+ } else if (gridIndex + columnSize === 12) {
+ // Finish the row.
+ $currentCol = grid[gridIndex];
+ _applyColspanToGridElement($currentCol, columnSize);
+ $currentRow.append(...grid.filter(td => td.attr('colspan')));
+ if (columnIndex !== $bootstrapColumns.length - 1) {
+ // The row was filled before we handled all of its
+ // columns. Create a new one and start again from there.
+ const $previousRow = $currentRow;
+ $currentRow = $($currentRow[0].cloneNode());
+ $previousRow.after($currentRow);
+ grid = _createColumnGrid();
+ gridIndex = 0;
+ }
+ } else {
+ // Fill the row with what was in the grid before it
+ // overflowed.
+ _applyColspanToGridElement(grid[gridIndex], 12 - gridIndex);
+ $currentRow.append(...grid.filter(td => td.attr('colspan')));
+ // Start a new row that starts with the current col.
+ const $previousRow = $currentRow;
+ $currentRow = $($currentRow[0].cloneNode());
+ $previousRow.after($currentRow);
+ grid = _createColumnGrid();
+ $currentCol = grid[0];
+ _applyColspanToGridElement($currentCol, columnSize);
+ gridIndex = columnSize;
+ }
+ if ($currentCol) {
+ for (const attr of bootstrapColumn.attributes) {
+ if (attr.name !== 'colspan') {
+ $currentCol.attr(attr.name, attr.value);
+ }
+ }
+ const colMatch = bootstrapColumn.className.match(reColMatch);
+ $currentCol.removeClass(colMatch[0]);
+ for (const child of [...bootstrapColumn.childNodes]) {
+ $currentCol.append(child);
+ }
+ // Adapt width to colspan.
+ _applyColspanToGridElement($currentCol, +$currentCol.attr('colspan'));
+ }
+ columnIndex++;
+ }
+ $row.remove(); // $row was cloned and inserted already
+ }
+ }
+}
+
+function cardToTable($editable) {
+ for (const card of $editable.find('.card')) {
+ const $card = $(card);
+ // Table
+ const $table = _createTable(card.attributes);
+ for (const child of [...card.childNodes]) {
+ if (child.nodeType === Node.TEXT_NODE) {
+ $table.append(child);
+ } else {
+ const $row = $('
');
+ const $col = $(' | ');
+ if (child.nodeName === 'IMG') {
+ $col.append(child);
+ } else {
+ for (const attr of child.attributes) {
+ $col.attr(attr.name, attr.value);
+ }
+ for (const descendant of [...child.childNodes]) {
+ $col.append(descendant);
+ }
+ $(child).remove();
+ }
+ $row.append($col);
+ $table.append($row);
+ }
+ }
+ $card.before($table);
+ $card.remove();
+ }
+}
+
+function listGroupToTable($editable) {
+ for (const listGroup of $editable.find('.list-group')) {
+ const $listGroup = $(listGroup);
+ // Table
+ let $table;
+ if ($listGroup.find('.list-group-item').length) {
+ $table = _createTable(listGroup.attributes);
+ } else {
+ $table = $(listGroup.cloneNode());
+ for (const attr of $listGroup.attributes) {
+ $table.attr(attr.name, attr.value);
+ }
+ }
+ for (const child of [...listGroup.childNodes]) {
+ const $child = $(child);
+ if ($child.hasClass('list-group-item')) {
+ // List groups are s that render like tables. Their
+ // li.list-group-item children should translate to tr > td.
+ const $row = $('
');
+ const $col = $(' | ');
+ for (const attr of child.attributes) {
+ $col.attr(attr.name, attr.value);
+ }
+ for (const descendant of [...child.childNodes]) {
+ $col.append(descendant);
+ }
+ $col.removeClass('list-group-item');
+ $row.append($col);
+ $table.append($row);
+ $(child).remove();
+ } else {
+ $table.append(child);
+ }
+ }
+ $table.removeClass('list-group');
+ if ($listGroup.is('td')) {
+ $listGroup.append($table);
+ $listGroup.removeClass('list-group');
+ } else {
+ $listGroup.before($table);
+ $listGroup.remove();
+ }
+ }
+}
+
+function addTables($editable) {
+ for (const snippet of $editable.find('.o_mail_snippet_general, .o_layout')) {
+ // Convert all snippets and the mailing itself into table > tr > td
+ const $table = _createTable(snippet.attributes);
+ const $row = $('
');
+ const $col = $(' | ');
+ $row.append($col);
+ $table.append($row);
+ for (const child of [...snippet.childNodes]) {
+ $col.append(child);
+ }
+ $(snippet).before($table);
+ $(snippet).remove();
+
+ // If snippet doesn't have a table as child, wrap its contents in one.
+ if (!$col.children().filter('table')) {
+ const $tableB = _createTable();
+ $tableB[0].style.width
+ const $rowB = $('
');
+ const $colB = $(' | ');
+ $rowB.append($colB);
+ $tableB.append($rowB);
+ for (const child of [...$table[0].childNodes]) {
+ $colB.append(child);
+ }
+ $col.append($tableB);
+ }
+ }
+}
+
+const rePadding = /(\d+)/;
+function formatTables($editable) {
+ for (const table of $editable.find('table.o_mail_snippet_general, .o_mail_snippet_general table')) {
+ const $table = $(table);
+ const tablePaddingTop = +$table.css('padding-top').match(rePadding)[1];
+ const tablePaddingRight = +$table.css('padding-right').match(rePadding)[1];
+ const tablePaddingBottom = +$table.css('padding-bottom').match(rePadding)[1];
+ const tablePaddingLeft = +$table.css('padding-left').match(rePadding)[1];
+ for (const column of $table.find('td').filter((i, td) => $(td).closest('table').is($table))) {
+ const $column = $(column);
+ if ($column.css('padding')) {
+ const columnPaddingTop = +$column.css('padding-top').match(rePadding)[1];
+ const columnPaddingRight = +$column.css('padding-right').match(rePadding)[1];
+ const columnPaddingBottom = +$column.css('padding-bottom').match(rePadding)[1];
+ const columnPaddingLeft = +$column.css('padding-left').match(rePadding)[1];
+ $column.css({
+ 'padding-top': columnPaddingTop + tablePaddingTop,
+ 'padding-right': columnPaddingRight + tablePaddingRight,
+ 'padding-bottom': columnPaddingBottom + tablePaddingBottom,
+ 'padding-left': columnPaddingLeft + tablePaddingLeft,
+ });
+ }
+ }
+ $table.css('padding', '');
+ }
+}
+
/**
* Converts css style to inline style (leave the classes on elements but forces
* the style they give as inline style).
@@ -247,9 +645,17 @@ function classToStyle($editable) {
applyOverDescendants($editable[0], function (node) {
var $target = $(node);
var css = getMatchedCSSRules(node);
+ // Flexbox
+ for (const styleName in node.style) {
+ if (styleName.includes('flex') || `${node.style[styleName]}`.includes('flex')) {
+ node.style[styleName] = '';
+ }
+ }
+
+ // Do not apply css that would override inline styles (which are prioritary).
var style = $target.attr('style') || '';
_.each(css, function (v,k) {
- if (!(new RegExp('(^|;)\s*' + k).test(style))) {
+ if (!(new RegExp('(^|;)\\s*' + k).test(style))) {
style = k+':'+v+';'+style;
}
});
@@ -258,6 +664,9 @@ function classToStyle($editable) {
} else {
$target.attr('style', style);
}
+ if ($target.get(0).style.width) {
+ $target.attr('width', $target.css('width')); // Widths need to be applied as attributes as well.
+ }
// Apple Mail
if (node.nodeName === 'TD' && !node.childNodes.length) {
$(node).html(' ');
@@ -265,26 +674,8 @@ function classToStyle($editable) {
// Outlook
if (node.nodeName === 'A' && $target.hasClass('btn') && !$target.hasClass('btn-link') && !$target.children().length) {
- var $hack = $('');
- $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();
- }
+ $target.prepend(``);
+ $target.append(``);
}
else if (node.nodeName === 'IMG' && $target.is('.mx-auto.d-block')) {
$target.wrap('');
@@ -292,6 +683,33 @@ function classToStyle($editable) {
});
}
+// Note: ignores rgba colors, which are not supported in Outlook.
+function normalizeColors($editable) {
+ for (const node of $editable.find('[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)));
+ }
+ }
+}
+
+/**
+ * Converts all css values that use the rem unit to px.
+ *
+ * @param {JQuery} $editable
+ */
+function normalizeRem($editable) {
+ const rootFontSizeProperty = $editable.closest('html').css('font-size');
+ const rootFontSize = parseFloat(rootFontSizeProperty.replace(/[^\d\.]/g, ''));
+ for (const node of $editable.find('[style*="rem"]')) {
+ const remMatch = node.getAttribute('style').match(/[\d\.]+\s*rem/g);
+ for (const rem of remMatch || []) {
+ const remValue = parseFloat(rem.replace(/[^\d\.]/g, ''));
+ node.setAttribute('style', node.getAttribute('style').replace(rem, remValue * rootFontSize + 'px'));
+ }
+ }
+}
+
/**
* Converts css display for attachment link to real image.
* Without this post process, the display depends on the css and the picture
@@ -353,13 +771,20 @@ FieldHtml.include({
attachmentThumbnailToLinkImg($editable);
fontToImg($editable);
classToStyle($editable);
+ bootstrapToTable($editable);
+ cardToTable($editable);
+ listGroupToTable($editable);
+ addTables($editable);
+ formatTables($editable);
+ normalizeColors($editable);
+ normalizeRem($editable);
// fix outlook image rendering bug
_.each(['width', 'height'], function(attribute) {
- $editable.find('img[style*="width"], img[style*="height"]').attr(attribute, function(){
+ $editable.find('img').attr(attribute, function(){
return $(this)[attribute]();
}).css(attribute, function(){
- return $(this).get(0).style[attribute] || 'auto';
+ return $(this).get(0).style[attribute] || $(this)[attribute]() || 'auto';
});
});
$odooEditor.addClass('odoo-editor');
@@ -372,7 +797,14 @@ FieldHtml.include({
return {
fontToImg: fontToImg,
+ bootstrapToTable: bootstrapToTable,
+ cardToTable: cardToTable,
+ listGroupToTable: listGroupToTable,
+ addTables: addTables,
+ formatTables: formatTables,
classToStyle: classToStyle,
+ normalizeColors: normalizeColors,
+ normalizeRem: normalizeRem,
attachmentThumbnailToLinkImg: attachmentThumbnailToLinkImg,
};
});