diff --git a/addons/web_editor/static/src/js/common/grid_layout_utils.js b/addons/web_editor/static/src/js/common/grid_layout_utils.js index 8e071690ba2..b3435e09155 100644 --- a/addons/web_editor/static/src/js/common/grid_layout_utils.js +++ b/addons/web_editor/static/src/js/common/grid_layout_utils.js @@ -141,26 +141,36 @@ function _placeColumns(columnEls, rowSize, rowGap, columnSize, columnGap) { const allBackgroundColor = [...columnEls].every(columnEl => columnEl.classList.contains('o_cc')); for (const columnEl of columnEls) { + // Finding out if the images are alone in their column. + let isImageColumn = _checkIfImageColumn(columnEl); + const imageEl = columnEl.querySelector('img'); + + // Placing the column. const style = window.getComputedStyle(columnEl); // Horizontal placement. - const columnLeft = columnEl.offsetLeft; + const columnLeft = isImageColumn ? imageEl.offsetLeft : columnEl.offsetLeft; // Getting the width of the column. const paddingLeft = parseFloat(style.paddingLeft); - const width = parseFloat(columnEl.scrollWidth) - (allBackgroundColor ? 0 : 2 * paddingLeft); - const columnSpan = Math.round((width + columnGap) / (columnSize + columnGap)); + const width = isImageColumn ? parseFloat(imageEl.scrollWidth) + : parseFloat(columnEl.scrollWidth) - (allBackgroundColor ? 0 : 2 * paddingLeft); + let columnSpan = Math.round((width + columnGap) / (columnSize + columnGap)); + if (columnSpan < 1) { + columnSpan = 1; + } const columnStart = Math.round(columnLeft / (columnSize + columnGap)) + 1; const columnEnd = columnStart + columnSpan; // Vertical placement. - const columnTop = columnEl.offsetTop; + const columnTop = isImageColumn ? imageEl.offsetTop : columnEl.offsetTop; // Getting the top and bottom paddings and computing the row offset. const paddingTop = parseFloat(style.paddingTop); const paddingBottom = parseFloat(style.paddingBottom); const rowOffsetTop = Math.floor((paddingTop + rowGap) / (rowSize + rowGap)); // Getting the height of the column. - const height = parseFloat(columnEl.scrollHeight) - (allBackgroundColor ? 0 : paddingTop + paddingBottom); + const height = isImageColumn ? parseFloat(imageEl.scrollHeight) + : parseFloat(columnEl.scrollHeight) - (allBackgroundColor ? 0 : paddingTop + paddingBottom); const rowSpan = Math.ceil((height + rowGap) / (rowSize + rowGap)); - const rowStart = Math.round(columnTop / (rowSize + rowGap)) + 1 + (allBackgroundColor ? 0 : rowOffsetTop); + const rowStart = Math.round(columnTop / (rowSize + rowGap)) + 1 + (allBackgroundColor || isImageColumn ? 0 : rowOffsetTop); const rowEnd = rowStart + rowSpan; columnEl.style.gridArea = `${rowStart} / ${columnStart} / ${rowEnd} / ${columnEnd}`; @@ -183,6 +193,7 @@ function _placeColumns(columnEls, rowSize, rowGap, columnSize, columnGap) { maxRowEnd = Math.max(rowEnd, maxRowEnd); columnSpans.push(columnSpan); + imageColumns.push(isImageColumn); } // If all the columns have a background color, set their padding to the @@ -196,14 +207,19 @@ function _placeColumns(columnEls, rowSize, rowGap, columnSize, columnGap) { rowEl.style.setProperty('--grid-item-padding-x', paddingX); } - // Removing padding and offset classes. for (const [i, columnEl] of [...columnEls].entries()) { + // Removing padding and offset classes. const regex = /^(pt|pb|col-|offset-)/; const toRemove = [...columnEl.classList].filter(c => { return regex.test(c); }); columnEl.classList.remove(...toRemove); columnEl.classList.add('col-lg-' + columnSpans[i]); + + // If the column only has an image, convert it. + if (imageColumns[i]) { + _convertImageColumn(columnEl); + } } return maxRowEnd; @@ -223,3 +239,42 @@ export function _reloadLazyImages(columnEl) { imageEl.src = src; } } +/** + * Checks whether the column only contains an image or not. An image is + * considered alone if the column only contains empty textnodes and line breaks + * in addition to the image. + * + * @private + * @param {Element} columnEl + * @returns {Boolean} + */ +export function _checkIfImageColumn(columnEl) { + let isImageColumn = false; + const imageEls = columnEl.querySelectorAll(':scope > img'); + const columnChildrenEls = [...columnEl.children].filter(el => el.nodeName !== 'BR'); + if (imageEls.length === 1 && columnChildrenEls.length === 1) { + // If there is only one image and if this image is the only "real" + // child of the column, we need to check if there is text in it. + const textNodeEls = [...columnEl.childNodes].filter(el => el.nodeType === Node.TEXT_NODE); + const areTextNodesEmpty = [...textNodeEls].every(textNodeEl => textNodeEl.nodeValue.trim() === ''); + isImageColumn = areTextNodesEmpty; + } + return isImageColumn; +} +/** + * Removes the line breaks and textnodes of the column, adds the grid class and + * sets the image width to default so it can be displayed as expected. Also + * blocks the edition of the column. + * + * @private + * @param {Element} columnEl a column containing only an image. + */ +function _convertImageColumn(columnEl) { + columnEl.querySelectorAll('br').forEach(el => el.remove()); + const textNodeEls = [...columnEl.childNodes].filter(el => el.nodeType === Node.TEXT_NODE); + textNodeEls.forEach(el => el.remove()); + const imageEl = columnEl.querySelector('img'); + columnEl.classList.add('o_grid_item_image'); + columnEl.contentEditable = false; + imageEl.style.removeProperty('width'); +} diff --git a/addons/web_editor/static/src/js/editor/snippets.editor.js b/addons/web_editor/static/src/js/editor/snippets.editor.js index 551244f2ae9..ce7a6dd14d0 100644 --- a/addons/web_editor/static/src/js/editor/snippets.editor.js +++ b/addons/web_editor/static/src/js/editor/snippets.editor.js @@ -263,6 +263,12 @@ var SnippetEditor = Widget.extend({ // a flickering when not needed. this.$target.on('transitionend.snippet_editor, animationend.snippet_editor', postAnimationCover); + // Set the `contenteditable` attribute to false for all the columns + // having the class `o_grid_item_image` (as it is removed when leaving + // the edit mode). + const imageColumnEls = this.$target[0].querySelectorAll('.o_grid_item_image'); + imageColumnEls.forEach(imageColumnEl => imageColumnEl.contentEditable = false); + return Promise.all(defs).then(() => { this.__isStartedResolveFunc(this); }); @@ -1177,6 +1183,10 @@ var SnippetEditor = Widget.extend({ // Case when dropping a grid item in a non-grid dropzone. this.$target[0].classList.remove('o_grid_item'); this.$target[0].style.removeProperty('grid-area'); + if (this.$target[0].classList.contains('o_grid_item_image')) { + this.$target[0].classList.remove('o_grid_item_image'); + this.$target[0].removeAttribute('contentEditable'); + } } // TODO lot of this is duplicated code of the d&d feature of snippets @@ -1202,6 +1212,10 @@ var SnippetEditor = Widget.extend({ // Case when a column is dropped near a non-grid dropzone. this.$target[0].classList.remove('o_grid_item'); this.$target[0].style.removeProperty('z-index'); + if (this.$target[0].classList.contains('o_grid_item_image')) { + this.$target[0].classList.remove('o_grid_item_image'); + this.$target[0].removeAttribute('contentEditable'); + } } } diff --git a/addons/web_editor/static/src/js/editor/snippets.options.js b/addons/web_editor/static/src/js/editor/snippets.options.js index 3ba4414db77..4a1d7e7d94b 100644 --- a/addons/web_editor/static/src/js/editor/snippets.options.js +++ b/addons/web_editor/static/src/js/editor/snippets.options.js @@ -4965,7 +4965,8 @@ registry.layout_column = SnippetOptionWidget.extend({ if (elementType === 'image') { // Set the columns properties. - newColumnEl.classList.add('col-lg-6', 'g-col-lg-6', 'g-height-6'); + newColumnEl.classList.add('col-lg-6', 'g-col-lg-6', 'g-height-6', 'o_grid_item_image'); + newColumnEl.contentEditable = false; numberColumns = 6; numberRows = 6; @@ -5109,6 +5110,10 @@ registry.layout_column = SnippetOptionWidget.extend({ columnEl.classList.remove('o_grid_item'); columnEl.style.removeProperty('grid-area'); columnEl.style.removeProperty('z-index'); + if (columnEl.classList.contains('o_grid_item_image')) { + columnEl.classList.remove('o_grid_item_image'); + columnEl.removeAttribute('contentEditable'); + } } // Removing the grid properties. delete rowEl.dataset.rowCount; diff --git a/addons/web_editor/static/src/scss/web_editor.frontend.scss b/addons/web_editor/static/src/scss/web_editor.frontend.scss index 9c32a7ddac5..47a3d8ca914 100644 --- a/addons/web_editor/static/src/scss/web_editor.frontend.scss +++ b/addons/web_editor/static/src/scss/web_editor.frontend.scss @@ -71,9 +71,9 @@ .o_grid_item { word-break: break-word; - > img { - width: 100%; - height: 100%; + &.o_grid_item_image > img:not([data-shape]) { + width: 100% !important; + height: 100% !important; object-fit: cover !important; } } diff --git a/addons/website/views/snippets/snippets.xml b/addons/website/views/snippets/snippets.xml index 2f045081bbe..769a6270ace 100644 --- a/addons/website/views/snippets/snippets.xml +++ b/addons/website/views/snippets/snippets.xml @@ -623,7 +623,7 @@ blockquote, .s_card:not(.s_timeline_card), .s_alert, .o_facebook_page, .s_share, .s_social_media, .s_rating, .s_hr, .s_google_map, .s_map, .s_countdown, .s_chart, .s_text_highlight, .s_progress_bar, .s_badge, .s_embed_code, .s_donation, .s_add_to_cart