[IMP] web_editor, *: automatically update snippet option UI on focus

* website, website_blog, website_sale

Some snippet option already needed to implement this by themself and now
that they use custom widget, this is even more needed. The long-term
goal is that developers should not have to override the onFocus method
or the updateUI method but only the _computeWidgetState method.

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 15:49:57 +00:00
parent fdac73e99d
commit 3be9ae5672
6 changed files with 170 additions and 152 deletions
@@ -319,6 +319,7 @@ var SnippetEditor = Widget.extend({
var styles = _.values(editor.styles);
_.sortBy(styles, '__order').forEach(style => {
if (show) {
style.updateUI();
style.onFocus();
} else {
style.onBlur();
@@ -828,7 +828,7 @@ const MultiUserValueWidget = UserValueWidget.extend({
},
});
const ColorpickerUserValueWidget = SelectUserValueWidget.extend({ // FIXME should be reloaded on focus
const ColorpickerUserValueWidget = SelectUserValueWidget.extend({
custom_events: {
'color_picked': '_onColorPicked',
'color_hover': '_onColorHovered',
@@ -1053,16 +1053,6 @@ const SnippetOptionWidget = Widget.extend({
this.el.appendChild(this.uiFragment);
this.uiFragment = null;
},
/**
* Called when the option is initialized (i.e. the parent edition overlay is
* shown for the first time).
*
* @override
*/
start: async function () {
await this._updateUI();
return this._super.apply(this, arguments);
},
/**
* Called when the parent edition overlay is covering the associated snippet
* (the first time, this follows the call to the @see start method).
@@ -1308,9 +1298,38 @@ const SnippetOptionWidget = Widget.extend({
*/
setTarget: async function ($target) {
this.$target = $target;
await this._updateUI();
await this.updateUI();
this.$target.trigger('snippet-option-change', [this]);
},
/**
* Updates the UI. For widget update, @see _computeWidgetState.
*
* @param {function} [callback] - used to filter widgets to update
* @returns {Promise}
*/
updateUI: async function (callback) {
// For each widget, for each of their option method, notify to the
// widget the current value they should hold according to the $target's
// current state, related for that method.
const proms = this._userValueWidgets.map(async widget => {
if (callback && !callback(widget)) {
return;
}
const methodsNames = widget.getMethodsNames();
const proms = methodsNames.map(async methodName => {
const value = await this._computeWidgetState(methodName, widget.getMethodsParams(methodName));
const normalizedValue = this._normalizeWidgetValue(value);
widget.setValue(normalizedValue, methodName);
});
await Promise.all(proms);
// Refresh the UI of all widgets (after all the current values they
// hold have been updated).
return widget.updateUI();
});
return Promise.all(proms);
},
//--------------------------------------------------------------------------
// Private
@@ -1467,7 +1486,6 @@ const SnippetOptionWidget = Widget.extend({
return uiFragment;
},
/**
* @private
* @param {HTMLElement} parentEl
@@ -1513,7 +1531,7 @@ const SnippetOptionWidget = Widget.extend({
return this._renderOriginalXML($xml).then(uiFragment => {
this.$el.append(uiFragment);
return this._updateUI();
return this.updateUI();
});
},
/**
@@ -1544,42 +1562,13 @@ const SnippetOptionWidget = Widget.extend({
});
if (!previewMode) {
await this._updateUI(w => !w.isPreviewed() || w === widget);
await this.updateUI(w => !w.isPreviewed() || w === widget);
} else {
await this._updateUI(w => w !== widget && !w.isPreviewed());
await this.updateUI(w => w !== widget && !w.isPreviewed());
}
this.$target.trigger('content_changed');
},
/**
* Updates the UI. For widget update, @see _computeWidgetState.
*
* @param {function} [callback] - used to filter widgets to update
* @returns {Promise}
*/
_updateUI: async function (callback) {
// For each widget, for each of their option method, notify to the
// widget the current value they should hold according to the $target's
// current state, related for that method.
const proms = this._userValueWidgets.map(async widget => {
if (callback && !callback(widget)) {
return;
}
const methodsNames = widget.getMethodsNames();
const proms = methodsNames.map(async methodName => {
const value = await this._computeWidgetState(methodName, widget.getMethodsParams(methodName));
const normalizedValue = this._normalizeWidgetValue(value);
widget.setValue(normalizedValue, methodName);
});
await Promise.all(proms);
// Refresh the UI of all widgets (after all the current values they
// hold have been updated).
return widget.updateUI();
});
return Promise.all(proms);
},
//--------------------------------------------------------------------------
// Handlers
@@ -1748,11 +1737,6 @@ registry.sizing = SnippetOptionWidget.extend({
* @override
*/
onFocus: function () {
var resizeValues = this._getSize();
_.each(resizeValues, (value, key) => {
this.$handles.filter('.' + key).toggleClass('readonly', !value);
});
this._onResize();
},
/**
@@ -1773,6 +1757,16 @@ registry.sizing = SnippetOptionWidget.extend({
await this._super(...arguments);
this._onResize();
},
/**
* @override
*/
updateUI: function () {
this._super(...arguments);
const resizeValues = this._getSize();
_.each(resizeValues, (value, key) => {
this.$handles.filter('.' + key).toggleClass('readonly', !value);
});
},
//--------------------------------------------------------------------------
// Private
@@ -1968,6 +1962,20 @@ registry.background = SnippetOptionWidget.extend({
this.bindBackgroundEvents();
this.__customImageSrc = this._getSrcFromCssValue();
},
/**
* @override
*/
updateUI: async function () {
await this._super(...arguments);
var src = this._getSrcFromCssValue();
this.removeBgWidget.el.classList.toggle('d-none', !src);
if (src) {
var split = src.split('/');
this.editBgTextEl.textContent = split[split.length - 1];
} else {
this.editBgTextEl.textContent = this._getDefaultTextContent();
}
},
//--------------------------------------------------------------------------
// Private
@@ -2026,20 +2034,6 @@ registry.background = SnippetOptionWidget.extend({
var srcValueWrapper = /url\(['"]*|['"]*\)|^none$/g;
return value && value.replace(srcValueWrapper, '') || '';
},
/**
* @override
*/
_updateUI: async function () {
await this._super.apply(this, arguments);
var src = this._getSrcFromCssValue();
this.removeBgWidget.el.classList.toggle('d-none', !src);
if (src) {
var split = src.split('/');
this.editBgTextEl.textContent = split[split.length - 1];
} else {
this.editBgTextEl.textContent = this._getDefaultTextContent();
}
},
/**
* @override
*/
@@ -2082,7 +2076,7 @@ registry.background = SnippetOptionWidget.extend({
this.__customImageSrc = value;
this.background(false, this.__customImageSrc, {});
this.$target.toggleClass('oe_custom_bg', !!value);
await this._updateUI();
await this.updateUI();
this.$target.trigger('snippet-option-change', [this]);
},
@@ -2142,7 +2136,7 @@ registry.BackgroundPosition = SnippetOptionWidget.extend({
this.$target.on('snippet-option-change', () => {
// Hides option if the bg image is removed in favor of a bg color
this._updateUI();
this.updateUI();
// this.img is used to compute dragging speed
this.img.src = this._getSrcFromCssValue();
});
@@ -2160,12 +2154,6 @@ registry.BackgroundPosition = SnippetOptionWidget.extend({
$(window).off('.bgposition');
this._super.apply(this, arguments);
},
/**
* @override
*/
onFocus: function () {
this._updateUI();
},
//--------------------------------------------------------------------------
// Options
@@ -2211,6 +2199,21 @@ registry.BackgroundPosition = SnippetOptionWidget.extend({
this._super(...arguments);
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Disables background position if no background image, disables size inputs
* in cover mode.
*
* @override
*/
updateUI: async function () {
await this._super(...arguments);
this.$el.toggleClass('d-none', this.$target.css('background-image') === 'none');
this.$el.find('we-input').toggleClass('d-none', this.$target.css('background-repeat') !== 'repeat');
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
@@ -2317,17 +2320,6 @@ registry.BackgroundPosition = SnippetOptionWidget.extend({
// This is caused by the click event which we are currently handling bubbling up to the document.
window.setTimeout(() => $(document).on('click.bgposition', this._onDocumentClicked.bind(this)), 0);
},
/**
* Disables background position if no background image, disables size inputs
* in cover mode.
*
* @override
*/
_updateUI: async function () {
await this._super.apply(this, arguments);
this.$el.toggleClass('d-none', this.$target.css('background-image') === 'none');
this.$el.find('we-input').toggleClass('d-none', this.$target.css('background-repeat') !== 'repeat');
},
/**
* Returns the src value from a css value related to a background image
* (e.g. "url('blabla')" => "blabla" / "none" => "").
@@ -107,7 +107,7 @@ options.registry.background.include({
delete target.dataset.bgVideoSrc;
}
this._refreshPublicWidgets();
await this._updateUI();
await this.updateUI();
},
/**
* Returns whether the current target has a background video or not.
@@ -350,12 +350,18 @@ options.registry.CarouselItem = options.Class.extend({
this._super(...arguments);
this.$carousel.off('.carousel_item_option');
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Updates the slide counter.
*
* @override
*/
onFocus: function () {
updateUI: function () {
this._super(...arguments);
const $items = this.$carousel.find('.carousel-item');
const $activeSlide = $items.filter('.active');
const updatedText = ` (${$activeSlide.index() + 1}/${$items.length})`;
@@ -483,11 +489,23 @@ options.registry.navTabs = options.Class.extend({
$activeLink.parent().remove();
$activePane.remove();
self._findLinksAndPanes();
self._updateUI(); // TODO forced to do this because we do not return deferred for options
self.updateUI(); // TODO forced to do this because we do not return deferred for options
});
$next.tab('show');
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @override
*/
updateUI: async function () {
await this._super(...arguments);
this.$el.filter('[data-remove-tab]').toggleClass('d-none', this.$tabPanes.length <= 2);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
@@ -522,14 +540,6 @@ options.registry.navTabs = options.Class.extend({
});
}
},
/**
* @private
* @override
*/
_updateUI: async function () {
await this._super.apply(this, arguments);
this.$el.filter('[data-remove-tab]').toggleClass('d-none', this.$tabPanes.length <= 2);
},
});
options.registry.sizing_x = options.registry.sizing.extend({
@@ -1120,7 +1130,7 @@ options.registry.gallery = options.Class.extend({
}
this.mode('reset', this.getMode());
this.trigger_up('cover_update');
this._updateUI();
this.updateUI();
});
dialog.open();
},
@@ -1352,6 +1362,21 @@ options.registry.gallery = options.Class.extend({
this.mode('reset', this.getMode());
}
},
/**
* @override
*/
updateUI: async function () {
await this._super(...arguments);
this.$el.find('[data-interval]').closest('we-select')[0]
.classList.toggle('d-none', this.activeMode !== 'slideshow');
this.$el.find('[data-columns]').closest('we-select')[0]
.classList.toggle('d-none', !(this.activeMode === 'grid' || this.activeMode === 'masonry'));
this.el.querySelector('.o_w_image_spacing_option')
.classList.toggle('d-none', this.activeMode === 'slideshow');
},
//--------------------------------------------------------------------------
// Private
@@ -1440,21 +1465,6 @@ options.registry.gallery = options.Class.extend({
$container.empty().append($content);
return $container;
},
/**
* @override
*/
_updateUI: async function () {
await this._super(...arguments);
this.$el.find('[data-interval]').closest('we-select')[0]
.classList.toggle('d-none', this.activeMode !== 'slideshow');
this.$el.find('[data-columns]').closest('we-select')[0]
.classList.toggle('d-none', !(this.activeMode === 'grid' || this.activeMode === 'masonry'));
this.el.querySelector('.o_w_image_spacing_option')
.classList.toggle('d-none', this.activeMode === 'slideshow');
},
});
options.registry.gallery_img = options.Class.extend({
@@ -1553,10 +1563,16 @@ options.registry.topMenuColor = options.Class.extend({
});
return def;
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @override
*/
onFocus: function () {
updateUI: function () {
this._super(...arguments);
this.trigger_up('action_demand', {
actionName: 'get_page_option',
params: ['header_overlay'],
@@ -1779,7 +1795,7 @@ options.registry.CoverProperties = options.Class.extend({
if (!this.$target.hasClass('o_record_has_cover')) {
this.$el.find('.o_record_cover_opt_size_default[data-select-class]').click();
}
this._updateUI();
this.updateUI();
});
},
/**
@@ -1804,33 +1820,13 @@ options.registry.CoverProperties = options.Class.extend({
},
//--------------------------------------------------------------------------
// Private
// Public
//--------------------------------------------------------------------------
/**
* @override
*/
_computeWidgetState: function (methodName, params) {
switch (methodName) {
case 'filterValue': {
return parseFloat(this.$filter.css('opacity')).toFixed(1);
}
case 'filterColor': {
const classes = this.filterColorClasses.split(' ');
for (const className of classes) {
if (this.$filter.hasClass(className)) {
return className;
}
}
return '';
}
}
return this._super(...arguments);
},
/**
* @private
*/
_updateUI: async function () {
updateUI: async function () {
await this._super(...arguments);
// Only show options which are useful to the current cover
@@ -1859,6 +1855,31 @@ options.registry.CoverProperties = options.Class.extend({
this.$target[0].dataset.filterValue = this.$filterValueOpts.filter('.active').data('filterValue') || 0.0;
this.$target[0].dataset.filterColor = this.$filterColorOpts.filter('.active').data('filterColor') || '';
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
*/
_computeWidgetState: function (methodName, params) {
switch (methodName) {
case 'filterValue': {
return parseFloat(this.$filter.css('opacity')).toFixed(1);
}
case 'filterColor': {
const classes = this.filterColorClasses.split(' ');
for (const className of classes) {
if (this.$filter.hasClass(className)) {
return className;
}
}
return '';
}
}
return this._super(...arguments);
},
});
/**
@@ -26,7 +26,6 @@ sOptions.registry.js_get_posts_selectBlog = sOptions.Class.extend({
el.textContent = blog.name;
menuEl.appendChild(el);
}
return this._updateUI();
});
},
});
@@ -111,7 +111,7 @@ options.registry.CoverProperties.include({
/**
* @override
*/
_updateUI: async function () {
updateUI: async function () {
await this._super(...arguments);
var isRegularCover = this.$target.is('.o_wblog_post_page_cover_regular');
var $coverFull = this.$el.find('[data-select-class*="cover_full"]');
@@ -214,6 +214,27 @@ options.registry.WebsiteSaleProductsItem = options.Class.extend({
}).then(reload);
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @override
*/
updateUI: async function () {
await this._super.apply(this, arguments);
var sizeX = parseInt(this.$target.attr('colspan') || 1);
var sizeY = parseInt(this.$target.attr('rowspan') || 1);
var $size = this.$el.find('.o_wsale_soptions_menu_sizes');
$size.find('tr:nth-child(-n + ' + sizeY + ') td:nth-child(-n + ' + sizeX + ')')
.addClass('selected');
// Adapt size array preview to fit ppr
$size.find('tr td:nth-child(n + ' + parseInt(this.ppr + 1) + ')').hide();
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
@@ -244,22 +265,6 @@ options.registry.WebsiteSaleProductsItem = options.Class.extend({
}
});
},
/**
* @override
*/
_updateUI: async function () {
await this._super.apply(this, arguments);
var sizeX = parseInt(this.$target.attr('colspan') || 1);
var sizeY = parseInt(this.$target.attr('rowspan') || 1);
var $size = this.$el.find('.o_wsale_soptions_menu_sizes');
$size.find('tr:nth-child(-n + ' + sizeY + ') td:nth-child(-n + ' + sizeX + ')')
.addClass('selected');
// Adapt size array preview to fit ppr
$size.find('tr td:nth-child(n + ' + parseInt(this.ppr + 1) + ')').hide();
},
//--------------------------------------------------------------------------
// Handlers