From 2361bc62e934eae0c0313b4c0223f27d109b898d Mon Sep 17 00:00:00 2001 From: Antoine Guenet Date: Fri, 21 Apr 2023 07:38:30 +0000 Subject: [PATCH] [FIX] web_editor: fix general column responsiveness issues Several issues were found regarding the responsiveness of columns, especially in the Masonry snippet. This implements a new, more robust approach to responsiveness of columns, based on article [1], where each column is wrapped inside a new table, itself wrapped in an inline-block div element, and all adjacent wrapped columns are in turn wrapped in a common table cell: ```html <.container> <.row> <.col id="A"> <.col id="B"> ``` becomes something like: ```html
``` with some additional attributes and styles to make it work. [1]: https://www.litmus.com/blog/mobile-responsive-email-stacking/ task-3184107 X-original-commit: 3ddba4dbd57891c2d9fc80fb7aa0dd08338fe3f6 Part-of: odoo/odoo#124465 --- .../data/mailing_data_templates.xml | 4 + .../static/src/js/backend/convert_inline.js | 107 +++++++++++++----- .../static/tests/convert_inline_tests.js | 8 ++ 3 files changed, 91 insertions(+), 28 deletions(-) diff --git a/addons/mass_mailing/data/mailing_data_templates.xml b/addons/mass_mailing/data/mailing_data_templates.xml index 99253914159..fed92d8892d 100644 --- a/addons/mass_mailing/data/mailing_data_templates.xml +++ b/addons/mass_mailing/data/mailing_data_templates.xml @@ -73,6 +73,10 @@ } } @media screen and (max-width: 1135px) { + .o_stacking_wrapper { + width: 100% !important; + height: unset !important; + } td { max-width: inherit !important; } 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 8fe458aa10a..32ff5c9143c 100644 --- a/addons/web_editor/static/src/js/backend/convert_inline.js +++ b/addons/web_editor/static/src/js/backend/convert_inline.js @@ -1,7 +1,7 @@ /** @odoo-module alias=web_editor.convertInline */ 'use strict'; -import { isBlock, rgbToHex } from '../editor/odoo-editor/src/utils/utils'; +import { getAdjacentPreviousSiblings, isBlock, rgbToHex, commonParentGet } from '../editor/odoo-editor/src/utils/utils'; /* global html2canvas */ @@ -461,38 +461,62 @@ function classToStyle($editable, cssRules) { * @param {Element} editable */ function enforceTablesResponsivity(editable) { - for (const tr of editable.querySelectorAll('tr')) { - tr.style.setProperty('width', '100%'); - } - for (const td of editable.querySelectorAll('td[colspan]')) { - const colspan = +td.getAttribute('colspan'); - const tdSiblings = [...td.parentElement.children].filter(child => child.nodeName === 'TD'); - if ( // Don't allow little duos of columns to wrap (eg., col-2 col-10). - colspan > 2 && colspan < 10 || tdSiblings.length > 2 - || tdSiblings.some(td => [...td.children].some(child => child.style.width === '100%')) // Unless they have a full width child. - ) { - td.setAttribute('width', '100%'); - td.style.setProperty('width', '100%'); - td.style.setProperty('display', 'inline-block'); // Allow cells to wrap. - } else if (td.getAttribute('width') === '100%') { - if (td.children.length === 1 && td.firstElementChild.nodeName === 'IMG') { - const width = td.firstElementChild.getAttribute('width'); - td.setAttribute('width', width); - td.style.removeProperty('width'); - td.style.setProperty('min-width', width + 'px'); + // Trying this: https://www.litmus.com/blog/mobile-responsive-email-stacking/ + const trs = [...editable.querySelectorAll('.o_mail_wrapper tr')] + .filter(tr => [...tr.children].some(td => td.classList.contains('o_converted_col'))) + .reverse(); + for (const tr of trs) { + const commonTable = _createTable(); + commonTable.style.height = '100%'; + const commonTr = document.createElement('tr'); + const commonTd = document.createElement('td'); + commonTr.appendChild(commonTd); + commonTable.appendChild(commonTr); + const tds = [...tr.children].filter(child => child.nodeName === 'TD'); + let index = 0; + for (const td of tds) { + const width = td.style.maxWidth; + const div = document.createElement('div'); + div.style.display = 'inline-block'; + div.style.verticalAlign = 'top'; + div.classList.toggle('o_stacking_wrapper', true); + commonTd.appendChild(div); + const newTable = _createTable(); + newTable.style.width = width; + newTable.classList.toggle('o_stacking_wrapper', true); + div.appendChild(newTable); + const newTr = document.createElement('tr'); + newTable.appendChild(newTr); + newTr.appendChild(td); + td.style.width = '100%'; + td.removeAttribute('width'); + if (index === 0) { + div.before(_createMso(` + + +
`)); } else { - td.removeAttribute('width'); - td.style.removeProperty('width'); + div.before(_createMso(``)); } + if (index === tds.length - 1) { + div.after(_createMso(`
`)); + } + index++; } + const topTd = document.createElement('td'); + topTd.appendChild(commonTable); + tr.prepend(topTd); } - // Masonry has crazy nested tables that require some extra treatment. +} +// Masonry has crazy nested tables that require some extra treatment. +function handleMasonry(editable) { const masonryTrs = editable.querySelectorAll('.s_masonry_block tr'); for (const tr of masonryTrs) { const height = _getHeight(tr); const tds = [...tr.children].filter(child => child.nodeName === 'TD'); const tdsWithTable = tds.filter(td => [...td.children].some(child => child.nodeName === 'TABLE')); if (tdsWithTable.length) { + // TODO: this seems a duplicate of the other o_desktop_h100 set below. // Set the cells' heights to fill their parents. for (const tdWithTable of tdsWithTable) { tdWithTable.classList.toggle('o_desktop_h100', true); @@ -512,6 +536,7 @@ function enforceTablesResponsivity(editable) { } for (const tr of masonryTrs) { const height = tr.style.height.includes('px') ? parseFloat(tr.style.height.replace('px', '').trim()) : _getHeight(tr); + tr.closest('table').classList.toggle('o_desktop_h100', true); tr.classList.toggle('o_desktop_h100', true); for (const td of [...tr.children].filter(child => child.nodeName === 'TD')) { td.classList.toggle('o_desktop_h100', true); @@ -521,10 +546,21 @@ function enforceTablesResponsivity(editable) { // Hack that makes vertical-align possible within an inline-block. const wrapper = document.createElement('div'); wrapper.style.setProperty('display', 'inline-block'); - for (const child of [...td.childNodes]) { + wrapper.style.setProperty('width', '100%'); + // Transfer color to wrapper for Outlook on MacOS/iOS. + const tdStyle = getComputedStyle(td); + wrapper.style.setProperty('color', tdStyle.color); + const firstNonCommentChild = [...td.childNodes].find(child => child.nodeType !== Node.COMMENT_NODE); + let anchor; + if (firstNonCommentChild) { + anchor = getAdjacentPreviousSiblings(firstNonCommentChild) + .filter(sib => sib.nodeType !== Node.TEXT_NODE) + .shift(); + } + for (const child of [...td.childNodes].filter(child => child.nodeType !== Node.COMMENT_NODE)) { wrapper.append(child); } - td.append(wrapper); + anchor ? anchor.after(wrapper) : td.append(wrapper); const centeringSpan = document.createElement('span'); centeringSpan.style.setProperty('height', '100%'); centeringSpan.style.setProperty('display', 'inline-block'); @@ -534,9 +570,16 @@ function enforceTablesResponsivity(editable) { if (td.style.height.includes('%')) { const newHeight = height * parseFloat(td.style.height.replace('%').trim()) / 100; td.style.setProperty('height', newHeight + 'px'); + // Spread height down for responsivity + td.style.setProperty('max-height', newHeight + 'px'); + wrapper.style.setProperty('max-height', newHeight + 'px'); + if (wrapper.childElementCount === 1 && wrapper.firstElementChild.nodeName === 'IMG' && wrapper.firstElementChild.style.height === '100%') { + wrapper.firstElementChild.style.setProperty('max-height', newHeight + 'px'); + } } } } + } } /** * Modify the styles of images so they are responsive. @@ -626,6 +669,7 @@ async function toInline($editable, cssRules, $iframe) { cardToTable(editable); listGroupToTable(editable); addTables($editable); + handleMasonry(editable); normalizeColors($editable); const rootFontSizeProperty = getComputedStyle(editable.ownerDocument.documentElement).fontSize; const rootFontSize = parseFloat(rootFontSizeProperty.replace(/[^\d\.]/g, '')); @@ -645,6 +689,8 @@ async function toInline($editable, cssRules, $iframe) { // Styles were applied inline, we don't need a style element anymore. $editable.find('style').remove(); + editable.querySelectorAll('.o_converted_col').forEach(node => node.classList.remove('o_converted_col')); + for (const [node, displayValue] of displaysToRestore) { node.style.setProperty('display', displayValue); } @@ -871,8 +917,9 @@ function formatTables($editable) { } else if (alignItems === 'flex-end' || alignItems === 'baseline') { row.style.verticalAlign = 'bottom'; } else if (alignItems === 'stretch') { - const columns = [...row.children].filter(child => child.nodeName === 'TD'); - const biggestHeight = Math.max(...columns.map(column => column.clientHeight)); + const columns = [...row.querySelectorAll('td.o_converted_col')]; + const commonAncestor = commonParentGet(columns[0], columns[1]); + const biggestHeight = commonAncestor.clientHeight; for (const column of columns) { column.style.height = biggestHeight + 'px'; } @@ -1047,7 +1094,7 @@ function normalizeRem($editable, rootFontSize=16) { */ function responsiveToStaticForOutlook(editable) { // Replace the responsive tables with static ones for Outlook - for (const td of editable.querySelectorAll('td')) { + for (const td of editable.querySelectorAll('td.o_converted_col:not(.mso-hide)')) { const tdStyle = td.getAttribute('style') || ''; const msoAttributes = [...td.attributes].filter(attr => attr.name !== 'style' && attr.name !== 'width'); const msoWidth = td.style.getPropertyValue('max-width'); @@ -1062,6 +1109,9 @@ function normalizeRem($editable, rootFontSize=16) { } else { outlookTd.setAttribute('style', msoStyles); } + if (td.closest('.s_masonry_block')) { + outlookTd.style.padding = 0; // Not sure why this is needed. + } // The opening tag of `outlookTd` is for Outlook. td.before(_createMso(outlookTd.outerHTML.replace('', ''))); // The opening tag of `td` is for the others. @@ -1122,6 +1172,7 @@ function _applyColspan(element, colspan, tableWidth) { // Round to 2 decimal places. const width = Math.round(tableWidth * widthPercentage * 100) / 100; element.style.setProperty('max-width', width + 'px'); + element.classList.toggle('o_converted_col', true); } /** * Take a selector and return its specificity according to the w3 specification. diff --git a/addons/web_editor/static/tests/convert_inline_tests.js b/addons/web_editor/static/tests/convert_inline_tests.js index ab73b76ad01..ca50c37d748 100644 --- a/addons/web_editor/static/tests/convert_inline_tests.js +++ b/addons/web_editor/static/tests/convert_inline_tests.js @@ -16,6 +16,14 @@ QUnit.module('convert_inline', {}, function () { this.testConvertGrid = ({ before, after, title, stepFunction }) => { this.editable.innerHTML = before; (stepFunction || convertInline.bootstrapToTable)(this.editable); + // Remove class that is added by `bootstrapToTable` for use in + // further methods of `toInline`, and removed at the end of it. + this.editable.querySelectorAll('.o_converted_col').forEach(node => { + node.classList.remove('o_converted_col'); + if (!node.classList.length) { + node.removeAttribute('class'); + } + }); assert.strictEqual(removeComments(this.editable.innerHTML), after, title); } }