[IMP] web_editor, website: add a grid layout

This commit adds the possibility to use a grid to move and resize
columns inside some building blocks.

More precisely, for the majority of the building blocks having columns,
it is now possible to toggle between the current layout (= normal mode)
and the grid layout (= grid mode). The grid mode can be triggered in
two ways:
	- by using the "Grid" button in the options (when available),
	- by dragging a column using the move handle.

The toggle places the different columns in the grid as close as
possible as how they were placed in normal mode.

The normal mode is toggled back by clicking on the "Cols" button (when
available).
Since the move handle is now used to trigger the grid mode, the columns
in normal mode are now moved using the arrows.

NB: to avoid any confusion with the word 'column' in the explanation
below, the BS column will be called 'grid item' and the grid column
will be 'column'.

When in grid mode, the building block is a grid with 12 columns and a
number of rows which depends on the height of the grid items.
This mode offers the following functionalities:

- Moving a grid item:
When a grid item is dragged, it can be placed anywhere in the grid. If
it is dragged towards the bottom, new rows are added in the grid to
increase its height and to place the grid item lower. These added rows
are removed when it is dragged towards the top.

A grid item can also be placed over an other item (-> they can overlap)
and their order can be changed using the two new buttons to place an
item in front of/behind all the others.

A grid item can be moved from a grid to another, and also from a grid
to a building block in normal mode.

- Resizing a grid item:
A grid item can be resized vertically, horizontally and diagonally with
the help of the grid. When resized towards the bottom, new rows are
added in the grid and they are removed if it is towards the top (like
with the drag and drop).

- Adding elements:
The "Add Elements" option allows the user to add as many images, text
blocks or buttons as wanted in the grid.

Inner contents can still be dragged and dropped inside a grid item.

- Changing the padding of the grid items:
The padding option allows to change the vertical/horizontal padding of
all the grid items at the same time.

- Mobile view:
When in mobile view, the grid items are not in a grid anymore (the
display is back to flex) but they keep their grid properties. They
cannot be resized, nor dragged and dropped but they can be moved using
the arrows. By moving this way, it does not impact the desktop view.

task-2825241

closes odoo/odoo#93144

Signed-off-by: Quentin Smetz (qsm) <qsm@odoo.com>
This commit is contained in:
Soukéina Bojabza
2022-09-04 19:25:43 +02:00
committed by qsm-odoo
parent ca215a5bdf
commit cc406afcea
10 changed files with 1253 additions and 81 deletions
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<g class="o_overlay_back_front">
<rect x="4" y="4" width="9" height="9" class="o_graphic" fill="#EEE"/>
<rect x="7" y="7" width="9" height="9" class="o_subdle" fill="#AAA"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 293 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<g class="o_overlay_back_front">
<rect x="4" y="4" width="9" height="9" class="o_subdle" fill="#AAA"/>
<rect x="7" y="7" width="9" height="9" class="o_graphic" fill="#EEE"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 275 B

@@ -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;
}
}
@@ -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 = $('<div/>', {
'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
@@ -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<boolean>|boolean}
*/
_computeWidgetVisibility: async function (widgetName, params) {
if (widgetName === 'move_up_opt' || widgetName === 'move_left_opt') {
return !this.$target.is(':first-child');
}
if (widgetName === 'move_down_opt' || widgetName === 'move_right_opt') {
return !this.$target.is(':last-child');
const moveUpOrLeft = widgetName === 'move_up_opt' || widgetName === 'move_left_opt';
const moveDownOrRight = widgetName === 'move_down_opt' || widgetName === 'move_right_opt';
if (moveUpOrLeft || moveDownOrRight) {
// The arrows are not displayed if the target is in a grid and if not in mobile view.
const isMobileView = this.$target[0].ownerDocument.defaultView.frameElement.clientWidth < 768;
if (this.$target[0].classList.contains('o_grid_item') && !isMobileView) {
return false;
}
const firstOrLastChild = moveUpOrLeft ? ':first-child' : ':last-child';
return !this.$target.is(firstOrLastChild);
}
return true;
},
@@ -4185,22 +4192,28 @@ registry.sizing = SnippetOptionWidget.extend({
* @override
*/
start: function () {
var self = this;
var def = this._super.apply(this, arguments);
const self = this;
const def = this._super.apply(this, arguments);
this.$handles = this.$overlay.find('.o_handle');
var resizeValues = this._getSize();
let resizeValues = this._getSize();
this.$handles.on('mousedown', function (ev) {
ev.preventDefault();
// If the handle has the class 'readonly', don't allow to resize.
// (For the grid handles when we are in mobile view).
if (ev.currentTarget.classList.contains('readonly')) {
return;
}
// First update size values as some element sizes may not have been
// initialized on option start (hidden slides, etc)
resizeValues = self._getSize();
var $handle = $(ev.currentTarget);
const $handle = $(ev.currentTarget);
var compass = false;
var XY = false;
let compass = false;
let XY = false;
if ($handle.hasClass('n')) {
compass = 'n';
XY = 'Y';
@@ -4213,74 +4226,156 @@ registry.sizing = SnippetOptionWidget.extend({
} else if ($handle.hasClass('w')) {
compass = 'w';
XY = 'X';
} else if ($handle.hasClass('nw')) {
compass = 'nw';
XY = 'YX';
} else if ($handle.hasClass('ne')) {
compass = 'ne';
XY = 'YX';
} else if ($handle.hasClass('sw')) {
compass = 'sw';
XY = 'YX';
} else if ($handle.hasClass('se')) {
compass = 'se';
XY = 'YX';
}
var resize = resizeValues[compass];
if (!resize) {
// Don't call the normal resize methods if we are in a grid and
// vice-versa.
const isGrid = Object.keys(resizeValues).length === 4;
const isGridHandle = $handle[0].classList.contains('o_grid_handle');
if (isGrid && !isGridHandle || !isGrid && isGridHandle) {
return;
}
var current = 0;
var cssProperty = resize[2];
var cssPropertyValue = parseInt(self.$target.css(cssProperty));
_.each(resize[0], function (val, key) {
if (self.$target.hasClass(val)) {
current = key;
} else if (resize[1][key] === cssPropertyValue) {
current = key;
}
});
var begin = current;
var beginClass = self.$target.attr('class');
var regClass = new RegExp('\\s*' + resize[0][begin].replace(/[-]*[0-9]+/, '[-]*[0-9]+'), 'g');
let resizeVal;
if (compass.length > 1) {
resizeVal = [resizeValues[compass[0]], resizeValues[compass[1]]];
} else {
resizeVal = [resizeValues[compass]];
}
var cursor = $handle.css('cursor') + '-important';
var $body = $(this.ownerDocument.body);
if (resizeVal.some(rV => !rV)) {
return;
}
// If we are in grid mode, add a background grid and place the element
// we are resizing in front of it.
const rowEl = self.$target[0].parentNode;
let backgroundGridEl;
if (rowEl.classList.contains('o_grid_mode')) {
self.options.wysiwyg.odooEditor.observerUnactive('displayBackgroundGrid');
backgroundGridEl = gridUtils._addBackgroundGrid(rowEl, 0);
self.options.wysiwyg.odooEditor.observerActive('displayBackgroundGrid');
gridUtils._setElementToMaxZindex(backgroundGridEl, rowEl);
gridUtils._setElementToMaxZindex(self.$target[0], rowEl);
}
// For loop to handle the cases where it is ne, nw, se or sw. Since there are two
// directions, we compute for both directions and we store the values in an array.
const directions = [];
for (const [i, resize] of resizeVal.entries()) {
const props = {};
let current = 0;
const cssProperty = resize[2];
const cssPropertyValue = parseInt(self.$target.css(cssProperty));
_.each(resize[0], function (val, key) {
if (self.$target.hasClass(val)) {
current = key;
} else if (resize[1][key] === cssPropertyValue) {
current = key;
}
});
props.resize = resize;
props.current = current;
props.begin = current;
props.beginClass = self.$target.attr('class');
props.regClass = new RegExp('\\s*' + resize[0][current].replace(/[-]*[0-9]+/, '[-]*[0-9]+'), 'g');
props.xy = ev['page' + XY[i]];
props.XY = XY[i];
props.compass = compass[i];
directions.push(props);
}
const cursor = $handle.css('cursor') + '-important';
const $body = $(this.ownerDocument.body);
$body.addClass(cursor);
var xy = ev['page' + XY];
var bodyMouseMove = function (ev) {
const bodyMouseMove = function (ev) {
ev.preventDefault();
var dd = ev['page' + XY] - xy + resize[1][begin];
var next = current + (current + 1 === resize[1].length ? 0 : 1);
var prev = current ? (current - 1) : 0;
let changeTotal = false;
for (const dir of directions) {
// dd is the number of pixels by which the mouse moved, compared to the
// initial position of the handle.
const dd = ev['page' + dir.XY] - dir.xy + dir.resize[1][dir.begin];
const next = dir.current + (dir.current + 1 === dir.resize[1].length ? 0 : 1);
const prev = dir.current ? (dir.current - 1) : 0;
var change = false;
if (dd > (2 * resize[1][next] + resize[1][current]) / 3) {
self.$target.attr('class', (self.$target.attr('class') || '').replace(regClass, ''));
self.$target.addClass(resize[0][next]);
current = next;
change = true;
}
if (prev !== current && dd < (2 * resize[1][prev] + resize[1][current]) / 3) {
self.$target.attr('class', (self.$target.attr('class') || '').replace(regClass, ''));
self.$target.addClass(resize[0][prev]);
current = prev;
change = true;
let change = false;
// If the mouse moved to the right/down by at least 2/3 of the space between the
// previous and the next steps, the handle is snapped to the next step and the
// class is replaced by the one matching this step.
if (dd > (2 * dir.resize[1][next] + dir.resize[1][dir.current]) / 3) {
self.$target.attr('class', (self.$target.attr('class') || '').replace(dir.regClass, ''));
self.$target.addClass(dir.resize[0][next]);
dir.current = next;
change = true;
}
// Same as above but to the left/up.
if (prev !== dir.current && dd < (2 * dir.resize[1][prev] + dir.resize[1][dir.current]) / 3) {
self.$target.attr('class', (self.$target.attr('class') || '').replace(dir.regClass, ''));
self.$target.addClass(dir.resize[0][prev]);
dir.current = prev;
change = true;
}
if (change) {
self._onResize(dir.compass, dir.beginClass, dir.current);
}
changeTotal = changeTotal || change;
}
if (change) {
self._onResize(compass, beginClass, current);
if (changeTotal) {
self.trigger_up('cover_update');
$handle.addClass('o_active');
}
};
var bodyMouseUp = function () {
const bodyMouseUp = function () {
$body.off('mousemove', bodyMouseMove);
$body.off('mouseup', bodyMouseUp);
$body.removeClass(cursor);
$handle.removeClass('o_active');
// If we are in grid mode, removes the background grid.
// Also sync the col-* class with the g-col-* class so the toggle to normal mode
// and the mobile view are well done.
if (rowEl.classList.contains('o_grid_mode')) {
self.options.wysiwyg.odooEditor.observerUnactive('displayBackgroundGrid');
backgroundGridEl.remove();
self.options.wysiwyg.odooEditor.observerActive('displayBackgroundGrid');
gridUtils._resizeGrid(rowEl);
gridUtils._setElementToMaxZindex(self.$target[0], rowEl);
const colClass = [...self.$target[0].classList].find(c => /^col-/.test(c));
const gColClass = [...self.$target[0].classList].find(c => /^g-col-/.test(c));
self.$target[0].classList.remove(colClass);
self.$target[0].classList.add(gColClass.substring(2));
}
// Highlights the previews for a while
var $handlers = self.$overlay.find('.o_handle');
const $handlers = self.$overlay.find('.o_handle');
$handlers.addClass('o_active').delay(300).queue(function () {
$handlers.removeClass('o_active').dequeue();
});
if (begin === current) {
if (directions.every(dir => dir.begin === dir.current)) {
return;
}
setTimeout(function () {
self.options.wysiwyg.odooEditor.historyStep();
}, 0);
@@ -4292,6 +4387,9 @@ registry.sizing = SnippetOptionWidget.extend({
_.each(resizeValues, (value, key) => {
this.$handles.filter('.' + key).toggleClass('readonly', !value);
});
if (this.$target[0].classList.contains('o_grid_item')) {
this.$handles.filter('.o_grid_handle').toggleClass('readonly', false);
}
return def;
},
@@ -4316,6 +4414,45 @@ registry.sizing = SnippetOptionWidget.extend({
// updateUI is called when the target is changed
this._onResize();
},
/**
* @override
*/
async updateUIVisibility() {
await this._super(...arguments);
const isMobileView = this.$target[0].ownerDocument.defaultView.frameElement.clientWidth < 768;
const isGrid = this.$target[0].classList.contains('o_grid_item');
if (this.$target[0].parentNode.classList.contains('row')) {
// Hiding/showing the correct resize handles if we are in grid mode or not.
for (const handle of this.$handles) {
const isGridHandle = handle.classList.contains('o_grid_handle');
handle.classList.toggle('d-none', isGrid ^ isGridHandle);
// Disabling the resize if we are in mobile view.
handle.classList.toggle('readonly', isMobileView && isGridHandle);
}
// Hiding the move handle for some snippets so we can't drag them and
// so we can't toggle grid mode.
const moveHandle = this.$overlay[0].querySelector('.o_move_handle');
const untoggleableColumns = '.s_masonry_block, .s_showcase, .s_features_grid, .s_website_form, .s_color_blocks_2';
const disableToggle = this.$target[0].closest(untoggleableColumns);
moveHandle.classList.toggle('d-none', disableToggle || isMobileView);
// Hiding/showing the arrows.
if (isGrid) {
const moveLeftArrow = this.$overlay[0].querySelector('.fa-angle-left');
const moveRightArrow = this.$overlay[0].querySelector('.fa-angle-right');
const showLeft = await this._computeWidgetVisibility('move_left_opt');
const showRight = await this._computeWidgetVisibility('move_right_opt');
moveLeftArrow.classList.toggle('d-none', !showLeft);
moveRightArrow.classList.toggle('d-none', !showRight);
}
// Show/hide the buttons to send back/front a grid item.
const bringFrontBack = this.$overlay[0].querySelectorAll('.o_front_back');
bringFrontBack.forEach(button => button.classList.toggle('d-none', !isGrid || isMobileView));
}
},
//--------------------------------------------------------------------------
// Private
@@ -4505,6 +4642,108 @@ registry['sizing_x'] = registry.sizing.extend({
},
});
/**
* Handles the sizing in grid mode: edition of grid-{column|row}-{start|end}.
*/
registry['sizing_grid'] = registry.sizing.extend({
/**
* @override
*/
_getSize() {
const rowEl = this.$target.closest('.row')[0];
const gridProp = gridUtils._getGridProperties(rowEl);
const rowStart = this.$target[0].style.gridRowStart;
const rowEnd = parseInt(this.$target[0].style.gridRowEnd);
const columnStart = this.$target[0].style.gridColumnStart;
const columnEnd = this.$target[0].style.gridColumnEnd;
const gridN = [];
const gridS = [];
for (let i = 1; i < rowEnd + 12; i++) {
gridN.push(i);
gridS.push(i + 1);
}
const gridW = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
const gridE = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
this.grid = {
n: [_.map(gridN, v => ('g-height-' + (rowEnd - v))), _.map(gridN, v => ((gridProp.rowSize + gridProp.rowGap) * (v - 1))), 'grid-row-start'],
s: [_.map(gridS, v => ('g-height-' + (v - rowStart))), _.map(gridS, v => ((gridProp.rowSize + gridProp.rowGap) * (v - 1))), 'grid-row-end'],
w: [_.map(gridW, v => ('g-col-lg-' + (columnEnd - v))), _.map(gridW, v => ((gridProp.columnSize + gridProp.columnGap) * (v - 1))), 'grid-column-start'],
e: [_.map(gridE, v => ('g-col-lg-' + (v - columnStart))), _.map(gridE, v => ((gridProp.columnSize + gridProp.columnGap) * (v - 1))), 'grid-column-end'],
};
return this.grid;
},
/**
* @override
*/
_onResize(compass, beginClass, current) {
if (compass === 'n') {
const rowEnd = parseInt(this.$target[0].style.gridRowEnd);
if (current < 0) {
this.$target[0].style.gridRowStart = 1;
} else if (current + 1 >= rowEnd) {
this.$target[0].style.gridRowStart = rowEnd - 1;
} else {
this.$target[0].style.gridRowStart = current + 1;
}
} else if (compass === 's') {
const rowStart = parseInt(this.$target[0].style.gridRowStart);
const rowEnd = parseInt(this.$target[0].style.gridRowEnd);
if (current + 2 <= rowStart) {
this.$target[0].style.gridRowEnd = rowStart + 1;
} else {
this.$target[0].style.gridRowEnd = current + 2;
}
// Updating the grid height.
const rowEl = this.$target[0].parentNode;
const rowCount = parseInt(rowEl.dataset.rowCount);
const backgroundGridEl = rowEl.querySelector('.o_we_background_grid');
const backgroundGridRowEnd = parseInt(backgroundGridEl.style.gridRowEnd);
let rowMove = 0;
if (this.$target[0].style.gridRowEnd > rowEnd && this.$target[0].style.gridRowEnd > rowCount + 1) {
rowMove = this.$target[0].style.gridRowEnd - rowEnd;
} else if (this.$target[0].style.gridRowEnd < rowEnd && this.$target[0].style.gridRowEnd >= rowCount + 1) {
rowMove = this.$target[0].style.gridRowEnd - rowEnd;
}
backgroundGridEl.style.gridRowEnd = backgroundGridRowEnd + rowMove;
} else if (compass === 'w') {
const columnEnd = parseInt(this.$target[0].style.gridColumnEnd);
if (current < 0) {
this.$target[0].style.gridColumnStart = 1;
} else if (current + 1 >= columnEnd) {
this.$target[0].style.gridColumnStart = columnEnd - 1;
} else {
this.$target[0].style.gridColumnStart = current + 1;
}
} else if (compass === 'e') {
const columnStart = parseInt(this.$target[0].style.gridColumnStart);
if (current + 2 > 13) {
this.$target[0].style.gridColumnEnd = 13;
} else if (current + 2 <= columnStart) {
this.$target[0].style.gridColumnEnd = columnStart + 1;
} else {
this.$target[0].style.gridColumnEnd = current + 2;
}
}
if (compass === 'n' || compass === 's') {
const numberRows = this.$target[0].style.gridRowEnd - this.$target[0].style.gridRowStart;
this.$target.attr('class', this.$target.attr('class').replace(/\s*(g-height-)([0-9-]+)/g, ''));
this.$target.addClass('g-height-' + numberRows);
}
if (compass === 'w' || compass === 'e') {
const numberColumns = this.$target[0].style.gridColumnEnd - this.$target[0].style.gridColumnStart;
this.$target.attr('class', this.$target.attr('class').replace(/\s*(g-col-lg-)([0-9-]+)/g, ''));
this.$target.addClass('g-col-lg-' + numberColumns);
}
},
});
/**
* Controls box properties.
*/
@@ -4664,6 +4903,112 @@ registry.layout_column = SnippetOptionWidget.extend({
name: 'change_columns',
});
},
/**
* Changes the layout (columns or grid).
*
* @see this.selectClass for parameters
*/
async selectLayout(previewMode, widgetValue, params) {
if (widgetValue === "grid") {
const rowEl = this.$target[0].querySelector('.row');
if (!rowEl || !rowEl.classList.contains('o_grid_mode')) { // Prevent toggling grid mode twice.
gridUtils._toggleGridMode(this.$target[0]);
this.trigger_up('activate_snippet', {$snippet: this.$target});
}
} else {
// Toggle normal mode only if grid mode was activated (as it's in normal mode by default).
const rowEl = this.$target[0].querySelector('.row');
if (rowEl && rowEl.classList.contains('o_grid_mode')) {
this._toggleNormalMode(rowEl);
this.trigger_up('activate_snippet', {$snippet: this.$target});
}
}
this.trigger_up('option_update', {
optionName: 'StepsConnector',
name: 'change_columns',
});
},
/**
* Adds an image, some text or a button in the grid.
*
* @see this.selectClass for parameters
*/
async addElement(previewMode, widgetValue, params) {
const rowEl = this.$target[0].querySelector('.row');
const elementType = widgetValue;
// If it has been less than 15 seconds that we have added an element, shift the new element
// right and down by one cell. Otherwise, put it on the top left corner.
const currentTime = new Date().getTime();
if (this.lastAddTime && (currentTime - this.lastAddTime) / 1000 < 15) {
this.lastStartPosition = [this.lastStartPosition[0] + 1, this.lastStartPosition[1] + 1];
} else {
this.lastStartPosition = [1, 1]; // [rowStart, columnStart]
}
this.lastAddTime = currentTime;
// Create the new column.
const newColumn = document.createElement('div');
newColumn.classList.add('o_grid_item');
let numberColumns, numberRows;
if (elementType === 'image') {
// Set the columns properties.
newColumn.classList.add('col-lg-6', 'g-col-lg-6', 'g-height-6');
numberColumns = 6;
numberRows = 6;
// Create a default image and add it to the new column.
const img = document.createElement('img');
img.classList.add('img', 'img-fluid', 'mx-auto');
img.src = '/web/image/website.s_text_image_default_image';
img.alt = '';
img.loading = 'lazy';
newColumn.appendChild(img);
} else if (elementType === 'text') {
newColumn.classList.add('col-lg-4', 'g-col-lg-4', 'g-height-2');
numberColumns = 4;
numberRows = 2;
// Create default text content.
const p = document.createElement('p');
p.classList.add('o_default_snippet_text');
p.textContent = _t("Write something...");
newColumn.appendChild(p);
} else if (elementType === 'button') {
newColumn.classList.add('col-lg-2', 'g-col-lg-2', 'g-height-1');
numberColumns = 2;
numberRows = 1;
// Create default button.
const a = document.createElement('a');
a.href = '#';
a.classList.add('mb-2', 'btn', 'btn-primary');
a.textContent = "Button";
newColumn.appendChild(a);
}
// Place the column in the grid.
const rowStart = this.lastStartPosition[0];
let columnStart = this.lastStartPosition[1];
if (columnStart + numberColumns > 13) {
columnStart = 1;
this.lastStartPosition[1] = columnStart;
}
newColumn.style.gridArea = `${rowStart} / ${columnStart} / ${rowStart + numberRows} / ${columnStart + numberColumns}`;
// Setting the z-index to the maximum of the grid.
gridUtils._setElementToMaxZindex(newColumn, rowEl);
// Add the new column and update the grid height.
rowEl.appendChild(newColumn);
gridUtils._resizeGrid(rowEl);
this.trigger_up('activate_snippet', {$snippet: $(newColumn)});
},
//--------------------------------------------------------------------------
// Private
@@ -4675,6 +5020,14 @@ registry.layout_column = SnippetOptionWidget.extend({
_computeWidgetState: function (methodName, params) {
if (methodName === 'selectCount') {
return this.$('> .row').children().length;
} else if (methodName === 'selectLayout') {
const rowEl = this.$target[0].querySelector('.row');
if (rowEl && rowEl.classList.contains('o_grid_mode')) {
return "grid";
} else {
return 'normal';
}
}
return this._super(...arguments);
},
@@ -4730,6 +5083,33 @@ registry.layout_column = SnippetOptionWidget.extend({
$columns.first().addClass('offset-lg-' + colOffset);
}
},
/**
* Toggles the normal mode.
*
* @private
* @param {Element} rowEl
*/
_toggleNormalMode(rowEl) {
// Removing the grid class
rowEl.classList.remove('o_grid_mode');
const columns = rowEl.children;
for (const column of columns) {
// Reload the images.
gridUtils._reloadLazyImages(column);
// Removing the grid properties.
column.classList.remove('o_grid_item');
column.style.removeProperty('grid-area');
column.style.removeProperty('z-index');
}
// Removing the grid properties.
delete rowEl.dataset.rowCount;
// Adding back an align-items-* class.
rowEl.classList.add('align-items-start');
},
});
/**
@@ -4744,8 +5124,9 @@ registry.SnippetMove = SnippetOptionWidget.extend({
start: function () {
var $buttons = this.$el.find('we-button');
var $overlayArea = this.$overlay.find('.o_overlay_move_options');
// Putting the arrows side by side.
$overlayArea.prepend($buttons[1]);
$overlayArea.prepend($buttons[0]);
$overlayArea.append($buttons[1]);
return this._super(...arguments);
},
@@ -44,3 +44,36 @@
transform: scale(-1);
}
// GRID LAYOUT
.o_grid_mode {
@include media-breakpoint-up(md) {
display: grid !important;
grid-auto-rows: 50px;
grid-template-columns: repeat(12, 1fr);
row-gap: 0px;
column-gap: 0px;
}
--gutter-x: 0px;
--grid-item-padding-y: 10px;
--grid-item-padding-x: 10px;
> * {
width: 100%;
margin: 0;
padding: var(--grid-item-padding-y) var(--grid-item-padding-x);
}
}
.container-fluid > .o_grid_mode {
--gutter-x: 30px;
}
.o_grid_item {
word-break: break-word;
> img {
width: 100%;
height: 100%;
object-fit: cover !important;
}
}
@@ -1949,11 +1949,16 @@ body.editor_enable.editor_has_snippets {
position: relative;
z-index: $o-we-overlay-zindex;
width: 100%;
border: 2px dashed $o-we-border-color;
}
}
.oe_drop_zone:not(.oe_grid_zone) {
&.oe_insert {
min-width: $o-we-dropzone-size;
height: $o-we-dropzone-size;
min-height: $o-we-dropzone-size;
margin: (-$o-we-dropzone-size/2) 0;
border: 2px dashed $o-we-border-color;
&.oe_vertical {
width: $o-we-dropzone-size;
@@ -2015,10 +2020,18 @@ body.editor_enable.editor_has_snippets {
border-right-width: 0;
cursor: e-resize;
&.o_grid_handle {
cursor: ew-resize;
}
&:after {
@include o-position-absolute($top: 50%, $left: 40%);
margin-top: -$o-we-handles-btn-size/2;
}
&.o_grid_handle:after {
display: none;
}
}
&.e {
@include o-position-absolute($o-we-handles-offset-to-hide, 0, -$o-we-handles-offset-to-hide, auto);
@@ -2026,10 +2039,18 @@ body.editor_enable.editor_has_snippets {
border-right-width: $o-we-handle-border-width;
cursor: w-resize;
&.o_grid_handle {
cursor: ew-resize;
}
&:after {
@include o-position-absolute($top: 50%, $right: 40%);
margin-top: -$o-we-handles-btn-size/2;
}
&.o_grid_handle:after{
display: none;
}
}
&.n {
@include o-position-absolute($o-we-handles-offset-to-hide, 0, auto, 0);
@@ -2041,6 +2062,10 @@ body.editor_enable.editor_has_snippets {
@include o-position-absolute($left: 50%, $top: 40%);
margin-left: -$o-we-handles-btn-size/2;
}
&.o_grid_handle:after{
display: none;
}
}
&.s {
@include o-position-absolute(auto, 0, -$o-we-handles-offset-to-hide, 0);
@@ -2052,6 +2077,53 @@ body.editor_enable.editor_has_snippets {
@include o-position-absolute($left: 50%, $bottom: 40%);
margin-left: -$o-we-handles-btn-size/2;
}
&.o_grid_handle:after {
display: none;
}
}
&.ne {
@include o-position-absolute($o-we-handles-offset-to-hide + $o-we-handle-border-width, $o-we-handle-border-width, auto, auto);
height: $o-we-handle-edge-size;
cursor: nesw-resize;
}
&.se {
@include o-position-absolute(auto, $o-we-handle-border-width, -$o-we-handles-offset-to-hide - $o-we-handle-border-width, auto);
height: $o-we-handle-edge-size;
cursor: nwse-resize;
&:after {
@include o-position-absolute($top: 50%, $right: 40%);
}
}
&.sw {
@include o-position-absolute(auto, auto, -$o-we-handles-offset-to-hide - $o-we-handle-border-width/2, $o-we-handle-border-width);
height: $o-we-handle-edge-size;
cursor: nesw-resize;
&:after {
@include o-position-absolute($left: 40%, $bottom: 40%);
}
}
&.nw {
@include o-position-absolute($o-we-handles-offset-to-hide - $o-we-handle-border-width, auto, auto, $o-we-handle-border-width + 1);
height: $o-we-handle-edge-size;
cursor: nwse-resize;
&:after {
@include o-position-absolute($left: 50%, $bottom: 40%);
}
}
&.ne,
&.sw {
content: "\f065";
}
&.nw,
&.se {
content: "\f065";
transform: rotate(90deg);
}
&::after {
@@ -2092,6 +2164,17 @@ body.editor_enable.editor_has_snippets {
content: "\f07d";
}
&.ne:after,
&.sw:after {
content: "\f065";
}
&.nw:after,
&.se:after {
content: "\f065";
transform: rotate(180deg);
}
&.o_handle_start {
&.w:after,
@@ -2159,6 +2242,20 @@ body.editor_enable.editor_has_snippets {
background-position: center;
background-repeat: no-repeat;
}
> .o_overlay_move_options > .o_send_back {
width: 30px;
height: 22px;
background-image: url('/web_editor/static/src/img/snippets_options/bring-backward.svg');
background-position: center;
background-repeat: no-repeat;
}
> .o_overlay_move_options > .o_bring_front {
width: 30px;
height: 22px;
background-image: url('/web_editor/static/src/img/snippets_options/bring-forward.svg');
background-position: center;
background-repeat: no-repeat;
}
&:hover {
.o_overlay_move_options > *, .o_overlay_edit_options > *, > we-button {
@include o-hover-opacity(.6);
@@ -2416,3 +2513,35 @@ we-button:hover .o_we_shape_animated_label > span {
// also not work to override important inline style... this is a limitation.
transition: none !important;
}
// GRID LAYOUT
we-button.o_grid {
min-width: fit-content;
padding-left: 3px !important;
padding-right: 3px !important;
}
we-select.o_grid we-toggler {
width: fit-content !important;
}
we-button-group.o_grid {
min-width: fit-content !important;
}
// Background grid.
.o_we_background_grid {
padding: 0;
}
.o_we_cell {
fill: $o-brand-odoo;
fill-opacity: .2;
stroke: $o-we-border-color;
stroke-width: 1px;
}
.o_we_drag_helper {
padding: 0;
border: $o-we-handle-border-width solid $o-we-accent;
}
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<templates id="template" xml:space="preserve">
<t t-name="web_editor.background_grid">
<div t-attf-style="grid-area: 1 / 1 / #{rowCount} / -1;" class="o_we_background_grid">
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%">
<defs>
<pattern id="cell" t-attf-width="#{columnSize + columnGap}px" t-attf-height="#{rowSize + rowGap}px" patternUnits="userSpaceOnUse">
<rect class="o_we_cell" x="0" y="0" t-attf-width="#{columnSize}px" t-attf-height="#{rowSize}px"></rect>
</pattern>
</defs>
<rect fill="url(#cell)" width="100%" height="100%"></rect>
</svg>
</div>
</t>
</templates>
@@ -9,10 +9,22 @@
<div class="o_handle w readonly"/>
<div class="o_handle s readonly"/>
<!-- Grid resize handles -->
<div class="o_handle o_grid_handle n d-none"/>
<div class="o_handle o_grid_handle e d-none"/>
<div class="o_handle o_grid_handle w d-none"/>
<div class="o_handle o_grid_handle s d-none"/>
<div class="o_handle o_grid_handle ne d-none"/>
<div class="o_handle o_grid_handle nw d-none"/>
<div class="o_handle o_grid_handle se d-none"/>
<div class="o_handle o_grid_handle sw d-none"/>
<div class="o_overlay_options_wrap">
<!-- Overlay move specific section -->
<div class="o_overlay_move_options">
<!-- Button-like handle to drag and drop -->
<div class="o_front_back o_send_back d-none"/>
<div class="o_front_back o_bring_front d-none"/>
<div class="o_move_handle"/>
</div>
<div class="o_overlay_edit_options">
+55 -21
View File
@@ -466,9 +466,8 @@
</div>
<div data-js="layout_column"
data-selector="section"
data-target="> *:has(> .row:not(.s_nb_column_fixed)), > .s_allow_columns"
data-exclude=".s_masonry_block">
data-selector="section.s_features_grid, section.s_color_blocks_2"
data-target="> *:has(> .row:not(.s_nb_column_fixed)), > .s_allow_columns">
<we-select string="Columns" data-no-preview="true">
<we-button data-select-count="0" data-name="zero_cols_opt">None</we-button>
<we-button data-select-count="1">1</we-button>
@@ -480,6 +479,54 @@
</we-select>
</div>
<div data-js="layout_column"
data-selector="section"
data-target="> *:has(> .row:not(.s_nb_column_fixed)), > .s_allow_columns"
data-exclude=".s_masonry_block, .s_features_grid, .s_color_blocks_2">
<we-row>
<we-button-group string="Layout" data-no-preview="true">
<we-button data-select-layout="grid" data-name="grid_mode">Grid</we-button>
<we-button data-select-layout="normal" data-name="normal_mode">Cols</we-button>
</we-button-group>
<we-select class="o_grid" data-no-preview="true" data-dependencies="normal_mode">
<we-button data-select-count="0" data-name="zero_cols_opt">None</we-button>
<we-button data-select-count="1">1</we-button>
<we-button data-select-count="2">2</we-button>
<we-button data-select-count="3">3</we-button>
<we-button data-select-count="4">4</we-button>
<we-button data-select-count="5">5</we-button>
<we-button data-select-count="6">6</we-button>
</we-select>
</we-row>
<we-button-group class="o_grid o_we_sublevel_1" string="Add Elements" data-no-preview="true" data-dependencies="grid_mode">
<we-button class="o_we_bg_brand_primary o_grid" data-add-element="image" title="Image" data-name="image">Image</we-button>
<we-button class="o_we_bg_brand_primary o_grid" data-add-element="text" title="Text" data-name="text">Text</we-button>
<we-button class="o_we_bg_brand_primary o_grid" data-add-element="button" title="Button" data-name="button">Button</we-button>
</we-button-group>
<we-row class="o_we_sublevel_1" string="Padding (Y, X)">
<we-input data-dependencies="grid_mode" data-select-style="" data-css-property="--grid-item-padding-y" data-unit="px" data-apply-to=".row"/>
<we-input data-dependencies="grid_mode" data-select-style="" data-css-property="--grid-item-padding-x" data-unit="px" data-apply-to=".row"/>
</we-row>
</div>
<!-- Vertical Alignment -->
<div data-option-name="vAlignment" id="row_valign_snippet_option" data-selector=".s_text_image, .s_image_text, .s_three_columns" data-target=".row">
<we-button-group class="o_we_sublevel_1" string="Vert. Alignment" title="Vertical Alignment" data-dependencies="normal_mode">
<we-button title="Align Top"
data-select-class="align-items-start"
data-img="/website/static/src/img/snippets_options/align_top.svg"/>
<we-button title="Align Middle"
data-select-class="align-items-center"
data-img="/website/static/src/img/snippets_options/align_middle.svg"/>
<we-button title="Align Bottom"
data-select-class="align-items-end"
data-img="/website/static/src/img/snippets_options/align_bottom.svg"/>
<we-button title="Stretch to Equal Height"
data-select-class="align-items-stretch"
data-img="/website/static/src/img/snippets_options/align_stretch.svg"/>
</we-button-group>
</div>
<!-- Move snippets around -->
<div data-js="SnippetMove" data-selector="section, .accordion > .card, .s_showcase .row:not(.s_col_no_resize) > div" data-no-scroll=".accordion > .card">
<we-button class="fa fa-fw fa-angle-up" data-move-snippet="prev" data-no-preview="true" data-name="move_up_opt"/>
@@ -558,6 +605,11 @@
data-drop-near=".row:not(.s_col_no_resize) > div"
data-exclude=".s_col_no_resize.row > div, .s_col_no_resize"/>
<div data-js="sizing_grid"
data-selector=".row > div"
data-drop-near=".row.o_grid_mode > div"
data-exclude=".s_col_no_resize.row > div, .s_col_no_resize"/>
<t t-set="so_snippet_addition_selector" t-translation="off">section, .parallax</t>
<div id="so_snippet_addition"
t-att-data-selector="so_snippet_addition_selector"
@@ -1150,24 +1202,6 @@
</we-select>
</div>
<!-- Vertical Alignment -->
<div data-option-name="vAlignment" id="row_valign_snippet_option" data-selector=".s_text_image, .s_image_text, .s_three_columns" data-target=".row">
<we-button-group string="Vert. Alignment" title="Vertical Alignment">
<we-button title="Align Top"
data-select-class="align-items-start"
data-img="/website/static/src/img/snippets_options/align_top.svg"/>
<we-button title="Align Middle"
data-select-class="align-items-center"
data-img="/website/static/src/img/snippets_options/align_middle.svg"/>
<we-button title="Align Bottom"
data-select-class="align-items-end"
data-img="/website/static/src/img/snippets_options/align_bottom.svg"/>
<we-button title="Stretch to Equal Height"
data-select-class="align-items-stretch"
data-img="/website/static/src/img/snippets_options/align_stretch.svg"/>
</we-button-group>
</div>
<div data-js="ConditionalVisibility" data-selector="section">
<t t-set="current_website" t-value="request.env['website'].get_current_website()"/>
<we-collapse>