[REF] web_editor, *: make options in charge of their rendering

* website, website_sale

Also make the "isTopOption" method a property.

This is done in preparation of better code for coupling sub-widgets used
to retrieve user value (button, input, colorpicker, ...) to actual
change in the edited DOM through snippet options methods.

Part of https://github.com/odoo/odoo/pull/40282
In preparation of task-2122935 and other tasks
This commit is contained in:
qsm-odoo
2019-12-01 14:07:07 +00:00
parent 0dd1f4595b
commit 75ab52af9c
5 changed files with 230 additions and 239 deletions
@@ -321,62 +321,6 @@ var SnippetEditor = Widget.extend({
// Private
//--------------------------------------------------------------------------
/**
* Transforms option UI description into actual DOM.
*
* @private
* @param {jQuery} $el
*/
_createOptionUI: function ($el) {
const $optionSection = $(core.qweb.render('web_editor.customize_block_option'));
$optionSection.append($el);
const uiEl = $optionSection[0];
// Build group first as their internal components will be built after
uiEl.querySelectorAll('we-row').forEach(el => {
const infos = this._extraInfoFromDescriptionElement(el);
const groupEl = options.Class.prototype.buildRowElement(infos.title, infos.options);
el.parentNode.insertBefore(groupEl, el);
el.parentNode.removeChild(el);
});
// Build standard components
uiEl.querySelectorAll('we-select').forEach(el => {
const infos = this._extraInfoFromDescriptionElement(el);
const selectEl = options.Class.prototype.buildSelectElement(infos.title, infos.options);
el.parentNode.insertBefore(selectEl, el);
el.parentNode.removeChild(el);
});
uiEl.querySelectorAll('we-checkbox').forEach(el => {
const infos = this._extraInfoFromDescriptionElement(el);
const checkboxEl = options.Class.prototype.buildCheckboxElement(infos.title, infos.options);
el.parentNode.insertBefore(checkboxEl, el);
el.parentNode.removeChild(el);
});
uiEl.querySelectorAll('we-input').forEach(el => {
const infos = this._extraInfoFromDescriptionElement(el);
const inputEl = options.Class.prototype.buildInputElement(infos.title, infos.options);
el.parentNode.insertBefore(inputEl, el);
el.parentNode.removeChild(el);
});
return $optionSection;
},
/**
* @private
* @param {HTMLElement} el
* @returns {Object}
*/
_extraInfoFromDescriptionElement: function (el) {
return {
title: el.getAttribute('string'),
options: {
classes: el.classList,
dataAttributes: el.dataset,
childNodes: [...el.childNodes],
},
};
},
/**
* DOMElements have a default name which appears in the overlay when they
* are being edited. This method retrieves this name; it can be defined
@@ -418,7 +362,8 @@ var SnippetEditor = Widget.extend({
var $optionsSection = $(core.qweb.render('web_editor.customize_block_options_section', {
name: this._getName(),
})).data('editor', this);
$optionsSection.find('we-button-group').contents().each((i, node) => {
const $optionsSectionBtnGroup = $optionsSection.find('we-button-group');
$optionsSectionBtnGroup.contents().each((i, node) => {
if (node.nodeType === Node.TEXT_NODE) {
node.parentNode.removeChild(node);
}
@@ -447,13 +392,15 @@ var SnippetEditor = Widget.extend({
}
var optionName = val.option;
var $ui = val.$el.children().clone(true);
var option = new (options.registry[optionName] || options.Class)(
this,
val.$el.children(),
val.base_target ? this.$target.find(val.base_target).eq(0) : this.$target,
this.$el,
_.extend({snippetName: this._getName()}, val.data),
_.extend({
optionName: optionName,
snippetName: this._getName(),
}, val.data),
this.options
);
var key = optionName || _.uniqueId('option');
@@ -465,8 +412,7 @@ var SnippetEditor = Widget.extend({
}
this.styles[key] = option;
option.__order = i++;
const $option = this._createOptionUI($ui).addClass('snippet-option-' + optionName);
return option.attachTo($option);
return option.appendTo(document.createDocumentFragment());
});
this.isTargetMovable = (this.selectorSiblings.length > 0 || this.selectorChildren.length > 0);
@@ -476,8 +422,8 @@ var SnippetEditor = Widget.extend({
return Promise.all(defs).then(() => {
const options = _.sortBy(this.styles, '__order');
options.forEach(option => {
if (option.isTopOption()) {
$optionsSection.find('we-button-group').first().prepend(option.$el);
if (option.isTopOption) {
$optionsSectionBtnGroup.prepend(option.$el);
} else {
$optionsSection.append(option.$el);
}
@@ -59,6 +59,7 @@ function _computePxByRem(toRem) {
* referenced thanks to the data-js key in the web_editor options template.
*/
var SnippetOption = Widget.extend({
tagName: 'we-customizeblock-option',
events: {
'mouseenter we-button': '_onOptionPreview',
'click we-button': '_onOptionSelection',
@@ -69,6 +70,13 @@ var SnippetOption = Widget.extend({
'click we-input': '_onOptionInputClick',
'keydown we-input input': '_onOptionInputKeydown',
},
/**
* Indicates if the option should be displayed in the button group at the
* top of the options panel, next to the clone/remove button.
*
* @type {boolean}
*/
isTopOption: false,
/**
* The option `$el` is supposed to be the associated DOM UI element.
@@ -78,15 +86,63 @@ var SnippetOption = Widget.extend({
*
* @constructor
*/
init: function (parent, $target, $overlay, data, options) {
init: function (parent, $uiElements, $target, $overlay, data, options) {
this._super.apply(this, arguments);
this.uiFragment = document.createDocumentFragment();
$uiElements.clone(true).appendTo(this.uiFragment);
this.$target = $target;
this.$overlay = $overlay;
this.data = data;
this.options = options;
this.className = 'snippet-option-' + this.data.optionName;
this.ownerDocument = this.$target[0].ownerDocument;
this.__methodNames = [];
},
/**
* @override
*/
willStart: function () {
// Build group first as their internal components will be built after
this.uiFragment.querySelectorAll('we-row').forEach(el => {
const infos = this._extraInfoFromDescriptionElement(el);
const groupEl = this._buildRowElement(infos.title, infos.options);
el.parentNode.insertBefore(groupEl, el);
el.parentNode.removeChild(el);
});
// Build standard components
this.uiFragment.querySelectorAll('we-select').forEach(el => {
const infos = this._extraInfoFromDescriptionElement(el);
const selectEl = this._buildSelectElement(infos.title, infos.options);
el.parentNode.insertBefore(selectEl, el);
el.parentNode.removeChild(el);
});
this.uiFragment.querySelectorAll('we-checkbox').forEach(el => {
const infos = this._extraInfoFromDescriptionElement(el);
const checkboxEl = this._buildCheckboxElement(infos.title, infos.options);
el.parentNode.insertBefore(checkboxEl, el);
el.parentNode.removeChild(el);
});
this.uiFragment.querySelectorAll('we-input').forEach(el => {
const infos = this._extraInfoFromDescriptionElement(el);
const inputEl = this._buildInputElement(infos.title, infos.options);
el.parentNode.insertBefore(inputEl, el);
el.parentNode.removeChild(el);
});
return this._super(...arguments);
},
/**
* @override
*/
renderElement: function () {
this._super(...arguments);
this.el.appendChild(this.uiFragment);
},
/**
* Called when the option is initialized (i.e. the parent edition overlay is
* shown for the first time).
@@ -97,15 +153,6 @@ var SnippetOption = Widget.extend({
this._updateUI();
return this._super.apply(this, arguments);
},
/**
* Indicates if the option should be displayed in the button group at the
* top of the options panel, next to the clone/remove button.
*
* @returns {boolean}
*/
isTopOption: function () {
return false;
},
/**
* Called when the parent edition overlay is covering the associated snippet
* (the first time, this follows the call to the @see start method).
@@ -382,6 +429,148 @@ var SnippetOption = Widget.extend({
// Private
//--------------------------------------------------------------------------
/**
* Build the correct DOM for an element according to the given options.
*
* @private
* @param {string} tagName
* @param {string} [title]
* @param {Object} [options]
* @param {string[]} [options.classes]
* @param {Object} [options.dataAttributes]
* @returns {HTMLElement}
*/
_buildElement: function (tagName, title, options) {
const el = document.createElement(tagName);
if (title) {
const titleEl = this._buildTitleElement(title);
el.appendChild(titleEl);
}
if (options && options.classes) {
el.classList.add(...options.classes);
}
if (options && options.dataAttributes) {
for (const key in options.dataAttributes) {
el.dataset[key] = options.dataAttributes[key];
}
}
return el;
},
/**
* Build the correct DOM for a we-checkbox element.
*
* @private
* @param {string} [title]
* @param {Object} [options] - @see this._buildElement
* @returns {HTMLElement}
*/
_buildCheckboxElement: function (title, options) {
const buttonEl = this._buildElement('we-button', title, options);
buttonEl.classList.add('o_we_checkbox_wrapper');
const checkboxEl = document.createElement('we-checkbox');
buttonEl.appendChild(checkboxEl);
return buttonEl;
},
/**
* Build the correct DOM for a we-row element.
*
* @private
* @param {string} [title]
* @param {Object} [options] - @see this._buildElement
* @param {HTMLElement[]} [options.childNodes]
* @returns {HTMLElement}
*/
_buildRowElement: function (title, options) {
const groupEl = this._buildElement('we-row', title, options);
const rowEl = document.createElement('div');
groupEl.appendChild(rowEl);
if (options && options.childNodes) {
options.childNodes.forEach(node => rowEl.appendChild(node));
}
return groupEl;
},
/**
* Build the correct DOM for a we-input element.
*
* @private
* @param {string} [title]
* @param {Object} [options] - @see this._buildElement
*/
_buildInputElement: function (title, options) {
const inputWrapperEl = this._buildElement('we-input', title, options);
var unit = inputWrapperEl.dataset.unit;
if (unit === undefined) {
unit = 'px';
}
inputWrapperEl.dataset.unit = unit;
if (inputWrapperEl.dataset.saveUnit === undefined) {
inputWrapperEl.dataset.saveUnit = unit;
}
var defaultValue = inputWrapperEl.dataset.defaultValue;
if (defaultValue === undefined) {
defaultValue = ('0' + unit);
}
inputWrapperEl.dataset.defaultValue = defaultValue;
var inputEl = document.createElement('input');
inputEl.setAttribute('type', 'text');
inputEl.setAttribute('placeholder', defaultValue.replace(unit, ''));
inputWrapperEl.appendChild(inputEl);
var unitEl = document.createElement('span');
unitEl.textContent = unit;
inputWrapperEl.appendChild(unitEl);
return inputWrapperEl;
},
/**
* Build the correct DOM for a we-select element.
*
* @private
* @param {string} [title]
* @param {Object} [options] - @see this._buildElement
* @param {HTMLElement[]} [options.childNodes]
* @param {HTMLElement} [options.valueEl]
* @returns {HTMLElement}
*/
_buildSelectElement: function (title, options) {
const selectEl = this._buildElement('we-select', title, options);
if (options && options.valueEl) {
selectEl.appendChild(options.valueEl);
}
const menuTogglerEl = document.createElement('we-toggler');
selectEl.appendChild(menuTogglerEl);
var menuEl = document.createElement('we-select-menu');
if (options && options.childNodes) {
options.childNodes.forEach(node => menuEl.appendChild(node));
}
selectEl.appendChild(menuEl);
return selectEl;
},
/**
* @private
* @param {string} title
* @returns {HTMLElement}
*/
_buildTitleElement: function (title) {
const titleEl = document.createElement('we-title');
titleEl.textContent = title;
return titleEl;
},
/**
* Converts the given (value + unit) string to a numeric value expressed in
* the given css unit.
@@ -424,6 +613,21 @@ var SnippetOption = Widget.extend({
}
return value * converter(cssProp, this.$target);
},
/**
* @statis
* @param {HTMLElement} el
* @returns {Object}
*/
_extraInfoFromDescriptionElement: function (el) {
return {
title: el.getAttribute('string'),
options: {
classes: el.classList,
dataAttributes: el.dataset,
childNodes: [...el.childNodes],
},
};
},
/**
* Reactivate the options that were activated before previews.
*/
@@ -778,153 +982,6 @@ var SnippetOption = Widget.extend({
}
this._reset();
},
//--------------------------------------------------------------------------
// Static
//--------------------------------------------------------------------------
/**
* Build the correct DOM for an element according to the given options.
*
* @static
* @param {string} tagName
* @param {string} [title]
* @param {Object} [options]
* @param {string[]} [options.classes]
* @param {Object} [options.dataAttributes]
* @returns {HTMLElement}
*/
buildElement: function (tagName, title, options) {
const el = document.createElement(tagName);
if (title) {
const titleEl = SnippetOption.prototype.buildTitleElement(title);
el.appendChild(titleEl);
}
if (options && options.classes) {
el.classList.add(...options.classes);
}
if (options && options.dataAttributes) {
for (const key in options.dataAttributes) {
el.dataset[key] = options.dataAttributes[key];
}
}
return el;
},
/**
* Build the correct DOM for a we-checkbox element.
*
* @static
* @param {string} [title]
* @param {Object} [options] - @see this.buildElement
* @returns {HTMLElement}
*/
buildCheckboxElement: function (title, options) {
const buttonEl = SnippetOption.prototype.buildElement('we-button', title, options);
buttonEl.classList.add('o_we_checkbox_wrapper');
const checkboxEl = document.createElement('we-checkbox');
buttonEl.appendChild(checkboxEl);
return buttonEl;
},
/**
* Build the correct DOM for a we-row element.
*
* @static
* @param {string} [title]
* @param {Object} [options] - @see this.buildElement
* @param {HTMLElement[]} [options.childNodes]
* @returns {HTMLElement}
*/
buildRowElement: function (title, options) {
const groupEl = SnippetOption.prototype.buildElement('we-row', title, options);
const rowEl = document.createElement('div');
groupEl.appendChild(rowEl);
if (options && options.childNodes) {
options.childNodes.forEach(node => rowEl.appendChild(node));
}
return groupEl;
},
/**
* Build the correct DOM for a we-input element.
*
* @static
* @param {string} [title]
* @param {Object} [options] - @see this.buildElement
*/
buildInputElement: function (title, options) {
const inputWrapperEl = SnippetOption.prototype.buildElement('we-input', title, options);
var unit = inputWrapperEl.dataset.unit;
if (unit === undefined) {
unit = 'px';
}
inputWrapperEl.dataset.unit = unit;
if (inputWrapperEl.dataset.saveUnit === undefined) {
inputWrapperEl.dataset.saveUnit = unit;
}
var defaultValue = inputWrapperEl.dataset.defaultValue;
if (defaultValue === undefined) {
defaultValue = ('0' + unit);
}
inputWrapperEl.dataset.defaultValue = defaultValue;
var inputEl = document.createElement('input');
inputEl.setAttribute('type', 'text');
inputEl.setAttribute('placeholder', defaultValue.replace(unit, ''));
inputWrapperEl.appendChild(inputEl);
var unitEl = document.createElement('span');
unitEl.textContent = unit;
inputWrapperEl.appendChild(unitEl);
return inputWrapperEl;
},
/**
* Build the correct DOM for a we-select element.
*
* @static
* @param {string} [title]
* @param {Object} [options] - @see this.buildElement
* @param {HTMLElement[]} [options.childNodes]
* @param {HTMLElement} [options.valueEl]
* @returns {HTMLElement}
*/
buildSelectElement: function (title, options) {
const selectEl = SnippetOption.prototype.buildElement('we-select', title, options);
if (options && options.valueEl) {
selectEl.appendChild(options.valueEl);
}
const menuTogglerEl = document.createElement('we-toggler');
selectEl.appendChild(menuTogglerEl);
var menuEl = document.createElement('we-select-menu');
if (options && options.childNodes) {
options.childNodes.forEach(node => menuEl.appendChild(node));
}
selectEl.appendChild(menuEl);
return selectEl;
},
/**
* @static
* @param {string} title
* @returns {HTMLElement}
*/
buildTitleElement: function (title) {
const titleEl = document.createElement('we-title');
titleEl.textContent = title;
return titleEl;
},
});
//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
@@ -1219,7 +1276,7 @@ registry.colorpicker = SnippetOption.extend({
// Build the select element with a custom span to hold the color preview
this.colorPreviewEl = document.createElement('span');
const selectEl = this.buildSelectElement(this.data.string, {
const selectEl = this._buildSelectElement(this.data.string, {
classes: ['o_we_so_color_palette'],
childNodes: [this.colorPalette.el],
valueEl: this.colorPreviewEl,
@@ -1343,7 +1400,7 @@ registry.background = SnippetOption.extend({
this.removeBgEl.dataset.background = '';
this.removeBgEl.dataset.noPreview = 'true';
this.$el.append(this.buildRowElement(this.data.string, {
this.$el.append(this._buildRowElement(this.data.string, {
childNodes: [editBgEl, this.removeBgEl],
}));
@@ -31,9 +31,6 @@
</we-title>
</we-customizeblock-options>
</t>
<t t-name="web_editor.customize_block_option">
<we-customizeblock-option/>
</t>
<!-- options -->
<div t-name="web_editor.snippet.option.colorpicker" class="colorpicker">
@@ -329,6 +329,8 @@ options.registry.Carousel = options.Class.extend({
});
options.registry.CarouselItem = options.Class.extend({
isTopOption: true,
/**
* @override
*/
@@ -353,12 +355,6 @@ options.registry.CarouselItem = options.Class.extend({
this._super(...arguments);
this.$carousel.off('.carousel_item_option');
},
/**
* @override
*/
isTopOption: function () {
return true;
},
/**
* Updates the slide counter.
*
@@ -1597,6 +1593,7 @@ options.registry.topMenuColor = options.registry.colorpicker.extend({
*/
options.registry.anchor = options.Class.extend({
xmlDependencies: ['/website/static/src/xml/website.editor.xml'],
isTopOption: true,
//--------------------------------------------------------------------------
// Public
@@ -1620,12 +1617,6 @@ options.registry.anchor = options.Class.extend({
return this._super.apply(this, arguments);
},
/**
* @override
*/
isTopOption: function () {
return true;
},
/**
* @override
*/
@@ -184,7 +184,7 @@ options.registry.WebsiteSaleProductsItem = options.Class.extend({
}).then(data => {
var $menu = this.$el.find('[name="style"]');
for (var k in data) {
$menu.append(this.buildCheckboxElement(data[k]['name'], {
$menu.append(this._buildCheckboxElement(data[k]['name'], {
dataAttributes: {
'style': data[k]['id'],
'toggleClass': data[k]['html_class'],