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 fac3d6c4d62..9104c2107d3 100644
--- a/addons/web_editor/static/src/js/backend/convert_inline.js
+++ b/addons/web_editor/static/src/js/backend/convert_inline.js
@@ -4,8 +4,457 @@
import FieldHtml from 'web_editor.field.html';
import { isBlock, rgbToHex } from '../../../lib/odoo-editor/src/utils/utils';
-const SELECTORS_IGNORE = /(^\*$|:hover|:before|:after|:active|:link|::|'|\([^(),]+[,(])/;
+//--------------------------------------------------------------------------
+// Constants
+//--------------------------------------------------------------------------
+const RE_COL_MATCH = /(^| )col(-[\w\d]+)*( |$)/;
+const RE_OFFSET_MATCH = /(^| )offset(-[\w\d]+)*( |$)/;
+const RE_PADDING = /([\d.]+)/;
+const SELECTORS_IGNORE = /(^\*$|:hover|:before|:after|:active|:link|::|'|\([^(),]+[,(])/;
+// Attributes all tables should have in a mailing.
+const TABLE_ATTRIBUTES = {
+ cellspacing: 0,
+ cellpadding: 0,
+ border: 0,
+ width: '100%',
+ align: 'center',
+ role: 'presentation',
+};
+// Cancel tables default styles.
+const TABLE_STYLES = {
+ 'border-collapse': 'collapse',
+ 'text-align': 'inherit',
+ 'font-size': 'unset',
+ 'line-height': 'unset',
+};
+
+//--------------------------------------------------------------------------
+// Public
+//--------------------------------------------------------------------------
+
+/**
+ * Convert snippets and mailing bodies to tables.
+ *
+ * @param {JQuery} $editable
+ */
+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').length) {
+ const $tableB = _createTable();
+ $tableB[0].style.width
+ const $rowB = $('
');
+ const $colB = $(' | ');
+ $rowB.append($colB);
+ $tableB.append($rowB);
+ for (const child of [...$col[0].childNodes]) {
+ $colB.append(child);
+ }
+ $col.append($tableB);
+ }
+ }
+}
+/**
+ * Convert 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 = $('
')
+ .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);
+ });
+}
+/**
+ * Convert 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) => RE_COL_MATCH.test(row.parentElement.className)).wrap('');
+
+ // These containers from the mass mailing masonry snippet require full
+ // height contents, which is only possible if the table itself has a set
+ // height. We also need to restyle it because of the change in structure.
+ $editable.find('.o_masonry_grid_container').css('padding', 0)
+ .find('> .o_fake_table').css('height', function() { return $(this).height() });
+ for (const masonryRow of $editable.find('.o_masonry_grid_container > .o_fake_table > .row.h-100')) {
+ masonryRow.style.removeProperty('height');
+ masonryRow.parentElement.style.setProperty('height', '100%');
+ }
+
+ // 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');
+ if (!$table[0].className) {
+ $table.removeAttr('class');
+ }
+ $container.before($table);
+ $container.remove();
+
+
+ // ROWS
+ // First give all siblings of rows a separate row/col parent combo.
+ $table.children().filter((i, child) => isBlock(child) && !$(child).hasClass('row')).wrap('');
+
+ 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');
+ if (!$row[0].className) {
+ $row.removeAttr('class');
+ }
+ 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(RE_COL_MATCH));
+
+ // 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(RE_COL_MATCH)[0] || '0'));
+ const colTotalSize = $bootstrapColumns.toArray().map(child => _getColumnSize(child)).reduce((a, b) => a + b);
+ const colSize = Math.max(1, Math.round((12 - colTotalSize) / $flexColumns.length));
+ for (const flexColumn of $flexColumns) {
+ flexColumn.classList.remove(flexColumn.className.match(RE_COL_MATCH)[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];
+ _applyColspan($currentCol, 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')));
+ }
+ gridIndex += columnSize;
+ } else if (gridIndex + columnSize === 12) {
+ // Finish the row.
+ $currentCol = grid[gridIndex];
+ _applyColspan($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.
+ _applyColspan(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];
+ _applyColspan($currentCol, columnSize);
+ gridIndex = columnSize;
+ if (columnIndex === $bootstrapColumns.length - 1 && gridIndex < 12) {
+ // 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')));
+ // Adapt width to colspan.
+ _applyColspan(grid[gridIndex], 12 - gridIndex);
+ }
+ }
+ if ($currentCol) {
+ for (const attr of bootstrapColumn.attributes) {
+ if (attr.name !== 'colspan') {
+ $currentCol.attr(attr.name, attr.value);
+ }
+ }
+ const colMatch = bootstrapColumn.className.match(RE_COL_MATCH);
+ $currentCol.removeClass(colMatch[0]);
+ if (!$currentCol[0].className) {
+ $currentCol.removeAttr('class');
+ }
+ for (const child of [...bootstrapColumn.childNodes]) {
+ $currentCol.append(child);
+ }
+ // Adapt width to colspan.
+ _applyColspan($currentCol, +$currentCol.attr('colspan'));
+ }
+ columnIndex++;
+ }
+ $row.remove(); // $row was cloned and inserted already
+ }
+ }
+}
+/**
+ * Convert Bootstrap cards to table structures.
+ *
+ * @param {JQuery} $editable
+ */
+function cardToTable($editable) {
+ for (const card of $editable.find('.card')) {
+ const $card = $(card);
+ 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();
+ }
+}
+/**
+ * Convert CSS style to inline style (leave the classes on elements but forces
+ * the style they give as inline style).
+ *
+ * @param {jQuery} $editable
+ * @param {Object} cssRules
+ */
+function classToStyle($editable, cssRules) {
+ _applyOverDescendants($editable[0], function (node) {
+ const $target = $(node);
+ const css = _getMatchedCSSRules(node, cssRules);
+ // 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).
+ let style = $target.attr('style') || '';
+ for (const [key, value] of Object.entries(css)) {
+ if (!(new RegExp(`(^|;)\\s*${key}`).test(style))) {
+ style = `${key}:${value};${style}`;
+ }
+ };
+ if (_.isEmpty(style)) {
+ $target.removeAttr('style');
+ } 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.
+ }
+
+ // Media list images should not have an inline height
+ if (node.nodeName === 'IMG' && $target.hasClass('s_media_list_img')) {
+ $target.css('height', '');
+ }
+ // Apple Mail
+ if (node.nodeName === 'TD' && !node.childNodes.length) {
+ $(node).html(' ');
+ }
+ // Outlook
+ if (node.nodeName === 'A' && $target.hasClass('btn') && !$target.hasClass('btn-link') && !$target.children().length) {
+ $target.prepend(``);
+ $target.append(``);
+ } else if (node.nodeName === 'IMG' && $target.is('.mx-auto.d-block')) {
+ $target.wrap('');
+ }
+ });
+}
+/**
+ * Convert 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, '');
+ 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': style,
+ class: $font.attr('class').replace(new RegExp('(^|\\s+)' + icon + '(-[^\\s]+)?', 'gi'), ''), // remove inline font-awsome style
+ 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();
+ }
+ });
+}
+/**
+ * Format table styles so they display well in most mail clients. This implies
+ * moving table paddings to its cells, adding tbody (with canceled styles) where
+ * needed, and adding pixel heights to parents of elements with percent heights.
+ *
+ * @param {JQuery} $editable
+ */
+function formatTables($editable) {
+ for (const table of $editable.find('table.o_mail_snippet_general, .o_mail_snippet_general table')) {
+ const $table = $(table);
+ const tablePaddingTop = parseFloat($table.css('padding-top').match(RE_PADDING)[1]);
+ const tablePaddingRight = parseFloat($table.css('padding-right').match(RE_PADDING)[1]);
+ const tablePaddingBottom = parseFloat($table.css('padding-bottom').match(RE_PADDING)[1]);
+ const tablePaddingLeft = parseFloat($table.css('padding-left').match(RE_PADDING)[1]);
+ const $rows = $table.find('tr').filter((i, tr) => $(tr).closest('table').is($table));
+ const $columns = $table.find('td').filter((i, td) => $(td).closest('table').is($table));
+ for (const column of $columns) {
+ const $column = $(column);
+ const $columnsInRow = $column.closest('tr').find('td');
+ const columnIndex = $columnsInRow.toArray().findIndex(col => $(col).is($column));
+ const rowIndex = $rows.toArray().findIndex(row => $(row).is($column.closest('tr')));
+ if (!rowIndex) {
+ const match = $column.css('padding-top').match(RE_PADDING);
+ const columnPaddingTop = match ? parseFloat(match[1]) : 0;
+ $column.css('padding-top', columnPaddingTop + tablePaddingTop);
+ }
+ if (columnIndex === $columnsInRow.length - 1) {
+ const match = $column.css('padding-right').match(RE_PADDING);
+ const columnPaddingRight = match ? parseFloat(match[1]) : 0;
+ $column.css('padding-right', columnPaddingRight + tablePaddingRight);
+ }
+ if (rowIndex === $rows.length - 1) {
+ const match = $column.css('padding-bottom').match(RE_PADDING);
+ const columnPaddingBottom = match ? parseFloat(match[1]) : 0;
+ $column.css('padding-bottom', columnPaddingBottom + tablePaddingBottom);
+ }
+ if (!columnIndex) {
+ const match = $column.css('padding-left').match(RE_PADDING);
+ const columnPaddingLeft = match ? parseFloat(match[1]) : 0;
+ $column.css('padding-left', columnPaddingLeft + tablePaddingLeft);
+ }
+ }
+ $table.css('padding', '');
+ }
+ // Ensure a tbody in every table and cancel its default style.
+ for (const table of $editable.find('table:not(:has(tbody))')) {
+ $(table).contents().wrap('');
+ }
+ // Children will only take 100% height if the parent has a height property.
+ for (const node of $editable.find('*').filter((i, n) => (
+ n.style && n.style.getPropertyValue('height') === '100%' && (
+ !n.parentElement.style.getPropertyValue('height') ||
+ n.parentElement.style.getPropertyValue('height').includes('%'))
+ ))) {
+ let parent = node.parentElement;
+ let height = parent.style.getPropertyValue('height');
+ while (parent && height && height.includes('%')) {
+ parent = parent.parentElement;
+ height = parent.style.getPropertyValue('height');
+ }
+ if (parent) {
+ parent.style.setProperty('height', '0');
+ }
+ }
+}
/**
* Parse through the given document's stylesheets, preprocess(*) them and return
* the result as an array of objects, each containing a selector string , a
@@ -112,29 +561,138 @@ function getCSSRules(doc) {
return groupedRules;
}
/**
- * Take a css style declaration return a "normalized" version of it (as a
- * standard object) for the purposes of emails. This means removing its styles
- * that are invalid, describe animations or aren't standard css (webkit
- * extensions). It also involves adding the "!important" suffix to styles that
- * have that priority, so they can be handled without access to the full
- * declaration.
+ * Convert Bootstrap list groups and their items to table structures.
*
- * @param {CSSStyleDeclaration} style
- * @returns {Object} {[styleName]: string}
+ * @param {JQuery} $editable
*/
-function _normalizeStyle(style) {
- const normalizedStyle = {};
- for (const styleName of style) {
- const value = style[styleName];
- if (value && !styleName.includes('animation') && !styleName.includes('-webkit') && _.isString(value)) {
- const normalizedStyleName = styleName.replace(/-(.)/g, (a, b) => b.toUpperCase());
- normalizedStyle[styleName] = style[normalizedStyleName];
- if (style.getPropertyPriority(styleName) === 'important') {
- normalizedStyle[styleName] += ' !important';
+function listGroupToTable($editable) {
+ for (const listGroup of $editable.find('.list-group')) {
+ const $listGroup = $(listGroup);
+ 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');
+ if (!$col[0].className) {
+ $col.removeAttr('class');
+ }
+ $row.append($col);
+ $table.append($row);
+ $(child).remove();
+ } else if (child.nodeName === 'LI') {
+ $table.append(...child.childNodes);
+ } else {
+ $table.append(child);
+ }
+ }
+ $table.removeClass('list-group');
+ if (!$table[0].className) {
+ $table.removeAttr('class');
+ }
+ if ($listGroup.is('td')) {
+ $listGroup.append($table);
+ $listGroup.removeClass('list-group');
+ if (!$listGroup[0].className) {
+ $listGroup.removeAttr('class');
+ }
+ } else {
+ $listGroup.before($table);
+ $listGroup.remove();
+ }
+ }
+}
+/**
+ * Convert all styles containing rgb colors to hexadecimal colors.
+ * Note: ignores rgba colors, which are not supported in Microsoft Outlook.
+ *
+ * @param {JQuery} $editable
+ */
+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)));
+ }
+ }
+}
+/**
+ * Convert 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, ''));
+ const pxValue = Math.round(remValue * rootFontSize * 10) / 10;
+ node.setAttribute('style', node.getAttribute('style').replace(rem, pxValue + 'px'));
+ }
+ }
+}
+
+//--------------------------------------------------------------------------
+// Private
+//--------------------------------------------------------------------------
+
+/**
+ * Take a JQuery element and apply a colspan to it. In this context, this
+ * implies to also apply a width to it, that corresponds to the colspan.
+ *
+ * @param {JQuery} $element
+ * @param {number} colspan
+ */
+function _applyColspan($element, colspan) {
+ $element.attr('colspan', colspan);
+ const width = Math.round(+$element.attr('colspan') * 100 / 12) + '%';
+ $element.attr('width', width);
+ $element.css('width', width);
+}
+/*
+ * 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;
}
- return normalizedStyle;
}
/**
* Take a selector and return its specificity according to the w3 specification.
@@ -152,16 +710,77 @@ function _computeSpecificity(selector) {
selector = selector.replace(/(^|\s+|:+)[a-z0-9_-]+/gi, a => { if (!a.includes(':not(')) c++; return ''; });
return (a * 100) + (b * 10) + c;
}
-
/**
- * Returns the css rules which applies on an element, tweaked so that they are
+ * Return an array of twelve table cells as JQuery elements.
+ *
+ * @returns {JQuery[]}
+ */
+function _createColumnGrid() {
+ return new Array(12).fill().map(() => $(' | '));
+}
+/**
+ * Return a table as a JQuery element, with its default styles and attributes,
+ * as well as the applicable given attributes, if any.
+ *
+ * @see TABLE_ATTRIBUTES
+ * @see TABLE_STYLES
+ * @param {NamedNodeMap | Attr[]} [attributes] default: []
+ * @returns {JQuery}
+ */
+function _createTable(attributes = []) {
+ const $table = $('');
+ $table.attr(TABLE_ATTRIBUTES);
+ $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);
+ }
+ }
+ if ($table.hasClass('o_layout')) {
+ // The top mailing element inherits the body's font size and line-height
+ // and should keep them.
+ const layoutStyles = {...TABLE_STYLES};
+ delete layoutStyles['font-size'];
+ delete layoutStyles['line-height'];
+ $table.css(layoutStyles);
+ } else {
+ for (const styleName in TABLE_STYLES) {
+ if (!('style' in attributes && attributes.style.value.includes(styleName + ':'))) {
+ $table.css(styleName, TABLE_STYLES[styleName]);
+ }
+ }
+ }
+ return $table;
+}
+/**
+ * Take a Bootstrap grid column element and return its size, computed by using
+ * its Bootstrap classes.
+ *
+ * @see RE_COL_MATCH
+ * @see RE_OFFSET_MATCH
+ * @param {Element} column
+ * @returns {number}
+ */
+function _getColumnSize(column) {
+ const colMatch = column.className.match(RE_COL_MATCH);
+ 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(RE_OFFSET_MATCH);
+ const offsetOptions = offsetMatch && offsetMatch[2] && offsetMatch[2].substr(1).split('-');
+ const offsetSize = offsetOptions && (offsetOptions.length === 2 ? +offsetOptions[1] : +offsetOptions[0]) || 0;
+ return colSize + offsetSize;
+}
+/**
+ * Return the CSS rules which applies on an element, tweaked so that they are
* browser/mail client ok.
*
- * @param {DOMElement} node
- * @param {Object} cssRules
- * @returns {Object} css property name -> css property value
+ * @param {Node} node
+ * @param {Object[]} Array<{selector: string;
+ * style: {[styleName]: string};
+ * specificity: number;}>
+ * @returns {Object} {[styleName]: string}
*/
-function getMatchedCSSRules(node, cssRules) {
+function _getMatchedCSSRules(node, cssRules) {
node.matches = node.matches || node.webkitMatchesSelector || node.mozMatchesSelector || node.msMatchesSelector || node.oMatchesSelector;
const css = [];
for (const rule of cssRules) {
@@ -262,606 +881,34 @@ function getMatchedCSSRules(node, cssRules) {
return style;
}
-
/**
- * Converts font icons to images.
+ * Take a css style declaration return a "normalized" version of it (as a
+ * standard object) for the purposes of emails. This means removing its styles
+ * that are invalid, describe animations or aren't standard css (webkit
+ * extensions). It also involves adding the "!important" suffix to styles that
+ * have that priority, so they can be handled without access to the full
+ * declaration.
*
- * @param {jQuery} $editable - the element in which the font icons have to be
- * converted to images
+ * @param {CSSStyleDeclaration} style
+ * @returns {Object} {[styleName]: string}
*/
-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, '');
- 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': style,
- class: $font.attr('class').replace(new RegExp('(^|\\s+)' + icon + '(-[^\\s]+)?', 'gi'), ''), // remove inline font-awsome style
- 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();
- }
- });
-}
-
-/*
- * 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;
- }
-}
-
-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',
- 'font-size': 'unset',
- 'line-height': 'unset',
-};
-function _createTable(attributes = []) {
- const $table = $('');
- $table.attr(tableAttributes);
- $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);
- }
- }
- if ($table.hasClass('o_layout')) {
- // The top mailing element inherits the body's font size and line-height
- // and should keep them.
- const layoutStyles = {...tableStyles};
- delete layoutStyles['font-size'];
- delete layoutStyles['line-height'];
- $table.css(layoutStyles);
- } else {
- for (const styleName in tableStyles) {
- if (!('style' in attributes && attributes.style.value.includes(styleName + ':'))) {
- $table.css(styleName, tableStyles[styleName]);
+function _normalizeStyle(style) {
+ const normalizedStyle = {};
+ for (const styleName of style) {
+ const value = style[styleName];
+ if (value && !styleName.includes('animation') && !styleName.includes('-webkit') && _.isString(value)) {
+ const normalizedStyleName = styleName.replace(/-(.)/g, (a, b) => b.toUpperCase());
+ normalizedStyle[styleName] = style[normalizedStyleName];
+ if (style.getPropertyPriority(styleName) === 'important') {
+ normalizedStyle[styleName] += ' !important';
}
}
}
- return $table;
+ return normalizedStyle;
}
-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('');
-
- // These containers from the mass mailing masonry snippet require full
- // height contents, which is only possible if the table itself has a set
- // height. We also need to restyle it because of the change in structure.
- $editable.find('.o_masonry_grid_container').css('padding', 0)
- .find('> .o_fake_table').css('height', function() { return $(this).height() });
- for (const masonryRow of $editable.find('.o_masonry_grid_container > .o_fake_table > .row.h-100')) {
- masonryRow.style.removeProperty('height');
- masonryRow.parentElement.style.setProperty('height', '100%');
- }
-
- // 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');
- if (!$table[0].className) {
- $table.removeAttr('class');
- }
- $container.before($table);
- $container.remove();
-
-
- // ROWS
- // First give all siblings of rows a separate row/col parent combo.
- $table.children().filter((i, child) => isBlock(child) && !$(child).hasClass('row')).wrap('');
-
- 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');
- if (!$row[0].className) {
- $row.removeAttr('class');
- }
- 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.max(1, 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);
- 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')));
- }
- gridIndex += columnSize;
- } 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 (columnIndex === $bootstrapColumns.length - 1 && gridIndex < 12) {
- // 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')));
- // Adapt width to colspan.
- _applyColspanToGridElement(grid[gridIndex], 12 - gridIndex);
- }
- }
- 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]);
- if (!$currentCol[0].className) {
- $currentCol.removeAttr('class');
- }
- 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 if (child.nodeName === 'LI') {
- $table.append(...child.childNodes);
- } 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();
- }
- }
-}
-
-/**
- * Convert snippets and mailing bodies to tables.
- *
- * @param {JQuery} $editable
- */
-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').length) {
- const $tableB = _createTable();
- $tableB[0].style.width
- const $rowB = $('
');
- const $colB = $(' | ');
- $rowB.append($colB);
- $tableB.append($rowB);
- for (const child of [...$col[0].childNodes]) {
- $colB.append(child);
- }
- $col.append($tableB);
- }
- }
-}
-
-const rePadding = /([\d.]+)/;
-/**
- * Format table styles so they display well in most mail clients. This implies
- * moving table paddings to its cells, adding tbody (with canceled styles) where
- * needed, and adding pixel heights to parents of elements with percent heights.
- *
- * @param {JQuery} $editable
- */
-function formatTables($editable) {
- for (const table of $editable.find('table.o_mail_snippet_general, .o_mail_snippet_general table')) {
- const $table = $(table);
- const tablePaddingTop = parseFloat($table.css('padding-top').match(rePadding)[1]);
- const tablePaddingRight = parseFloat($table.css('padding-right').match(rePadding)[1]);
- const tablePaddingBottom = parseFloat($table.css('padding-bottom').match(rePadding)[1]);
- const tablePaddingLeft = parseFloat($table.css('padding-left').match(rePadding)[1]);
- const $rows = $table.find('tr').filter((i, tr) => $(tr).closest('table').is($table));
- const $columns = $table.find('td').filter((i, td) => $(td).closest('table').is($table));
- for (const column of $columns) {
- const $column = $(column);
- const $columnsInRow = $column.closest('tr').find('td');
- const columnIndex = $columnsInRow.toArray().findIndex(col => $(col).is($column));
- const rowIndex = $rows.toArray().findIndex(row => $(row).is($column.closest('tr')));
- if (!rowIndex) {
- const match = $column.css('padding-top').match(rePadding);
- const columnPaddingTop = match ? parseFloat(match[1]) : 0;
- $column.css('padding-top', columnPaddingTop + tablePaddingTop);
- }
- if (columnIndex === $columnsInRow.length - 1) {
- const match = $column.css('padding-right').match(rePadding);
- const columnPaddingRight = match ? parseFloat(match[1]) : 0;
- $column.css('padding-right', columnPaddingRight + tablePaddingRight);
- }
- if (rowIndex === $rows.length - 1) {
- const match = $column.css('padding-bottom').match(rePadding);
- const columnPaddingBottom = match ? parseFloat(match[1]) : 0;
- $column.css('padding-bottom', columnPaddingBottom + tablePaddingBottom);
- }
- if (!columnIndex) {
- const match = $column.css('padding-left').match(rePadding);
- const columnPaddingLeft = match ? parseFloat(match[1]) : 0;
- $column.css('padding-left', columnPaddingLeft + tablePaddingLeft);
- }
- }
- $table.css('padding', '');
- }
- // Ensure a tbody in every table and cancel its default style.
- for (const table of $editable.find('table:not(:has(tbody))')) {
- $(table).contents().wrap('');
- }
- // Children will only take 100% height if the parent has a height property.
- for (const node of $editable.find('*').filter((i, n) => (
- n.style && n.style.getPropertyValue('height') === '100%' && (
- !n.parentElement.style.getPropertyValue('height') ||
- n.parentElement.style.getPropertyValue('height').includes('%'))
- ))) {
- let parent = node.parentElement;
- let height = parent.style.getPropertyValue('height');
- while (parent && height && height.includes('%')) {
- parent = parent.parentElement;
- height = parent.style.getPropertyValue('height');
- }
- if (parent) {
- parent.style.setProperty('height', '0');
- }
- }
-}
-
-/**
- * Converts css style to inline style (leave the classes on elements but forces
- * the style they give as inline style).
- *
- * @param {jQuery} $editable
- * @param {Object} cssRules
- */
-function classToStyle($editable, cssRules) {
- applyOverDescendants($editable[0], function (node) {
- const $target = $(node);
- const css = getMatchedCSSRules(node, cssRules);
- // 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).
- let style = $target.attr('style') || '';
- for (const [key, value] of Object.entries(css)) {
- if (!(new RegExp(`(^|;)\\s*${key}`).test(style))) {
- style = `${key}:${value};${style}`;
- }
- };
- if (_.isEmpty(style)) {
- $target.removeAttr('style');
- } 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.
- }
-
- // Media list images should not have an inline height
- if (node.nodeName === 'IMG' && $target.hasClass('s_media_list_img')) {
- $target.css('height', '');
- }
- // Apple Mail
- if (node.nodeName === 'TD' && !node.childNodes.length) {
- $(node).html(' ');
- }
- // Outlook
- if (node.nodeName === 'A' && $target.hasClass('btn') && !$target.hasClass('btn-link') && !$target.children().length) {
- $target.prepend(``);
- $target.append(``);
- } else if (node.nodeName === 'IMG' && $target.is('.mx-auto.d-block')) {
- $target.wrap('');
- }
- });
-}
-
-/**
- * Converts all styles containing rgb colors to hexadecimal colors.
- * Note: ignores rgba colors, which are not supported in Microsoft Outlook.
- *
- * @param {JQuery} $editable
- */
-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, ''));
- const pxValue = Math.round(remValue * rootFontSize * 10) / 10;
- node.setAttribute('style', node.getAttribute('style').replace(rem, pxValue + 'px'));
- }
- }
-}
-
-/**
- * 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 = $('
')
- .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);
- });
-}
-
//--------------------------------------------------------------------------
+// Widget
//--------------------------------------------------------------------------
@@ -934,15 +981,15 @@ FieldHtml.include({
});
export default {
- getCSSRules: getCSSRules,
- fontToImg: fontToImg,
+ addTables: addTables,
+ attachmentThumbnailToLinkImg: attachmentThumbnailToLinkImg,
bootstrapToTable: bootstrapToTable,
cardToTable: cardToTable,
- listGroupToTable: listGroupToTable,
- addTables: addTables,
- formatTables: formatTables,
classToStyle: classToStyle,
+ fontToImg: fontToImg,
+ formatTables: formatTables,
+ getCSSRules: getCSSRules,
+ listGroupToTable: listGroupToTable,
normalizeColors: normalizeColors,
normalizeRem: normalizeRem,
- attachmentThumbnailToLinkImg: attachmentThumbnailToLinkImg,
};