diff --git a/addons/web_editor/static/src/img/snippets_options/bring-backward.svg b/addons/web_editor/static/src/img/snippets_options/bring-backward.svg new file mode 100644 index 00000000000..30cc48ac4d4 --- /dev/null +++ b/addons/web_editor/static/src/img/snippets_options/bring-backward.svg @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/addons/web_editor/static/src/img/snippets_options/bring-forward.svg b/addons/web_editor/static/src/img/snippets_options/bring-forward.svg new file mode 100644 index 00000000000..727c0154b63 --- /dev/null +++ b/addons/web_editor/static/src/img/snippets_options/bring-forward.svg @@ -0,0 +1,6 @@ + \ No newline at end of file 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 new file mode 100644 index 00000000000..f3051b2a567 --- /dev/null +++ b/addons/web_editor/static/src/js/common/grid_layout_utils.js @@ -0,0 +1,208 @@ +/** @odoo-module **/ +'use strict'; + +import {qweb} from 'web.core'; +const rowSize = 50; // 50px. + +/** + * Returns the grid properties: rowGap, rowSize, columnGap and columnSize. + * + * @private + * @param {Element} rowEl the grid element + * @returns {Object} + */ +export function _getGridProperties(rowEl) { + const style = window.getComputedStyle(rowEl); + const rowGap = parseFloat(style.rowGap); + const columnGap = parseFloat(style.columnGap); + const columnSize = (rowEl.clientWidth - 11 * columnGap) / 12; + return {rowGap: rowGap, rowSize: rowSize, columnGap: columnGap, columnSize: columnSize}; +} +/** + * Sets the z-index property of the element to the maximum z-index present in + * the grid increased by one (so it is in front of all the other elements). + * + * @private + * @param {Element} element the element of which we want to set the z-index + * @param {Element} rowEl the parent grid element of the element + */ +export function _setElementToMaxZindex(element, rowEl) { + const childrenEls = [...rowEl.children].filter(el => el !== element); + element.style.zIndex = Math.max(...childrenEls.map(el => el.style.zIndex)) + 1; +} +/** + * Creates the background grid appearing everytime a change occurs in a grid. + * + * @private + * @param {Element} rowEl + * @param {Number} gridHeight + */ +export function _addBackgroundGrid(rowEl, gridHeight) { + const gridProp = _getGridProperties(rowEl); + const rowCount = Math.max(rowEl.dataset.rowCount, gridHeight); + + const backgroundGrid = qweb.render('web_editor.background_grid', { + rowCount: rowCount + 1, rowGap: gridProp.rowGap, rowSize: gridProp.rowSize, + columnGap: gridProp.columnGap, columnSize: gridProp.columnSize, + }); + rowEl.insertAdjacentHTML("afterbegin", backgroundGrid); + return rowEl.firstElementChild; +} +/** + * Updates the number of rows in the grid to the end of the lowest column + * present in it. + * + * @private + * @param {Element} rowEl + */ +export function _resizeGrid(rowEl) { + const columnEls = [...rowEl.children].filter(c => c.classList.contains('o_grid_item')); + rowEl.dataset.rowCount = Math.max(...columnEls.map(el => el.style.gridRowEnd)) - 1; +} +/** + * Removes the properties and elements added to make the drag work. + * + * @private + * @param {Element} rowEl + * @param {Element} column + */ +export function _gridCleanUp(rowEl, columnEl) { + columnEl.style.removeProperty('position'); + columnEl.style.removeProperty('top'); + columnEl.style.removeProperty('left'); + columnEl.style.removeProperty('height'); + columnEl.style.removeProperty('width'); + rowEl.style.removeProperty('position'); +} +/** + * Toggles the row (= child element of containerEl) in grid mode. + * + * @private + * @param {Element} containerEl element with the class "container" + */ +export function _toggleGridMode(containerEl) { + let rowEl = containerEl.querySelector('.row'); + + // If the number of columns is "None", create a column with the content. + if (!rowEl) { + rowEl = document.createElement('div'); + rowEl.classList.add('row'); + + const columnEl = document.createElement('div'); + columnEl.classList.add('col-lg-12'); + + const containerChildren = containerEl.children; + // Looping backwards because elements are removed, so the indexes are + // not lost. + for (let i = containerChildren.length - 1; i >= 0; i--) { + columnEl.prepend(containerChildren[i]); + } + rowEl.appendChild(columnEl); + containerEl.appendChild(rowEl); + } + + // Converting the columns to grid and getting back the number of rows. + const columnEls = rowEl.children; + const columnSize = (rowEl.clientWidth) / 12; + rowEl.style.position = 'relative'; + const rowCount = _placeColumns(columnEls, rowSize, 0, columnSize, 0) - 1; + rowEl.style.removeProperty('position'); + rowEl.dataset.rowCount = rowCount; + + // Removing the classes that break the grid. + const classesToRemove = [...rowEl.classList].filter(c => { + return /^align-items/.test(c); + }); + rowEl.classList.remove(...classesToRemove); + + rowEl.classList.add('o_grid_mode'); +} +/** + * Places each column in the grid based on their position and returns the + * lowest row end. + * + * @private + * @param {HTMLCollection} columnEls + * The children of the row element we are toggling in grid mode. + * @param {Number} rowSize + * @param {Number} rowGap + * @param {Number} columnSize + * @param {Number} columnGap + * @returns {Number} + */ +function _placeColumns(columnEls, rowSize, rowGap, columnSize, columnGap) { + let maxRowEnd = 0; + const columnSpans = []; + const columnCount = columnEls.length; // number of column in the grid. + for (const columnEl of columnEls) { + const style = window.getComputedStyle(columnEl); + // Horizontal placement. + const columnLeft = columnEl.offsetLeft; + // Getting the width of the column. + const paddingLeft = parseFloat(style.paddingLeft); + const width = parseFloat(columnEl.scrollWidth) - 2 * paddingLeft; + const columnSpan = Math.ceil((width + columnGap) / (columnSize + columnGap)); + const columnStart = Math.round(columnLeft / (columnSize + columnGap)) + 1; + const columnEnd = columnStart + columnSpan; + + // Vertical placement. + const columnTop = 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) - paddingTop - paddingBottom; + const rowSpan = Math.ceil((height + rowGap) / (rowSize + rowGap)); + const rowStart = Math.round(columnTop / (rowSize + rowGap)) + 1 + rowOffsetTop; + const rowEnd = rowStart + rowSpan; + + columnEl.style.gridArea = `${rowStart} / ${columnStart} / ${rowEnd} / ${columnEnd}`; + columnEl.classList.add('o_grid_item'); + + // Removing the grid classes (since they end with 0) and adding the + // correct ones. + const regex = /^(g-)/; + const toRemove = [...columnEl.classList].filter(c => { + return regex.test(c); + }); + columnEl.classList.remove(...toRemove); + columnEl.classList.add('g-col-lg-' + columnSpan, 'g-height-' + rowSpan); + + // Setting the initial z-index to the number of columns. + columnEl.style.zIndex = columnCount; + + // Reload the images. + _reloadLazyImages(columnEl); + + maxRowEnd = Math.max(rowEnd, maxRowEnd); + columnSpans.push(columnSpan); + } + + // Removing padding and offset classes. + for (const [i, columnEl] of [...columnEls].entries()) { + 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]); + } + + return maxRowEnd; +} +/** + * Removes and sets back the 'src' attribute of the images inside a column. + * (To avoid the disappearing image problem in Chrome). + * + * @private + * @param {Element} columnEl + */ +export function _reloadLazyImages(columnEl) { + const imageEls = columnEl.querySelectorAll('img'); + for (const imageEl of imageEls) { + const src = imageEl.src; + imageEl.src = ''; + imageEl.src = src; + } +} 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 e01db7a1f38..324a50bdcfd 100644 --- a/addons/web_editor/static/src/js/editor/snippets.editor.js +++ b/addons/web_editor/static/src/js/editor/snippets.editor.js @@ -5,12 +5,13 @@ var concurrency = require('web.concurrency'); var core = require('web.core'); var Dialog = require('web.Dialog'); var dom = require('web.dom'); -const {Markup, sprintf} = require('web.utils'); +const {Markup, sprintf, confine} = require('web.utils'); var Widget = require('web.Widget'); var options = require('web_editor.snippets.options'); const {ColorPaletteWidget} = require('web_editor.ColorPalette'); const SmoothScrollOnDrag = require('web/static/src/js/core/smooth_scroll_on_drag.js'); const {getCSSVariableValue} = require('web_editor.utils'); +const gridUtils = require('@web_editor/js/common/grid_layout_utils'); const QWeb = core.qweb; var _t = core._t; @@ -133,6 +134,8 @@ var SnippetEditor = Widget.extend({ events: { 'click .oe_snippet_remove': '_onRemoveClick', 'wheel': '_onMouseWheel', + 'click .o_send_back': '_onSendBackClick', + 'click .o_bring_front': '_onBringFrontClick', }, custom_events: { 'option_update': '_onOptionUpdate', @@ -214,6 +217,7 @@ var SnippetEditor = Widget.extend({ this._onDragAndDropStop(...args); }, 0); }, + refreshPositions: true, // So the dropzone expands when its size increases. }, }); const modalAncestorEl = this.$target[0].closest('.modal'); @@ -510,6 +514,14 @@ var SnippetEditor = Widget.extend({ this.$target.remove(); this.$el.remove(); + // Resize the grid to have the correct row count. + // Must be done here and not in a dedicated onRemove method because + // onRemove is called before actually removing the element and it + // should be the case in order to resize the grid. + if (this.$target[0].classList.contains('o_grid_item')) { + gridUtils._resizeGrid($parent[0]); + } + var node = $parent[0]; if (node && node.firstChild) { if (!node.firstChild.tagName && node.firstChild.textContent === ' ') { @@ -911,10 +923,47 @@ var SnippetEditor = Widget.extend({ * @private */ _onDragAndDropStart: function () { - this.options.wysiwyg.odooEditor.observerUnactive('dragAndDropMoveSnippet'); this.trigger_up('drag_and_drop_start'); this.options.wysiwyg.odooEditor.automaticStepUnactive(); var self = this; + + // Number of grid columns and rows in the grid item (BS column). + let columnColCount; + let columnRowCount; + const rowEl = this.$target[0].parentNode; + if (rowEl.classList.contains('row') && this.options.isWebsite) { + // Toggle grid mode if it is not already on. + if (!rowEl.classList.contains('o_grid_mode')) { + const containerEl = rowEl.parentNode; + gridUtils._toggleGridMode(containerEl); + } + + this.dragState = {}; + // Computing the moving column width and height in terms of columns + // and rows. + const columnStart = self.$target[0].style.gridColumnStart; + const columnEnd = self.$target[0].style.gridColumnEnd; + const rowStart = self.$target[0].style.gridRowStart; + const rowEnd = self.$target[0].style.gridRowEnd; + + columnColCount = columnEnd - columnStart; + columnRowCount = rowEnd - rowStart; + this.dragState.columnColCount = columnColCount; + this.dragState.columnRowCount = columnRowCount; + + // Deactivate the snippet so the overlay doesn't show. + this.trigger_up('deactivate_snippet', {$snippet: self.$target}); + // Storing the current grid and grid area to use them for the + // history. + this.dragState.previousGrid = rowEl; + this.dragState.prevGridArea = self.$target[0].style.gridArea; + + // Reload the images. + gridUtils._reloadLazyImages(this.$target[0]); + } + + const isPopup = this.$target[0].closest('div.s_popup'); + this.dropped = false; this._dropSiblings = { prev: self.$target.prev()[0], @@ -946,11 +995,35 @@ var SnippetEditor = Widget.extend({ } const canBeSanitizedUnless = this._canBeSanitizedUnless(this.$target[0]); + // Remove the siblings that belong to a snippet in grid mode + // and put the identified grid mode snippets in their own "selector". + const selectorGrids = new Set(); + if (this.$target[0].classList.contains('o_grid_item')) { + if ($selectorSiblings) { + // Looping backwards because elements are removed, so the + // indexes are not lost. + for (let i = $selectorSiblings.length - 1; i >= 0; i--) { + if (isPopup && !$selectorSiblings[i].closest('div.s_popup')) { + // Removing the siblings that are outside the popup if + // the grid item is in a popup. + $selectorSiblings.splice(i, 1); + } else { + const gridSnippet = $selectorSiblings[i].closest('div.o_grid_mode'); + if (gridSnippet) { + $selectorSiblings.splice(i, 1); + selectorGrids.add(gridSnippet); + } + } + } + } + } + this.trigger_up('activate_snippet', {$snippet: this.$target.parent()}); this.trigger_up('activate_insertion_zones', { $selectorSiblings: $selectorSiblings, $selectorChildren: $selectorChildren, canBeSanitizedUnless: canBeSanitizedUnless, + selectorGrids: selectorGrids, }); this.$body.addClass('move-important'); @@ -968,9 +1041,74 @@ var SnippetEditor = Widget.extend({ $('.oe_drop_zone').removeClass('invisible'); } self.dropped = true; - $(this).first().after(self.$target).addClass('invisible'); + const $dropzone = $(this).first().after(self.$target); + $dropzone.addClass('invisible'); + + if ($dropzone[0].classList.contains('oe_grid_zone')) { + // Case where the column we are dragging is over a grid + // dropzone. + const rowEl = $dropzone[0].parentNode; + + // Creating the drag helper. + const dragHelperEl = document.createElement('div'); + dragHelperEl.classList.add('o_we_drag_helper'); + dragHelperEl.style.gridArea = `1 / 1 / ${1 + columnRowCount} / ${1 + columnColCount}`; + rowEl.append(dragHelperEl); + + // Creating the background grid and updating the dropzone + // (in the case where the column over the dropzone is + // bigger than the grid). + const backgroundGridEl = gridUtils._addBackgroundGrid(rowEl, columnRowCount); + const rowCount = Math.max(rowEl.dataset.rowCount, columnRowCount); + $dropzone[0].style.gridRowEnd = rowCount + 1; + + // Setting the background grid, the moving grid item and + // the drag helper z-indexes so they are in front of the + // other elements and in this order. + gridUtils._setElementToMaxZindex(backgroundGridEl, rowEl); + gridUtils._setElementToMaxZindex(self.$target[0], rowEl); + gridUtils._setElementToMaxZindex(dragHelperEl, rowEl); + + // Setting the column height and width to keep its size + // when the grid-area is removed (as it prevents it from + // moving with the mouse). + const gridProp = gridUtils._getGridProperties(rowEl); + const columnHeight = columnRowCount * (gridProp.rowSize + gridProp.rowGap) - gridProp.rowGap; + const columnWidth = columnColCount * (gridProp.columnSize + gridProp.columnGap) - gridProp.columnGap; + self.$target[0].style.height = columnHeight + 'px'; + self.$target[0].style.width = columnWidth + 'px'; + self.$target[0].style.position = 'absolute'; + self.$target[0].style.removeProperty('grid-area'); + rowEl.style.position = 'relative'; + + // Storing useful information and adding an event listener. + self.dragState.startingHeight = rowEl.clientHeight; + self.dragState.currentHeight = rowEl.clientHeight; + self.dragState.dragHelperEl = dragHelperEl; + self.dragState.backgroundGridEl = backgroundGridEl; + self.dragState.dropzoneEl = $dropzone[0]; + self.onDragMove = self._onDragMove.bind(self); + document.body.addEventListener('mousemove', self.onDragMove, false); + } }, out: function () { + const dropzoneEl = this; + const rowEl = dropzoneEl.parentNode; + if (rowEl.classList.contains('o_grid_mode')) { + // Removing the listener + cleaning. + document.body.removeEventListener('mousemove', self.onDragMove, false); + gridUtils._gridCleanUp(rowEl, self.$target[0]); + self.$target[0].style.removeProperty('z-index'); + + // Removing the drag helper and the background grid and + // resizing the grid and the dropzone. + self.dragState.dragHelperEl.remove(); + self.dragState.backgroundGridEl.remove(); + gridUtils._resizeGrid(rowEl); + const rowCount = parseInt(rowEl.dataset.rowCount); + dropzoneEl.style.gridRowEnd = Math.max(rowCount + 1, 1); + } + var prev = self.$target.prev(); if (this === prev[0]) { self.dropped = false; @@ -999,6 +1137,41 @@ var SnippetEditor = Widget.extend({ this.options.wysiwyg.odooEditor.automaticStepSkipStack(); this.options.wysiwyg.odooEditor.unbreakableStepUnactive(); + const rowEl = this.$target[0].parentNode; + if (rowEl && rowEl.classList.contains('o_grid_mode')) { + // Case when dropping the column in a grid. + + // Removing the event listener. + document.body.removeEventListener('mousemove', this.onDragMove, false); + + // Defining the column grid area with its position. + const gridProp = gridUtils._getGridProperties(rowEl); + + const top = parseFloat(this.$target[0].style.top); + const left = parseFloat(this.$target[0].style.left); + + const rowStart = Math.round(top / (gridProp.rowSize + gridProp.rowGap)) + 1; + const columnStart = Math.round(left / (gridProp.columnSize + gridProp.columnGap)) + 1; + const rowEnd = rowStart + this.dragState.columnRowCount; + const columnEnd = columnStart + this.dragState.columnColCount; + + this.$target[0].style.gridArea = `${rowStart} / ${columnStart} / ${rowEnd} / ${columnEnd}`; + + // Cleaning, removing the drag helper and the background grid and + // resizing the grid. + gridUtils._gridCleanUp(rowEl, this.$target[0]); + this.dragState.dragHelperEl.remove(); + this.dragState.backgroundGridEl.remove(); + gridUtils._resizeGrid(rowEl); + + // Setting the z-index to the maximum of the grid. + gridUtils._setElementToMaxZindex(this.$target[0], rowEl); + } else if (this.$target[0].classList.contains('o_grid_item') && this.dropped) { + // 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'); + } + // TODO lot of this is duplicated code of the d&d feature of snippets if (!this.dropped) { const { nearest } = this.$body[0].ownerDocument.defaultView.$; @@ -1006,6 +1179,25 @@ var SnippetEditor = Widget.extend({ // Some drop zones might have been disabled. $el = $el.filter(this.$dropZones); if ($el.length) { + // If the column is not dropped inside a dropzone. + if (this.$target[0].classList.contains('o_grid_item')) { + if ($el[0].classList.contains('oe_grid_zone')) { + // Case when a column is dropped near a grid. + // Placing it in the top left corner. + this.$target[0].style.gridArea = `1 / 1 / ${1 + this.dragState.columnRowCount} / ${1 + this.dragState.columnColCount}`; + const rowEl = $el[0].parentNode; + const rowCount = Math.max(rowEl.dataset.rowCount, 1 + this.dragState.columnRowCount); + rowEl.dataset.rowCount = rowCount; + + // Setting the z-index to the maximum of the grid. + gridUtils._setElementToMaxZindex(this.$target[0], rowEl); + } else { + // 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'); + } + } + $el.after(this.$target); this.dropped = true; } @@ -1053,11 +1245,15 @@ var SnippetEditor = Widget.extend({ $snippet: this.$target, }); this.draggableComponent.$scrollTarget.off('scroll.scrolling_element'); - const samePositionAsStart = this._dropSiblings.prev === this.$target.prev()[0] && this._dropSiblings.next === this.$target.next()[0]; + const samePositionAsStart = this.$target[0].classList.contains('o_grid_item') + ? (this.$target[0].parentNode === this.dragState.previousGrid + && this.$target[0].style.gridArea === this.dragState.prevGridArea) + : this._dropSiblings.prev === this.$target.prev()[0] && this._dropSiblings.next === this.$target.next()[0]; if (!samePositionAsStart) { this.options.wysiwyg.odooEditor.historyStep(); } delete this.$dropZones; + delete this.dragState; }, /** * @private @@ -1177,6 +1373,106 @@ var SnippetEditor = Widget.extend({ this.$el.css('pointer-events', ''); }, 250); }, + /** + * Called when the "send to back" overlay button is clicked. + * + * @private + * @param {Event} ev + */ + _onSendBackClick(ev) { + ev.stopPropagation(); + const rowEl = this.$target[0].parentNode; + const columnEls = [...rowEl.children].filter(el => el !== this.$target[0]); + const minZindex = Math.min(...columnEls.map(el => el.style.zIndex)); + + // While the minimum z-index is not 0, it is OK to decrease it and to + // set the column to it. Otherwise, the column is set to 0 and the + // other columns z-index are increased by one. + if (minZindex > 0) { + this.$target[0].style.zIndex = minZindex - 1; + } else { + for (const columnEl of columnEls) { + columnEl.style.zIndex++; + } + this.$target[0].style.zIndex = 0; + } + }, + /** + * Called when the "bring to front" overlay button is clicked. + * + * @private + * @param {Event} ev + */ + _onBringFrontClick(ev) { + ev.stopPropagation(); + const rowEl = this.$target[0].parentNode; + gridUtils._setElementToMaxZindex(this.$target[0], rowEl); + }, + /** + * Called when the mouse is moved to place a column in a grid. + * + * @private + * @param {Event} ev + */ + _onDragMove(ev) { + const columnEl = this.$target[0]; + const rowEl = columnEl.parentNode; + + // Computing the rowEl position. + const rowElTop = rowEl.getBoundingClientRect().top + document.documentElement.scrollTop; + const rowElLeft = rowEl.getBoundingClientRect().left; + + // Getting the column dimensions. + const borderWidth = parseFloat(window.getComputedStyle(columnEl).borderWidth); + const columnHeight = columnEl.clientHeight + 2 * borderWidth; + const columnWidth = columnEl.clientWidth + 2 * borderWidth; + const columnMiddle = columnWidth / 2; + + // Placing the column where the mouse is. + const top = ev.pageY - rowElTop; + const bottom = top + columnHeight; + let left = ev.pageX - rowElLeft - columnMiddle; + + // Horizontal overflow. + left = confine(left, 0, rowEl.clientWidth - columnWidth); + + columnEl.style.top = top + 'px'; + columnEl.style.left = left + 'px'; + + // Computing the drag helper corresponding grid area. + const gridProp = gridUtils._getGridProperties(rowEl); + + const rowStart = Math.round(top / (gridProp.rowSize + gridProp.rowGap)) + 1; + const columnStart = Math.round(left / (gridProp.columnSize + gridProp.columnGap)) + 1; + const rowEnd = rowStart + this.dragState.columnRowCount; + const columnEnd = columnStart + this.dragState.columnColCount; + + const dragHelperEl = this.dragState.dragHelperEl; + if (parseInt(dragHelperEl.style.gridRowStart) !== rowStart) { + dragHelperEl.style.gridRowStart = rowStart; + dragHelperEl.style.gridRowEnd = rowEnd; + } + + if (parseInt(dragHelperEl.style.gridColumnStart) !== columnStart) { + dragHelperEl.style.gridColumnStart = columnStart; + dragHelperEl.style.gridColumnEnd = columnEnd; + } + + // Vertical overflow/underflow. + // Updating the reference heights, the dropzone and the background grid. + const startingHeight = this.dragState.startingHeight; + const currentHeight = this.dragState.currentHeight; + const backgroundGridEl = this.dragState.backgroundGridEl; + const dropzoneEl = this.dragState.dropzoneEl; + const rowOverflow = Math.round((bottom - currentHeight) / (gridProp.rowSize + gridProp.rowGap)); + const updateRows = bottom > currentHeight || bottom <= currentHeight && bottom > startingHeight; + if (Math.abs(rowOverflow) >= 1 && updateRows) { + const dropzoneEnd = parseInt(dropzoneEl.style.gridRowEnd); + dropzoneEl.style.gridRowEnd = dropzoneEnd + rowOverflow; + backgroundGridEl.style.gridRowEnd = dropzoneEnd + rowOverflow; + this.dragState.currentHeight += rowOverflow * (gridProp.rowSize + gridProp.rowGap); + } + } }); /** @@ -1664,8 +1960,15 @@ var SnippetsMenu = Widget.extend({ * @param {jQuery} [$selectorChildren] * elements which must have child drop zones between each of existing * child + * @param {string or boolean} canBeSanitizedUnless + * true: always allows, + * false: always forbid, + * string: specific type of forbidden sanitization + * @param {Object} [selectorGrids = []] + * elements which are in grid mode and for which a grid dropzone + * needs to be inserted */ - _activateInsertionZones: function ($selectorSiblings, $selectorChildren, canBeSanitizedUnless) { + _activateInsertionZones($selectorSiblings, $selectorChildren, canBeSanitizedUnless, selectorGrids = []) { var self = this; // If a modal or a dropdown is open, the drop zones must be created @@ -1719,9 +2022,9 @@ var SnippetsMenu = Widget.extend({ }; } - // Firstly, add a dropzone after the clone + // Firstly, add a dropzone after the clone (if we are not in grid mode). var $clone = this.$body.find('.oe_drop_clone'); - if ($clone.length) { + if ($clone.length && $clone.closest('div.o_grid_mode').length === 0) { var $neighbor = $clone.prev(); if (!$neighbor.length) { $neighbor = $clone.next(); @@ -1841,6 +2144,11 @@ var SnippetsMenu = Widget.extend({ }; } }); + + // Inserting a grid dropzone for each row in grid mode. + for (const rowEl of selectorGrids) { + self._insertGridDropzone(rowEl); + } }, /** * Adds an entry for every invisible snippet in the left panel box. @@ -2490,6 +2798,24 @@ var SnippetsMenu = Widget.extend({ $hook.replaceWith($dropzone); return $dropzone; }, + /** + * Creates a dropzone taking the entire area of the row in grid mode in + * which it will be added. It allows to place elements dragged over it + * inside the grid it belongs to. + * + * @param {Element} rowEl + */ + _insertGridDropzone(rowEl) { + const columnCount = 12; + const rowCount = parseInt(rowEl.dataset.rowCount); + let $dropzone = $('
', { + 'class': 'oe_drop_zone oe_insert oe_grid_zone', + 'style': 'grid-area: ' + 1 + '/' + 1 + '/' + (rowCount + 1) + '/' + (columnCount + 1), + }); + $dropzone[0].style.minHeight = window.getComputedStyle(rowEl).height; + $dropzone[0].style.width = window.getComputedStyle(rowEl).width; + rowEl.append($dropzone[0]); + }, /** * Make given snippets be draggable/droppable thanks to their thumbnail. * @@ -2905,7 +3231,7 @@ var SnippetsMenu = Widget.extend({ * @param {OdooEvent} ev */ _onActivateInsertionZones: function (ev) { - this._activateInsertionZones(ev.data.$selectorSiblings, ev.data.$selectorChildren, ev.data.canBeSanitizedUnless); + this._activateInsertionZones(ev.data.$selectorSiblings, ev.data.$selectorChildren, ev.data.canBeSanitizedUnless, ev.data.selectorGrids); }, /** * Called when a child editor asks to deactivate the current snippet @@ -3544,18 +3870,37 @@ var SnippetsMenu = Widget.extend({ }); }); }, false); + + // Reload images inside grid items so that no image disappears when + // activating mobile preview. + const gridItemEls = this.getEditableArea().find('div.o_grid_item'); + for (const gridItemEl of gridItemEls) { + gridUtils._reloadLazyImages(gridItemEl); + } }, /** * Undo.. */ _onUndo: async function () { this.options.wysiwyg.undo(); + // Resizing all the grids. + // TODO maybe to remove when history will be fixed. + const $gridModeRows = this.getEditableArea().find('.row.o_grid_mode'); + for (const rowEl of $gridModeRows) { + gridUtils._resizeGrid(rowEl); + } }, /** * Redo. */ _onRedo: async function () { this.options.wysiwyg.redo(); + // Resizing all the grids. + // TODO maybe to remove when history will be fixed. + const $gridModeRows = this.getEditableArea().find('.row.o_grid_mode'); + for (const rowEl of $gridModeRows) { + gridUtils._resizeGrid(rowEl); + } }, /** * @private 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 cec98913686..cff72fd145d 100644 --- a/addons/web_editor/static/src/js/editor/snippets.options.js +++ b/addons/web_editor/static/src/js/editor/snippets.options.js @@ -13,6 +13,7 @@ const utils = require('web.utils'); var Widget = require('web.Widget'); var ColorPaletteWidget = require('web_editor.ColorPalette').ColorPaletteWidget; const weUtils = require('web_editor.utils'); +const gridUtils = require('@web_editor/js/common/grid_layout_utils'); const { normalizeColor, getBgImageURL, @@ -3744,11 +3745,17 @@ const SnippetOptionWidget = Widget.extend({ * @returns {Promise