summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--addons/web_editor/static/src/js/common/utils.js144
-rw-r--r--addons/web_editor/static/src/js/editor/snippets.editor.js48
-rw-r--r--addons/web_editor/static/src/js/editor/snippets.options.js1995
-rw-r--r--addons/web_editor/static/src/scss/wysiwyg_snippets.scss8
-rw-r--r--addons/web_editor/views/editor.xml1
-rw-r--r--addons/website/static/src/js/content/snippets.animation.js2
-rw-r--r--addons/website/static/src/js/editor/snippets.options.js322
-rw-r--r--addons/website/views/snippets.xml18
-rw-r--r--addons/website_blog/static/src/js/s_latest_posts_editor.js52
-rw-r--r--addons/website_blog/static/src/js/website_blog.editor.js4
-rw-r--r--addons/website_blog/views/snippets.xml4
-rw-r--r--addons/website_form/static/src/js/website_form_editor.js31
-rw-r--r--addons/website_form/static/tests/tours/website_form_editor.js2
-rw-r--r--addons/website_form/views/snippets.xml4
-rw-r--r--addons/website_sale/static/src/js/website_sale.editor.js98
-rw-r--r--addons/website_sale/views/snippets.xml2
16 files changed, 1656 insertions, 1079 deletions
diff --git a/addons/web_editor/static/src/js/common/utils.js b/addons/web_editor/static/src/js/common/utils.js
new file mode 100644
index 00000000000..f3373c7bad2
--- /dev/null
+++ b/addons/web_editor/static/src/js/common/utils.js
@@ -0,0 +1,144 @@
+odoo.define('web_editor.utils', function (require) {
+'use strict';
+
+/**
+ * window.getComputedStyle cannot work properly with CSS shortcuts (like
+ * 'border-width' which is a shortcut for the top + right + bottom + left border
+ * widths. If an option wants to customize such a shortcut, it should be listed
+ * here with the non-shortcuts property it stands for, in order.
+ *
+ * @type {Object<string[]>}
+ */
+const CSS_SHORTHANDS = {
+ 'border-width': ['border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width'],
+ 'border-radius': ['border-top-left-radius', 'border-top-right-radius', 'border-bottom-right-radius', 'border-bottom-left-radius'],
+};
+/**
+ * Key-value mapping to list converters from an unit A to an unit B.
+ * - The key is a string in the format '$1-$2' where $1 is the CSS symbol of
+ * unit A and $2 is the CSS symbol of unit B.
+ * - The value is a function that converts the received value (expressed in
+ * unit A) to another value expressed in unit B. Two other parameters is
+ * received: the css property on which the unit applies and the jQuery element
+ * on which that css property may change.
+ */
+const CSS_UNITS_CONVERSION = {
+ 's-ms': () => 1000,
+ 'ms-s': () => 0.001,
+ 'rem-px': () => _computePxByRem(),
+ 'px-rem': () => _computePxByRem(true),
+};
+
+/**
+ * Computes the number of "px" needed to make a "rem" unit. Subsequent calls
+ * returns the cached computed value.
+ *
+ * @param {boolean} [toRem=false]
+ * @returns {float} - number of px by rem if 'toRem' is false
+ * - the inverse otherwise
+ */
+function _computePxByRem(toRem) {
+ if (_computePxByRem.PX_BY_REM === undefined) {
+ const htmlStyle = window.getComputedStyle(document.documentElement);
+ _computePxByRem.PX_BY_REM = parseFloat(htmlStyle['font-size']);
+ }
+ return toRem ? (1 / _computePxByRem.PX_BY_REM) : _computePxByRem.PX_BY_REM;
+}
+/**
+ * Converts the given (value + unit) string to a numeric value expressed in
+ * the other given css unit.
+ *
+ * e.g. fct('400ms', 's') -> 0.4
+ *
+ * @param {string} value
+ * @param {string} unitTo
+ * @param {string} [cssProp] - the css property on which the unit applies
+ * @param {jQuery} [$target] - the jQuery element on which that css property
+ * may change
+ * @returns {number}
+ */
+function _convertValueToUnit(value, unitTo, cssProp, $target) {
+ const m = _getNumericAndUnit(value);
+ if (!m) {
+ return NaN;
+ }
+ const numValue = parseFloat(m[0]);
+ const valueUnit = m[1];
+ return _convertNumericToUnit(numValue, valueUnit, unitTo, cssProp, $target);
+}
+/**
+ * Converts the given numeric value expressed in the given css unit into
+ * the corresponding numeric value expressed in the other given css unit.
+ *
+ * e.g. fct(400, 'ms', 's') -> 0.4
+ *
+ * @param {number} value
+ * @param {string} unitFrom
+ * @param {string} unitTo
+ * @param {string} [cssProp] - the css property on which the unit applies
+ * @param {jQuery} [$target] - the jQuery element on which that css property
+ * may change
+ * @returns {number}
+ */
+function _convertNumericToUnit(value, unitFrom, unitTo, cssProp, $target) {
+ if (Math.abs(value) < Number.EPSILON || unitFrom === unitTo) {
+ return value;
+ }
+ const converter = CSS_UNITS_CONVERSION[`${unitFrom}-${unitTo}`];
+ if (converter === undefined) {
+ throw new Error(`Cannot convert '${unitFrom}' units into '${unitTo}' units !`);
+ }
+ return value * converter(cssProp, $target);
+}
+/**
+ * Returns the numeric value and unit of a css value.
+ *
+ * e.g. fct('400ms') -> [400, 'ms']
+ *
+ * @param {string} value
+ * @returns {Array|null}
+ */
+function _getNumericAndUnit(value) {
+ const m = value.trim().match(/^([0-9.]+)(\w*)$/);
+ if (!m) {
+ return null;
+ }
+ return [m[1], m[2]];
+}
+/**
+ * Checks if two css values are equal.
+ *
+ * @param {string} value1
+ * @param {string} value2
+ * @param {string} [cssProp] - the css property on which the unit applies
+ * @param {jQuery} [$target] - the jQuery element on which that css property
+ * may change
+ * @returns {boolean}
+ */
+function _areCssValuesEqual(value1, value2, cssProp, $target) {
+ // String comparison first
+ if (value1 === value2) {
+ return true;
+ }
+
+ // Convert the second value in the unit of the first one and compare
+ // floating values
+ const data = _getNumericAndUnit(value1);
+ if (!data) {
+ return false;
+ }
+ const numValue1 = data[0];
+ const numValue2 = _convertValueToUnit(value2, data[1], cssProp, $target);
+ return (Math.abs(numValue1 - numValue2) < Number.EPSILON);
+}
+
+return {
+ CSS_SHORTHANDS: CSS_SHORTHANDS,
+ CSS_UNITS_CONVERSION: CSS_UNITS_CONVERSION,
+ computePxByRem: _computePxByRem,
+ convertValueToUnit: _convertValueToUnit,
+ convertNumericToUnit: _convertNumericToUnit,
+ getNumericAndUnit: _getNumericAndUnit,
+ areCssValuesEqual: _areCssValuesEqual,
+};
+});
diff --git a/addons/web_editor/static/src/js/editor/snippets.editor.js b/addons/web_editor/static/src/js/editor/snippets.editor.js
index bf4dd804d1d..693eaf4001b 100644
--- a/addons/web_editor/static/src/js/editor/snippets.editor.js
+++ b/addons/web_editor/static/src/js/editor/snippets.editor.js
@@ -145,6 +145,15 @@ var SnippetEditor = Widget.extend({
//--------------------------------------------------------------------------
/**
+ * Checks whether the snippet options are shown or not.
+ *
+ * @returns {boolean}
+ */
+ areOptionsShown: function () {
+ const lastIndex = this._customize$Elements.length - 1;
+ return !!this._customize$Elements[lastIndex].parent().length;
+ },
+ /**
* Notifies all the associated snippet options that the snippet has just
* been dropped in the page.
*/
@@ -166,6 +175,17 @@ var SnippetEditor = Widget.extend({
});
},
/**
+ * Closes all widgets of all options.
+ */
+ closeWidgets: function () {
+ if (!this.areOptionsShown()) {
+ return;
+ }
+ Object.keys(this.styles).forEach(key => {
+ this.styles[key].closeWidgets();
+ });
+ },
+ /**
* Makes the editor overlay cover the associated snippet.
*/
cover: function () {
@@ -288,9 +308,7 @@ var SnippetEditor = Widget.extend({
return;
}
- var lastIndex = this._customize$Elements.length - 1;
- var optionsAlreadyShown = !!this._customize$Elements[lastIndex].parent().length;
- if (optionsAlreadyShown === show) {
+ if (this.areOptionsShown() === show) {
return;
}
this.trigger_up('update_customize_elements', {
@@ -666,7 +684,6 @@ var SnippetsMenu = Widget.extend({
id: 'oe_snippets',
cacheSnippetTemplate: {},
events: {
- 'click we-select': '_onOptionTogglerClick',
'click .o_install_btn': '_onInstallBtnClick',
},
custom_events: {
@@ -684,6 +701,7 @@ var SnippetsMenu = Widget.extend({
'hide_overlay': '_onHideOverlay',
'block_preview_overlays': '_onBlockPreviewOverlays',
'unblock_preview_overlays': '_onUnblockPreviewOverlays',
+ 'user_value_widget_opening': '_onUserValueWidgetOpening',
},
/**
@@ -1669,21 +1687,6 @@ var SnippetsMenu = Widget.extend({
this.cleanForSave();
},
/**
- * @private
- * @param {*} ev
- */
- _onOptionTogglerClick: function (ev) {
- var togglerEl = ev.currentTarget;
- if (togglerEl.tagName !== 'WE-TOGGLER') {
- togglerEl = togglerEl.querySelector('we-toggler');
- }
-
- var $hierarchyTogglers = $(togglerEl).parents('we-select').children('we-toggler');
- this.$('we-select').children('we-toggler').not($hierarchyTogglers).removeClass('active');
- $hierarchyTogglers.not(togglerEl).addClass('active');
- togglerEl.classList.toggle('active');
- },
- /**
* Called when the overlay dimensions/positions should be recomputed.
*
* @private
@@ -1836,6 +1839,13 @@ var SnippetsMenu = Widget.extend({
this.customizePanel.classList.toggle('d-none', !customize);
this.$('.o_we_customize_snippet_btn').toggleClass('active', customize);
},
+ /**
+ * Called when an user value widget is being opened -> close all the other
+ * user value widgets of all editors.
+ */
+ _onUserValueWidgetOpening: function () {
+ this.snippetEditors.forEach(editor => editor.closeWidgets());
+ },
});
return {
diff --git a/addons/web_editor/static/src/js/editor/snippets.options.js b/addons/web_editor/static/src/js/editor/snippets.options.js
index 794a733ceef..7df4ff69ade 100644
--- a/addons/web_editor/static/src/js/editor/snippets.options.js
+++ b/addons/web_editor/static/src/js/editor/snippets.options.js
@@ -2,73 +2,851 @@ odoo.define('web_editor.snippets.options', function (require) {
'use strict';
var core = require('web.core');
-var ColorPaletteWidget = require('web_editor.ColorPalette').ColorPaletteWidget;
+const concurrency = require('web.concurrency');
var Widget = require('web.Widget');
+var ColorPaletteWidget = require('web_editor.ColorPalette').ColorPaletteWidget;
+const weUtils = require('web_editor.utils');
var weWidgets = require('wysiwyg.widgets');
var qweb = core.qweb;
var _t = core._t;
/**
- * window.getComputedStyle cannot work properly with CSS shortcuts (like
- * 'border-width' which is a shortcut for the top + right + bottom + left border
- * widths. If an options wants to customize such a shortcut, it should be listed
- * here with the non-shortcuts property it stands for, in order.
- *
- * @type {Object<string[]>}
+ * @param {HTMLElement} el
+ * @param {string} [title]
+ * @param {Object} [options]
+ * @param {string[]} [options.classes]
+ * @param {Object} [options.dataAttributes]
+ * @returns {HTMLElement} - the original 'el' argument
*/
-const CSS_SHORTHANDS = {
- 'border-width': ['border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width'],
- 'border-radius': ['border-top-left-radius', 'border-top-right-radius', 'border-bottom-right-radius', 'border-bottom-left-radius'],
-};
+function _addTitleAndAllowedAttributes(el, title, options) {
+ if (title) {
+ const titleEl = _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;
+}
/**
- * Key-value mapping to list converters from an unit A to an unit B.
- * - The key is a string in the format '$1-$2' where $1 is the CSS symbol of
- * unit A and $2 is the CSS symbol of unit B.
- * - The value is a function that converts the received value (expressed in
- * unit A) to another value expressed in unit B. Two other parameters is
- * received: the css property on which the unit applies and the jQuery element
- * on which that css property may change.
+ * @param {string} tagName
+ * @param {string} title - @see _addTitleAndAllowedAttributes
+ * @param {Object} options - @see _addTitleAndAllowedAttributes
+ * @returns {HTMLElement}
*/
-const CSS_UNITS_CONVERSION = {
- 's-ms': () => 1000,
- 'ms-s': () => 0.001,
- 'rem-px': () => _computePxByRem(),
- 'px-rem': () => _computePxByRem(true),
-};
-
+function _buildElement(tagName, title, options) {
+ const el = document.createElement(tagName);
+ return _addTitleAndAllowedAttributes(el, title, options);
+}
/**
- * Computes the number of "px" needed to make a "rem" unit. Subsequent calls
- * returns the cached computed value.
+ * @param {string} title
+ * @returns {HTMLElement}
+ */
+function _buildTitleElement(title) {
+ const titleEl = document.createElement('we-title');
+ titleEl.textContent = title;
+ return titleEl;
+}
+/**
+ * Build the correct DOM for a we-row element.
*
- * @param {boolean} [toRem=false]
- * @returns {float} - number of px by rem if 'toRem' is false
- * - the inverse otherwise
+ * @param {string} [title] - @see _buildElement
+ * @param {Object} [options] - @see _buildElement
+ * @param {HTMLElement[]} [options.childNodes]
+ * @returns {HTMLElement}
*/
-function _computePxByRem(toRem) {
- if (_computePxByRem.PX_BY_REM === undefined) {
- const htmlStyle = window.getComputedStyle(document.documentElement);
- _computePxByRem.PX_BY_REM = parseFloat(htmlStyle['font-size']);
+function _buildRowElement(title, options) {
+ const groupEl = _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 toRem ? (1 / _computePxByRem.PX_BY_REM) : _computePxByRem.PX_BY_REM;
+
+ return groupEl;
}
+//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+
+/**
+ * Base class for components to be used in snippet options widgets to retrieve
+ * user values.
+ */
+const UserValueWidget = Widget.extend({
+ custom_events: {
+ 'user_value_change': '_onUserValueNotification',
+ 'user_value_preview': '_onUserValueNotification',
+ 'user_value_reset': '_onUserValueNotification',
+ },
+
+ /**
+ * @constructor
+ */
+ init: function (parent, title, options, $target) {
+ this._super(...arguments);
+ this.title = title;
+ this.options = options;
+ this._userValueWidgets = [];
+ this._value = '';
+ this.$target = $target;
+ },
+ /**
+ * @override
+ */
+ _makeDescriptive: function () {
+ const $el = this._super(...arguments);
+ _addTitleAndAllowedAttributes($el[0], this.title, this.options);
+ return $el;
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Closes the widget (only meaningful for widgets that can be closed).
+ */
+ close: function () {
+ this._userValueWidgets.forEach(widget => widget.close());
+ },
+ /**
+ * Returns the value that the widget would hold if it was active, by default
+ * the internal value it holds.
+ *
+ * @param {string} [methodName]
+ * @returns {string}
+ */
+ getActiveValue: function (methodName) {
+ return this._value;
+ },
+ /**
+ * Returns the default value the widget holds when inactive, by default the
+ * first "possible value".
+ *
+ * @param {string} [methodName]
+ * @returns {string}
+ */
+ getDefaultValue: function (methodName) {
+ const possibleValues = this._methodsParams.optionsPossibleValues[methodName];
+ return possibleValues && possibleValues[0] || '';
+ },
+ /**
+ * Returns the names of the option methods associated to the widget. Those
+ * are loaded with @see loadMethodsData.
+ *
+ * @returns {string[]}
+ */
+ getMethodsNames: function () {
+ return this._methodsNames;
+ },
+ /**
+ * Returns the option parameters associated to the widget (for a given
+ * method name or not). Most are loaded with @see loadMethodsData.
+ *
+ * @param {string} [methodName]
+ * @returns {Object}
+ */
+ getMethodsParams: function (methodName) {
+ const params = _.extend({}, this._methodsParams);
+ if (methodName) {
+ params.possibleValues = params.optionsPossibleValues[methodName] || [];
+ params.activeValue = this.getActiveValue(methodName);
+ params.defaultValue = this.getDefaultValue(methodName);
+ }
+ return params;
+ },
+ /**
+ * Returns the user value that the widget currently holds. The value is a
+ * string, this is the value that will be received in the option methods
+ * of SnippetOptionWidget instances.
+ *
+ * @param {string} [methodName]
+ * @returns {string}
+ */
+ getValue: function (methodName) {
+ const isActive = this.isActive();
+ if (!methodName || !this._methodsNames.includes(methodName)) {
+ return isActive ? 'true' : '';
+ }
+ if (isActive) {
+ return this.getActiveValue(methodName);
+ }
+ return this.getDefaultValue(methodName);
+ },
+ /**
+ * Returns whether or not the widget is active: holds a value which is
+ * not the default one.
+ *
+ * @returns {boolean}
+ */
+ isActive: function () {
+ return this._value && this._value !== this.getDefaultValue();
+ },
+ /**
+ * Indicates if the widget can contain sub user value widgets or not.
+ *
+ * @returns {boolean}
+ */
+ isContainer: function () {
+ return false;
+ },
+ /**
+ * Indicates if the widget is being previewed or not.
+ *
+ * @returns {boolean}
+ */
+ isPreviewed: function () {
+ return this.el.classList.contains('o_we_preview');
+ },
+ /**
+ * Loads option method names and option method parameters.
+ *
+ * @param {string[]} validMethodNames
+ * @param {Object} extraParams
+ */
+ loadMethodsData: function (validMethodNames, extraParams) {
+ this._methodsNames = [];
+ this._methodsParams = _.extend({}, extraParams);
+ this._methodsParams.optionsPossibleValues = {};
+
+ for (const key in this.el.dataset) {
+ const dataValue = this.el.dataset[key];
+
+ if (validMethodNames.includes(key)) {
+ this._methodsNames.push(key);
+ this._methodsParams.optionsPossibleValues[key] = dataValue.split(/\s*\|\s*/g);
+ } else {
+ this._methodsParams[key] = dataValue;
+ }
+ }
+ this._userValueWidgets.forEach(widget => {
+ const inheritedParams = _.extend({}, this._methodsParams);
+ inheritedParams.optionsPossibleValues = null;
+ widget.loadMethodsData(validMethodNames, inheritedParams);
+ const subMethodsNames = widget.getMethodsNames();
+ const subMethodsParams = widget.getMethodsParams();
+
+ for (const methodName of subMethodsNames) {
+ if (!this._methodsNames.includes(methodName)) {
+ this._methodsNames.push(methodName);
+ this._methodsParams.optionsPossibleValues[methodName] = [];
+ }
+ for (const subPossibleValue of subMethodsParams.optionsPossibleValues[methodName]) {
+ this._methodsParams.optionsPossibleValues[methodName].push(subPossibleValue);
+ }
+ }
+ });
+ for (const methodName of this._methodsNames) {
+ const arr = this._methodsParams.optionsPossibleValues[methodName];
+ const uniqArr = arr.filter((v, i, arr) => i === arr.indexOf(v));
+ this._methodsParams.optionsPossibleValues[methodName] = uniqArr;
+ }
+ },
+ /**
+ * Adds the given widget to the known list of user value sub-widgets (useful
+ * for container widgets).
+ *
+ * @param {UserValueWidget} widget
+ */
+ registerSubWidget: function (widget) {
+ this._userValueWidgets.push(widget);
+ },
+ /**
+ * Sets the user value that the widget should currently hold, for the
+ * given method name.
+ *
+ * Note: a widget typically only holds one value for the only method it
+ * supports. However, widgets can have several methods; in that case, the
+ * value is typically received for a first method and receiving the value
+ * for other ones should not affect the widget (otherwise, it means the
+ * methods are conflicting with each other).
+ *
+ * @param {string} value
+ * @param {string} [methodName]
+ */
+ setValue: function (value, methodName) {
+ this._value = value;
+ },
+ /**
+ * Updates the UI to match the user value the widget currently holds, only
+ * if the UI can currently be updated.
+ *
+ * Note: this method is only needed if @see setValue can make the widget
+ * hold a value which is not synchronized with its current UI (for focus
+ * reasons or other ones) or if the widget is not one capable of holding
+ * a value (but may have an UI which depends on other elements).
+ *
+ * @returns {Promise}
+ */
+ updateUI: async function () {
+ if (this._canUpdateUI()) {
+ await this._updateUI();
+ }
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Checks whether or not the UI can be updated. Base case: if an internal
+ * <input/> element is focused we prevent updating the UI.
+ *
+ * @private
+ * @param {*} previewMode
+ */
+ _canUpdateUI: function () {
+ const focusEl = document.activeElement;
+ if (focusEl && focusEl.tagName === 'INPUT'
+ && (this.el === focusEl || this.el.contains(focusEl))) {
+ return false;
+ }
+ return true;
+ },
+ /**
+ * @private
+ * @param {boolean} [previewMode=false]
+ */
+ _notifyValueChange: function (previewMode) {
+ const data = {
+ widget: this,
+ };
+ switch (previewMode) {
+ case undefined:
+ case false: {
+ this.trigger_up('user_value_change', data);
+ break;
+ }
+ case true: {
+ this.trigger_up('user_value_preview', data);
+ break;
+ }
+ default: {
+ this.trigger_up('user_value_reset', data);
+ }
+ }
+ },
+ /**
+ * Updates the UI to match the user value the widget currently holds (this
+ * method is called by @see updateUI and does not perform a check to verify
+ * if the UI can be updated).
+ *
+ * @private
+ * @returns {Promise}
+ */
+ _updateUI: async function () {
+ this.el.classList.remove('o_we_preview');
+ const proms = this._userValueWidgets.map(widget => widget.updateUI());
+ return Promise.all(proms);
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Should be called when an user event on the widget indicates a value
+ * change.
+ *
+ * @private
+ * @param {Event} ev
+ */
+ _onUserValueChange: function (ev) {
+ if (ev.isDefaultPrevented()) {
+ return;
+ }
+ if (!this.isPreviewed()) {
+ this._onUserValuePreview(ev);
+ }
+ ev.preventDefault();
+
+ this._notifyValueChange(false);
+ },
+ /**
+ * Allows container widgets to add additional data if needed.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onUserValueNotification: function (ev) {
+ ev.data.widget = this;
+ },
+ /**
+ * Should be called when an user event on the widget indicates a value
+ * preview.
+ *
+ * @private
+ * @param {Event} ev
+ */
+ _onUserValuePreview: function (ev) {
+ if (ev.isDefaultPrevented()) {
+ return;
+ }
+ ev.preventDefault();
+
+ this.el.classList.add('o_we_preview');
+ this._notifyValueChange(true);
+ },
+ /**
+ * Should be called when an user event on the widget indicates a value
+ * reset.
+ *
+ * @private
+ * @param {Event} ev
+ */
+ _onUserValueReset: function (ev) {
+ if (!this.isPreviewed()) {
+ return;
+ }
+ if (ev.isDefaultPrevented()) {
+ return;
+ }
+ ev.preventDefault();
+
+ this.el.classList.remove('o_we_preview');
+ this._notifyValueChange('reset');
+ },
+});
+
+const ButtonUserValueWidget = UserValueWidget.extend({
+ tagName: 'we-button',
+ events: {
+ 'click': '_onUserValueChange',
+ 'mouseenter': '_onUserValuePreview',
+ 'mouseleave': '_onUserValueReset',
+ },
+
+ /**
+ * @override
+ */
+ start: function (parent, title, options) {
+ if (this.options && this.options.childNodes) {
+ this.options.childNodes.forEach(node => this.el.appendChild(node));
+ }
+
+ return this._super(...arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ getActiveValue: function (methodName) {
+ const possibleValues = this._methodsParams.optionsPossibleValues[methodName];
+ return possibleValues && possibleValues[possibleValues.length - 1] || '';
+ },
+ /**
+ * @override
+ */
+ isActive: function () {
+ return (this.isPreviewed() !== this.el.classList.contains('active'));
+ },
+ /**
+ * @override
+ */
+ loadMethodsData: function (validMethodNames) {
+ this._super.apply(this, arguments);
+ for (const methodName of this._methodsNames) {
+ const possibleValues = this._methodsParams.optionsPossibleValues[methodName];
+ if (possibleValues.length <= 1) {
+ possibleValues.unshift('');
+ }
+ }
+ },
+ /**
+ * @override
+ */
+ setValue: function (value, methodName) {
+ let active = !!value;
+ if (methodName) {
+ if (!this._methodsNames.includes(methodName)) {
+ return;
+ }
+ active = (this.getActiveValue(methodName) === value);
+ }
+ this.el.classList.toggle('active', active);
+ },
+});
+
+const CheckboxUserValueWidget = ButtonUserValueWidget.extend({
+ className: (ButtonUserValueWidget.prototype.className || '') + ' o_we_checkbox_wrapper',
+
+ /**
+ * @override
+ */
+ start: function () {
+ const checkboxEl = document.createElement('we-checkbox');
+ this.el.appendChild(checkboxEl);
+
+ return this._super(...arguments);
+ },
+});
+
+const SelectUserValueWidget = UserValueWidget.extend({
+ tagName: 'we-select',
+ events: {
+ 'click': '_onClick',
+ },
+
+ /**
+ * @override
+ */
+ start: function () {
+ if (this.options && this.options.valueEl) {
+ this.el.appendChild(this.options.valueEl);
+ }
+
+ this.menuTogglerEl = document.createElement('we-toggler');
+ this.el.appendChild(this.menuTogglerEl);
+
+ this.menuEl = document.createElement('we-select-menu');
+ if (this.options && this.options.childNodes) {
+ this.options.childNodes.forEach(node => this.menuEl.appendChild(node));
+ }
+ this.el.appendChild(this.menuEl);
+
+ return this._super(...arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ close: function () {
+ this._super(...arguments);
+ this.menuTogglerEl.classList.remove('active');
+ },
+ /**
+ * @override
+ */
+ getValue: function (methodName) {
+ let activeWidget = this._userValueWidgets.find(widget => widget.isPreviewed());
+ if (!activeWidget) {
+ activeWidget = this._userValueWidgets.find(widget => widget.isActive());
+ }
+ if (activeWidget) {
+ return activeWidget.getActiveValue(methodName);
+ }
+ return this._super(...arguments);
+ },
+ /**
+ * @override
+ */
+ isContainer: function () {
+ return true;
+ },
+ /**
+ * @override
+ */
+ setValue: function (value, methodName) {
+ this._userValueWidgets.forEach(widget => {
+ widget.setValue('__NULL__', methodName);
+ });
+ for (const widget of [...this._userValueWidgets].reverse()) {
+ widget.setValue(value, methodName);
+ if (widget.isActive()) {
+ // Only one select item can be true at a time, we consider the
+ // last one if multiple would be active.
+ return;
+ }
+ }
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ _updateUI: async function () {
+ await this._super(...arguments);
+ const activeWidget = this._userValueWidgets.find(widget => !widget.isPreviewed() && widget.isActive());
+ this.menuTogglerEl.textContent = activeWidget ? activeWidget.el.textContent : "/";
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Called when the select is clicked anywhere -> open/close it.
+ *
+ * @private
+ */
+ _onClick: function () {
+ if (!this.menuTogglerEl.classList.contains('active')) {
+ this.trigger_up('user_value_widget_opening');
+ }
+ this.menuTogglerEl.classList.toggle('active');
+ },
+});
+
+const InputUserValueWidget = UserValueWidget.extend({
+ tagName: 'we-input',
+ events: {
+ 'input input': '_onInputInput',
+ 'blur input': '_onInputBlur',
+
+ 'click': '_onInputClick',
+ 'keydown input': '_onInputKeydown',
+ },
+
+ /**
+ * @override
+ */
+ start: function () {
+ const unit = this.el.dataset.unit || '';
+ this.el.dataset.unit = unit;
+ if (this.el.dataset.saveUnit === undefined) {
+ this.el.dataset.saveUnit = unit;
+ }
+
+ this.inputEl = document.createElement('input');
+ this.inputEl.setAttribute('type', 'text');
+ this.el.appendChild(this.inputEl);
+
+ var unitEl = document.createElement('span');
+ unitEl.textContent = unit;
+ this.el.appendChild(unitEl);
+
+ return this._super(...arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ getValue: function (methodName) {
+ const widgetValue = this._super(...arguments);
+
+ const params = this.getMethodsParams(methodName);
+ if (!params.unit) {
+ return widgetValue;
+ }
+
+ let defaultVal = weUtils.convertValueToUnit(params.defaultValue, params.saveUnit, params.cssProperty, this.$target);
+ defaultVal = isNaN(defaultVal) ? params.defaultValue : `${parseFloat(defaultVal.toFixed(3))}${params.saveUnit}`;
+
+ return widgetValue.split(/\s+/g).map(v => {
+ const numValue = parseFloat(v);
+ if (isNaN(numValue)) {
+ return defaultVal;
+ } else {
+ const value = weUtils.convertNumericToUnit(numValue, params.unit, params.saveUnit, params.cssProperty, this.$target);
+ return `${parseFloat(value.toFixed(3))}${params.saveUnit}`;
+ }
+ }).join(' ');
+ },
+ /**
+ * @override
+ */
+ loadMethodsData: function () {
+ this._super(...arguments);
+ let defaultVal = '';
+ for (const methodName of this._methodsNames) {
+ const params = this.getMethodsParams(methodName);
+ if (params.defaultValue && params.defaultValue !== 'true') {
+ defaultVal = weUtils.convertValueToUnit(params.defaultValue, params.saveUnit, params.cssProperty, this.$target);
+ defaultVal = isNaN(defaultVal) ? params.defaultValue : `${parseFloat(defaultVal.toFixed(3))}`;
+ }
+ }
+ this.inputEl.setAttribute('placeholder', defaultVal);
+ },
+ /**
+ * @override
+ */
+ setValue: function (value, methodName) {
+ const params = this.getMethodsParams(methodName);
+ if (!params.unit) {
+ return this._super(value, methodName);
+ }
+
+ const defaultValNum = weUtils.convertValueToUnit(params.defaultValue, params.unit, params.cssProperty, this.$target);
+
+ value = value.split(' ').map(v => {
+ const numValue = weUtils.convertValueToUnit(v, params.unit, params.cssProperty, this.$target);
+ if (isNaN(numValue) || Math.abs(numValue - defaultValNum) < Number.EPSILON) {
+ return ''; // Either equal to the default value and we don't want to display it, or something not supported
+ }
+ return `${parseFloat(numValue.toFixed(3))}`;
+ }).join(' ');
+
+ this._super(value, methodName);
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ _updateUI: async function () {
+ await this._super(...arguments);
+ this.inputEl.value = this._value;
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {Event} ev
+ */
+ _onInputInput: function (ev) {
+ this._value = this.inputEl.value;
+ this._onUserValuePreview(ev);
+ },
+ /**
+ * @private
+ * @param {Event} ev
+ */
+ _onInputBlur: function (ev) {
+ // Sometimes, an input is focusout for internal reason (like an undo
+ // recording) then focused again manually in the same JS stack
+ // execution. In that case, the blur should not trigger an option
+ // selection as the user did not leave the input. We thus defer the blur
+ // handling to then check that the target is indeed still blurred before
+ // executing the actual option selection.
+ setTimeout(() => {
+ if (ev.currentTarget === document.activeElement) {
+ return;
+ }
+ this._onUserValueChange(ev);
+ });
+ },
+ /**
+ * @private
+ * @param {Event} ev
+ */
+ _onInputClick: function (ev) {
+ const inputEl = ev.currentTarget.querySelector('input');
+ if (inputEl) {
+ inputEl.select();
+ }
+ },
+ /**
+ * @private
+ * @param {Event} ev
+ */
+ _onInputKeydown: function (ev) {
+ const input = ev.currentTarget;
+ let value = parseFloat(input.value || input.placeholder);
+ if (isNaN(value)) {
+ return;
+ }
+
+ let step = parseFloat(input.parentNode.dataset.step);
+ if (isNaN(step)) {
+ step = 1.0;
+ }
+ switch (ev.which) {
+ case $.ui.keyCode.UP: {
+ value += step;
+ break;
+ }
+ case $.ui.keyCode.DOWN: {
+ value -= step;
+ break;
+ }
+ default: {
+ return;
+ }
+ }
+
+ input.value = `${parseFloat(value.toFixed(3))}`;
+ $(input).trigger('input');
+ },
+});
+
+const MultiUserValueWidget = UserValueWidget.extend({
+ tagName: 'we-multi',
+
+ /**
+ * @override
+ */
+ start: function () {
+ this.el.appendChild(_buildRowElement('', this.options));
+ return this._super(...arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ getValue: function (methodName) {
+ const value = this._userValueWidgets.map(widget => {
+ return widget.getValue(methodName);
+ }).join(' ').trim();
+
+ return value || this._super(...arguments);
+ },
+ /**
+ * @override
+ */
+ isContainer: function () {
+ return true;
+ },
+ /**
+ * @override
+ */
+ setValue: function (value, methodName) {
+ let values = value.split(/\s*\|\s*/g);
+ if (values.length === 1) {
+ values = value.split(/\s+/g);
+ }
+ for (let i = 0; i < this._userValueWidgets.length - 1; i++) {
+ this._userValueWidgets[i].setValue(values.shift() || '', methodName);
+ }
+ this._userValueWidgets[this._userValueWidgets.length - 1].setValue(values.join(' '), methodName);
+ },
+});
+
+const userValueWidgetsRegistry = {
+ 'we-button': ButtonUserValueWidget,
+ 'we-checkbox': CheckboxUserValueWidget,
+ 'we-select': SelectUserValueWidget,
+ 'we-input': InputUserValueWidget,
+ 'we-multi': MultiUserValueWidget,
+};
+
+//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+
/**
* Handles a set of options for one snippet. The registry returned by this
- * module contains the names of the specialized SnippetOption which can be
+ * module contains the names of the specialized SnippetOptionWidget which can be
* referenced thanks to the data-js key in the web_editor options template.
*/
-var SnippetOption = Widget.extend({
+const SnippetOptionWidget = Widget.extend({
tagName: 'we-customizeblock-option',
- events: {
- 'mouseenter we-button': '_onOptionPreview',
- 'click we-button': '_onOptionSelection',
- 'mouseleave we-button': '_onOptionCancel',
-
- 'input we-input input': '_onOptionPreview',
- 'blur we-input input': '_onOptionInputBlur',
- 'click we-input': '_onOptionInputClick',
- 'keydown we-input input': '_onOptionInputKeydown',
+ custom_events: {
+ 'user_value_change': '_onOptionSelection',
+ 'user_value_preview': '_onOptionPreview',
+ 'user_value_reset': '_onOptionCancel',
},
/**
* Indicates if the option should be displayed in the button group at the
@@ -89,8 +867,7 @@ var SnippetOption = Widget.extend({
init: function (parent, $uiElements, $target, $overlay, data, options) {
this._super.apply(this, arguments);
- this.uiFragment = document.createDocumentFragment();
- $uiElements.clone(true).appendTo(this.uiFragment);
+ this.$originalUIElements = $uiElements;
this.$target = $target;
this.$overlay = $overlay;
@@ -100,41 +877,18 @@ var SnippetOption = Widget.extend({
this.className = 'snippet-option-' + this.data.optionName;
this.ownerDocument = this.$target[0].ownerDocument;
- this.__methodNames = [];
+
+ this._userValueWidgets = [];
+ this._selectEventMutex = new concurrency.Mutex();
},
/**
* @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);
+ willStart: async function () {
+ await this._super(...arguments);
+ return this._renderOriginalXML().then(uiFragment => {
+ this.uiFragment = uiFragment;
});
-
- return this._super(...arguments);
},
/**
* @override
@@ -142,6 +896,7 @@ var SnippetOption = Widget.extend({
renderElement: function () {
this._super(...arguments);
this.el.appendChild(this.uiFragment);
+ this.uiFragment = null;
},
/**
* Called when the option is initialized (i.e. the parent edition overlay is
@@ -149,8 +904,8 @@ var SnippetOption = Widget.extend({
*
* @override
*/
- start: function () {
- this._updateUI();
+ start: async function () {
+ await this._updateUI();
return this._super.apply(this, arguments);
},
/**
@@ -212,176 +967,107 @@ var SnippetOption = Widget.extend({
//--------------------------------------------------------------------------
/**
- * Default option method which allows to handle an user input and set the
- * appropriate data attribute on the associated snippet.
+ * Default option method which allows to select one and only one class in
+ * the option classes set and set it on the associated snippet. The common
+ * case is having a select with each item having a `data-select-class`
+ * value allowing to choose the associated class, or simply an unique
+ * checkbox to allow toggling a unique class.
*
- * @param {boolean} previewMode - never 'reset' for this method (@see this.selectClass)
- * @param {Object} dataName - the data name to customize
- * @param {jQuery} $opt - the related DOMElement option
+ * @param {boolean|string} previewMode
+ * - truthy if the option is enabled for preview or if leaving it (in
+ * that second case, the value is 'reset')
+ * - false if the option should be activated for good
+ * @param {string} widgetValue
+ * @param {Object} params
*/
- setDataAttribute: function (previewMode, dataName, $opt) {
- let hasUserValue = false;
- const weInput = $opt[0];
- const inputValue = weInput.querySelector('input').value.trim();
- const inputUnit = weInput.dataset.unit;
- const saveUnit = weInput.dataset.saveUnit;
- const defaultValue = weInput.dataset.defaultValue;
-
- let value;
- if (inputValue) {
- const numValue = parseFloat(inputValue);
- if (!isNaN(numValue)) {
- hasUserValue = true;
- value = this._convertNumericToUnit(numValue, inputUnit, saveUnit);
+ selectClass: function (previewMode, widgetValue, params) {
+ for (const classNames of params.possibleValues) {
+ if (classNames) {
+ this.$target[0].classList.remove(...classNames.trim().split(/\s+/g));
}
}
- if (value === undefined) {
- value = this._convertValueToUnit(defaultValue, saveUnit);
+ if (widgetValue) {
+ this.$target[0].classList.add(...widgetValue.trim().split(/\s+/g));
}
- this.$target[0].dataset[dataName] = isNaN(value) ? defaultValue : parseFloat(value.toFixed(3));
-
- var extraClass = weInput.dataset.extraClass;
- if (extraClass) {
- this.$target.toggleClass(extraClass, hasUserValue);
+ },
+ /**
+ * Default option method which allows to select a value and set it on the
+ * associated snippet as a data attribute. The name of the data attribute is
+ * given by the attributeName parameter.
+ *
+ * @param {boolean} previewMode - @see this.selectClass
+ * @param {string} widgetValue
+ * @param {Object} params
+ */
+ selectDataAttribute: function (previewMode, widgetValue, params) {
+ const dataName = params.attributeName;
+ if (dataName) {
+ if (params.saveUnit && !params.withUnit) {
+ // Values that come with an unit are saved without unit as
+ // data-attribute unless told otherwise.
+ widgetValue = widgetValue.split(params.saveUnit).join('');
+ }
+ this.$target[0].dataset[dataName] = widgetValue;
+ }
+ if (params.extraClass) {
+ this.$target.toggleClass(params.extraClass, params.defaultValue !== widgetValue);
}
},
/**
- * Default option method which allows to handle an user input and set the
- * appropriate css style on the associated snippet.
+ * Default option method which allows to select a value and set it on the
+ * associated snippet as a css style. The name of the css property is
+ * given by the cssProperty parameter.
*
- * @param {boolean} previewMode - never 'reset' for this method (@see this.selectClass)
- * @param {Object} mainCssProp - the cssProp to customize
- * @param {jQuery} $opt - the related DOMElement option
- */
- setStyle: function (previewMode, mainCssProp, $opt) {
- let hasUserValue = false;
- const cssProps = CSS_SHORTHANDS[mainCssProp] || [mainCssProp];
-
- // Join all inputs controlling the same css property and split user
- // input into sub-properties (note this code handles the two at the same
- // time but should normally not be combined).
- const $weInputs = this.$el.find(`[data-set-style=${mainCssProp}]`);
- const subValuesByInput = _.map($weInputs, weInput => {
- const inputValue = weInput.querySelector('input').value.trim();
- const inputUnit = weInput.dataset.unit;
- const saveUnit = weInput.dataset.saveUnit;
- const defaultValue = weInput.dataset.defaultValue;
- let convertedDefaultValue = this._convertValueToUnit(defaultValue, saveUnit, mainCssProp);
- convertedDefaultValue = isNaN(convertedDefaultValue) ? defaultValue : (parseFloat(convertedDefaultValue.toFixed(3)) + saveUnit);
-
- const values = inputValue.split(/\s+/g).map(v => {
- const numValue = parseFloat(v);
- if (isNaN(numValue)) {
- return convertedDefaultValue;
- } else {
- hasUserValue = true;
- const value = this._convertNumericToUnit(numValue, inputUnit, saveUnit, mainCssProp);
- return parseFloat(value.toFixed(3)) + saveUnit;
+ * @param {boolean} previewMode - @see this.selectClass
+ * @param {string} widgetValue
+ * @param {Object} params
+ */
+ selectStyle: function (previewMode, widgetValue, params) {
+ const cssProps = weUtils.CSS_SHORTHANDS[params.cssProperty] || [params.cssProperty];
+ const values = widgetValue.split(/\s+/g);
+ while (values.length < cssProps.length) {
+ switch (values.length) {
+ case 1:
+ case 2: {
+ values.push(values[0]);
+ break;
}
- });
- while (values.length < cssProps.length) {
- switch (values.length) {
- case 1:
- case 2:
- values.push(values[0]);
- break;
- case 3:
- values.push(values[1]);
- break;
- default:
- values.push(values[values.length - 1]);
+ case 3: {
+ values.push(values[1]);
+ break;
+ }
+ default: {
+ values.push(values[values.length - 1]);
}
}
- return values;
- });
+ }
for (const cssProp of cssProps) {
// Always reset the inline style first to not put inline style on an
// element which already have this style through css stylesheets.
this.$target[0].style.setProperty(cssProp, '');
}
- const styles = window.getComputedStyle(this.$target[0]);
- cssProps.forEach((cssProp, i) => {
- const cssValue = subValuesByInput.map(inputSubValues => inputSubValues[i]).join(' ');
- if (styles[cssProp] !== cssValue) {
- this.$target[0].style.setProperty(cssProp, cssValue, 'important');
- }
- });
-
- var extraClass = $opt[0].dataset.extraClass;
- if (extraClass) {
- this.$target.toggleClass(extraClass, hasUserValue);
- }
- },
- /**
- * Default option method which allows to select one and only one class in
- * the option classes set and set it on the associated snippet. The common
- * case is having a sub-collapse with each item having a `data-select-class`
- * value allowing to choose the associated class.
- *
- * @param {boolean|string} previewMode
- * - truthy if the option is enabled for preview or if leaving it (in
- * that second case, the value is 'reset')
- * - false if the option should be activated for good
- * @param {*} value - the class to activate ($opt.data('selectClass'))
- * @param {jQuery} $opt - the related DOMElement option
- */
- selectClass: function (previewMode, value, $opt) {
- var $group = $opt && $opt.parents('we-select').last();
- if (!$group || !$group.length) {
- $group = this.$el;
+ if (params.extraClass) {
+ this.$target.removeClass(params.extraClass);
}
- var $lis = $group.find('[data-select-class]');
- var classes = $lis.map(function () {
- return $(this).data('selectClass');
- }).get().join(' ');
- this.$target.removeClass(classes);
- if (value) {
- this.$target.addClass(value);
- }
- },
- /**
- * Default option method which allows to select one and only one value in
- * the option values set and set it on the associated snippet as a data
- * attribute. The name of the data attribute is given by the closest
- * ancestor's data value for 'attributeName'.
- *
- * @param {boolean} previewMode - @see this.selectClass
- * @param {Object} value - the data attribute value to set
- * @param {jQuery} $opt - the related DOMElement option
- */
- selectDataAttribute: function (previewMode, value, $opt) {
- const $ancestor = $opt && $opt.closest('[data-attribute-name]');
- if (!$ancestor || !$ancestor.length) {
- return;
+ const styles = window.getComputedStyle(this.$target[0]);
+ let hasUserValue = false;
+ for (let i = cssProps.length - 1; i > 0; i--) {
+ hasUserValue = applyCSS.call(this, cssProps[i], values.pop(), styles) || hasUserValue;
}
- const dataName = $ancestor[0].dataset.attributeName;
- if (!dataName) {
- return;
+ hasUserValue = applyCSS.call(this, cssProps[0], values.join(' '), styles) || hasUserValue;
+
+ function applyCSS(cssProp, cssValue, styles) {
+ if (!weUtils.areCssValuesEqual(styles[cssProp], cssValue)) {
+ this.$target[0].style.setProperty(cssProp, cssValue, 'important');
+ return true;
+ }
+ return false;
}
- this.$target[0].dataset[dataName] = value || '';
- },
- /**
- * Default option method which allows to select one or multiple classes in
- * the option classes set and set it on the associated snippet. The common
- * case is having a sub-collapse with each item having a `data-toggle-class`
- * value allowing to toggle the associated class.
- *
- * @see this.selectClass
- */
- toggleClass: function (previewMode, value, $opt) {
- const $opts = this.$el.find('[data-toggle-class]').not('[data-set-style]');
- const classes = $opts.map(function () {
- return $(this).data('toggleClass');
- }).get().join(' ');
- const activeClasses = $opts.filter('.active, :has(.active)').map(function () {
- return $(this).data('toggleClass');
- }).get().join(' ');
- this.$target.removeClass(classes).addClass(activeClasses);
- if (value && previewMode !== 'reset') {
- this.$target.toggleClass(value);
+ if (params.extraClass) {
+ this.$target.toggleClass(params.extraClass, hasUserValue);
}
},
@@ -399,16 +1085,23 @@ var SnippetOption = Widget.extend({
return this.$target.find.apply(this.$target, arguments);
},
/**
+ * Closes all user value widgets.
+ */
+ closeWidgets: function () {
+ this._userValueWidgets.forEach(widget => widget.close());
+ },
+ /**
* Sometimes, options may need to notify other options, even in parent
* editors. This can be done thanks to the 'option_update' event, which
* will then be handled by this function.
*
* @param {string} name - an identifier for a type of update
* @param {*} data
+ * @returns {Promise}
*/
- notify: function (name, data) {
+ notify: async function (name, data) {
if (name === 'target') {
- this.setTarget(data);
+ await this.setTarget(data);
}
},
/**
@@ -418,10 +1111,11 @@ var SnippetOption = Widget.extend({
* function allows to set the option's target.
*
* @param {jQuery} $target - the new target element
+ * @returns {Promise}
*/
- setTarget: function ($target) {
+ setTarget: async function ($target) {
this.$target = $target;
- this._updateUI();
+ await this._updateUI();
this.$target.trigger('snippet-option-change', [this]);
},
@@ -430,219 +1124,187 @@ var SnippetOption = Widget.extend({
//--------------------------------------------------------------------------
/**
- * Build the correct DOM for an element according to the given options.
+ * Returns the string value that should be hold by the widget which is
+ * related to the given method name.
*
* @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];
+ * @param {string} methodName
+ * @param {Object} params
+ * @returns {Promise<string>|string}
+ */
+ _computeWidgetState: async function (methodName, params) {
+ switch (methodName) {
+ case 'selectClass': {
+ let maxNbClasses = 0;
+ let activeClassNames = '';
+ params.possibleValues.forEach(classNames => {
+ if (!classNames) {
+ return;
+ }
+ const classes = classNames.split(/\s+/g);
+ if (classes.length >= maxNbClasses
+ && classes.every(className => this.$target[0].classList.contains(className))) {
+ maxNbClasses = classes.length;
+ activeClassNames = classNames;
+ }
+ });
+ return activeClassNames;
+ }
+ case 'selectDataAttribute': {
+ const dataName = params.attributeName;
+ let dataValue = (this.$target[0].dataset[dataName] || '').trim();
+ if (params.saveUnit && !params.withUnit) {
+ dataValue = dataValue.split(/\s+/g).map(v => v + params.saveUnit).join(' ');
+ }
+ return dataValue || params.attributeDefaultValue || '';
+ }
+ case 'selectStyle': {
+ const styles = window.getComputedStyle(this.$target[0]);
+ const cssProps = weUtils.CSS_SHORTHANDS[params.cssProperty] || [params.cssProperty];
+ const cssValues = cssProps.map(cssProp => {
+ return styles[cssProp].trim();
+ });
+ if (cssValues.length === 4 && weUtils.areCssValuesEqual(cssValues[3], cssValues[1], params.cssProperty, this.$target)) {
+ cssValues.pop();
+ }
+ if (cssValues.length === 3 && weUtils.areCssValuesEqual(cssValues[2], cssValues[0], params.cssProperty, this.$target)) {
+ cssValues.pop();
+ }
+ if (cssValues.length === 2 && weUtils.areCssValuesEqual(cssValues[1], cssValues[0], params.cssProperty, this.$target)) {
+ cssValues.pop();
+ }
+ return cssValues.join(' ');
}
}
-
- return el;
},
/**
- * Build the correct DOM for a we-checkbox element.
- *
- * @private
- * @param {string} [title]
- * @param {Object} [options] - @see this._buildElement
- * @returns {HTMLElement}
+ * @static
+ * @param {HTMLElement} el
+ * @returns {Object}
*/
- _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;
+ _extraInfoFromDescriptionElement: function (el) {
+ return {
+ title: el.getAttribute('string'),
+ options: {
+ classes: el.classList,
+ dataAttributes: el.dataset,
+ childNodes: [...el.childNodes],
+ },
+ };
},
/**
- * 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}
+ * @param {string} widgetName
+ * @param {UserValueWidget|this|null} parent
+ * @param {string} title
+ * @param {Object} options
+ * @returns {UserValueWidget}
*/
- _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));
+ _registerUserValueWidget: function (widgetName, parent, title, options) {
+ const widget = new userValueWidgetsRegistry[widgetName](parent, title, options, this.$target);
+ if (!parent || parent === this) {
+ this._userValueWidgets.push(widget);
+ } else {
+ parent.registerSubWidget(widget);
}
-
- return groupEl;
+ return widget;
},
/**
- * Build the correct DOM for a we-input element.
- *
* @private
- * @param {string} [title]
- * @param {Object} [options] - @see this._buildElement
+ * @param {HTMLElement} uiFragment
+ * @returns {Promise}
*/
- _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;
+ _renderCustomWidgets: function (uiFragment) {
+ return Promise.resolve();
},
/**
- * 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}
+ * @param {HTMLElement} uiFragment
+ * @returns {Promise}
*/
- _buildSelectElement: function (title, options) {
- const selectEl = this._buildElement('we-select', title, options);
+ _renderCustomXML: function (uiFragment) {
+ return Promise.resolve();
+ },
+ /**
+ * @private
+ * @param {jQuery} [$xml] - default to original xml content
+ * @returns {Promise}
+ */
+ _renderOriginalXML: async function ($xml) {
+ const uiFragment = document.createDocumentFragment();
+ ($xml || this.$originalUIElements).clone(true).appendTo(uiFragment);
- if (options && options.valueEl) {
- selectEl.appendChild(options.valueEl);
- }
+ // Build layouting components first
+ uiFragment.querySelectorAll('we-row').forEach(el => {
+ const infos = this._extraInfoFromDescriptionElement(el);
+ const groupEl = _buildRowElement(infos.title, infos.options);
+ el.parentNode.insertBefore(groupEl, el);
+ el.parentNode.removeChild(el);
+ });
- const menuTogglerEl = document.createElement('we-toggler');
- selectEl.appendChild(menuTogglerEl);
+ // Load widgets
+ await this._renderCustomXML(uiFragment);
+ await this._renderXMLWidgets(uiFragment);
+ await this._renderCustomWidgets(uiFragment);
- var menuEl = document.createElement('we-select-menu');
- if (options && options.childNodes) {
- options.childNodes.forEach(node => menuEl.appendChild(node));
+ const validMethodNames = [];
+ for (const key in this) {
+ validMethodNames.push(key);
}
- selectEl.appendChild(menuEl);
+ this._userValueWidgets.forEach(widget => {
+ widget.loadMethodsData(validMethodNames);
+ });
- return selectEl;
+ return uiFragment;
},
+
/**
* @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.
- *
- * e.g. fct('400ms', 's') -> 0.4
- *
- * @param {string} value
- * @param {string} unitTo
- * @param {string} [cssProp]
- * @returns {number}
+ * @param {HTMLElement} parentEl
+ * @param {SnippetOptionWidget|UserValueWidget} parentWidget
+ * @returns {Promise}
*/
- _convertValueToUnit: function (value, unitTo, cssProp) {
- const m = value.trim().match(/^([0-9.]+)(\w*)$/);
- if (!m) {
- return NaN;
- }
- const numValue = parseFloat(m[1]);
- const valueUnit = m[2];
- return this._convertNumericToUnit(numValue, valueUnit, unitTo, cssProp);
+ _renderXMLWidgets: function (parentEl, parentWidget) {
+ const proms = [...parentEl.children].map(el => {
+ const widgetName = el.tagName.toLowerCase();
+ if (!userValueWidgetsRegistry.hasOwnProperty(widgetName)) {
+ return this._renderXMLWidgets(el, parentWidget);
+ }
+
+ const infos = this._extraInfoFromDescriptionElement(el);
+ const widget = this._registerUserValueWidget(widgetName, parentWidget || this, infos.title, infos.options);
+ return widget.insertAfter(el).then(() => {
+ // Remove the original element afterwards as the insertion
+ // operation may move some of its inner content during
+ // widget start.
+ parentEl.removeChild(el);
+
+ if (widget.isContainer()) {
+ return this._renderXMLWidgets(widget.el, widget);
+ }
+ });
+ });
+ return Promise.all(proms);
},
/**
- * Converts the given numeric value expressed in the given css unit into
- * the corresponding numeric value expressed in the other given css unit.
- *
- * e.g. fct(400, 'ms', 's') -> 0.4
- *
- * @param {number} value
- * @param {string} unitFrom
- * @param {string} unitTo
- * @param {string} [cssProp]
- * @returns {number}
+ * @private
+ * @param {function<Promise<jQuery>>} [callback]
+ * @returns {Promise}
*/
- _convertNumericToUnit: function (value, unitFrom, unitTo, cssProp) {
- if (Math.abs(value) < Number.EPSILON || unitFrom === unitTo) {
- return value;
- }
- const converter = CSS_UNITS_CONVERSION[`${unitFrom}-${unitTo}`];
- if (converter === undefined) {
- throw new Error(`Cannot convert ${unitFrom} into ${unitTo} !`);
+ _rerenderXML: async function (callback) {
+ this._userValueWidgets.forEach(widget => widget.destroy());
+ this._userValueWidgets = [];
+ this.$el.empty();
+
+ let $xml = undefined;
+ if (callback) {
+ $xml = await callback.call(this);
}
- 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.
- */
- _reset: function () {
- var self = this;
- var $actives = this.$el.find('we-button.active');
- _.each($actives, function (activeElement) {
- var $activeElement = $(activeElement);
- self.__methodNames = _.without.apply(_, [self.__methodNames].concat(_.keys($activeElement.data())));
- self._select('reset', $activeElement);
- });
- _.each(this.__methodNames, function (methodName) {
- self[methodName]('reset');
+
+ return this._renderOriginalXML($xml).then(uiFragment => {
+ this.$el.append(uiFragment);
+ return this._updateUI();
});
- this.__methodNames = [];
},
/**
* Activates the option associated to the given DOM element.
@@ -652,223 +1314,60 @@ var SnippetOption = Widget.extend({
* - truthy if the option is enabled for preview or if leaving it (in
* that second case, the value is 'reset')
* - false if the option should be activated for good
- * @param {jQuery} $opt - the related DOMElement option
+ * @param {UserValueWidget} widget - the widget which triggered the option change
+ * @returns {Promise}
*/
- _select: function (previewMode, $opt) {
+ _select: async function (previewMode, widget) {
// Options can say they respond to strong choice
- if (previewMode && ($opt.data('noPreview') || $opt.closest('[data-no-preview="true"]').length)) {
+ if (previewMode && (widget.$el.closest('[data-no-preview="true"]').length)) {
+ // TODO the no-preview flag should be retrieved through widget params
return;
}
// If it is not preview mode, the user selected the option for good
// (so record the action)
if (!previewMode) {
- this._reset();
this.trigger_up('request_history_undo_record', {$target: this.$target});
}
- // Search for methods (data-...) (i.e. data-toggle-class) on the
- // selected (sub)option and its parents
- var el = $opt[0];
- var methods = [];
- do {
- methods.push([el, el.dataset]);
- el = el.parentNode;
- } while (this.$el.parent().has(el).length);
-
- // Call the found method in the right order (parents -> child)
- methods.reverse().forEach(data => {
- var $el = $(data[0]);
- var methods = data[1];
-
- const isInput = $el.is('we-input');
-
- Object.keys(methods).forEach(methodName => {
- if (!this[methodName]) {
- return;
- }
- if (previewMode === true && !isInput) {
- this.__methodNames.push(methodName);
- }
- this[methodName](previewMode, methods[methodName], $el);
- });
+ widget.getMethodsNames().forEach(methodName => {
+ this[methodName](previewMode, widget.getValue(methodName), widget.getMethodsParams(methodName));
});
- this.__methodNames = _.uniq(this.__methodNames);
if (!previewMode) {
- this._updateUI();
+ await this._updateUI(w => !w.isPreviewed() || w === widget);
+ } else {
+ await this._updateUI(w => w !== widget && !w.isPreviewed());
}
this.$target.trigger('content_changed');
},
/**
- * Tweaks the option DOM elements to show the selected value according to
- * the state of the $target the option customizes.
+ * Updates the UI. For widget update, @see _computeWidgetState.
*
- * @todo should be extendable in a more easy way
- * @private
+ * @param {function} [callback] - used to filter widgets to update
+ * @returns {Promise}
*/
- _setActive: function () {
- var self = this;
-
- // --- TOGGLE CLASS ---
-
- this.$el.find('[data-toggle-class]')
- .removeClass('active')
- .filter(function () {
- var className = $(this).data('toggleClass');
- return !className || self.$target.hasClass(className);
- })
- .addClass('active');
-
- // --- SELECT CLASS ---
-
- // Get submenus which are not inside submenus
- var $submenus = this.$el.find('we-select')
- .not('we-select *');
-
- // Add unique active class for each submenu active item
- _.each($submenus, function (submenu) {
- var $elements = $(submenu).find('[data-select-class]');
- _processSelectClassElements($elements);
- });
-
- // Add unique active class for out-of-submenu active item
- var $externalElements = this.$el.find('[data-select-class]')
- .not('we-select *');
- _processSelectClassElements($externalElements);
-
- function _processSelectClassElements($elements) {
- var maxNbClasses = -1;
- $elements.removeClass('active')
- .filter(function () {
- var className = $(this).data('selectClass');
- var nbClasses = className ? className.split(' ').length : 0;
- if (nbClasses >= maxNbClasses && (!className || self.$target.hasClass(className))) {
- maxNbClasses = nbClasses;
- return true;
- }
- return false;
- })
- .last()
- .addClass('active');
- }
-
- // --- SELECT DATA ATTRIBUTE ---
-
- this.$el.find('[data-select-data-attribute]')
- .removeClass('active')
- .filter((i, el) => {
- const ancestorEl = el.closest('[data-attribute-name]');
- if (!ancestorEl) {
- return false;
- }
- const dataName = ancestorEl.dataset.attributeName;
- const defaultValue = ancestorEl.dataset.attributeDefaultValue;
- const dataValue = el.dataset.selectDataAttribute;
- const targetValue = this.$target[0].dataset[dataName] || defaultValue;
- return (!targetValue && !dataValue) || targetValue === dataValue;
- })
- .addClass('active');
-
- // --- SET DATA ATTRIBUTE --- (note: important to be done last because of active removal)
-
- this.el.querySelectorAll('[data-set-data-attribute]').forEach(el => {
- el.classList.remove('active');
- const inputEl = el.querySelector('input');
- if (inputEl === document.activeElement) {
- return;
- }
-
- const inputUnit = el.dataset.unit;
- const saveUnit = el.dataset.saveUnit;
- const defaultValue = el.dataset.defaultValue;
- const convertedDefaultValue = this._convertValueToUnit(defaultValue, inputUnit);
-
- let value = this.$target[0].dataset[el.dataset.setDataAttribute];
- let numValue = parseFloat(value);
- if (!isNaN(numValue)) {
- numValue = this._convertNumericToUnit(numValue, saveUnit, inputUnit);
- }
- if (isNaN(numValue) || Math.abs(numValue - convertedDefaultValue) < Number.EPSILON) {
- value = '';
- } else {
- value = parseFloat(numValue.toFixed(3));
- }
- inputEl.value = value;
- });
-
- // --- SET STYLE --- (note: important to be done last because of active removal)
-
- let styles;
- const seenSetStyles = [];
- this.el.querySelectorAll('[data-set-style]').forEach(el => {
- const mainCssProp = el.dataset.setStyle;
- if (seenSetStyles.includes(mainCssProp)) {
+ _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;
}
- seenSetStyles.push(mainCssProp);
-
- const $els = this.$el.find(`[data-set-style="${mainCssProp}"]`);
- $els.removeClass('active');
- const $inputs = $els.find('> input');
- if (_.any($inputs, input => input === document.activeElement)) {
- return;
- }
-
- styles = styles || window.getComputedStyle(this.$target[0]);
- const cssProps = CSS_SHORTHANDS[mainCssProp] || [mainCssProp];
- const valuesByProp = cssProps.map(cssProp => {
- const cssValue = styles[cssProp];
- if (!cssValue) {
- return [];
- }
-
- return cssValue.split(/\s+/).map((v, i) => {
- const inputUnit = $els[i].dataset.unit;
- const numValue = this._convertValueToUnit(v, inputUnit, mainCssProp);
- return isNaN(numValue) ? v : numValue;
- });
+ const methodsNames = widget.getMethodsNames();
+ const proms = methodsNames.map(async methodName => {
+ const value = await this._computeWidgetState(methodName, widget.getMethodsParams(methodName));
+ widget.setValue(value, methodName);
});
- _.each($els, (el, i) => {
- let values = valuesByProp.map(propValues => propValues[i]);
-
- if (values.length === 4 && Math.abs(values[3] - values[1]) < Number.EPSILON) {
- values.pop();
- }
- if (values.length === 3 && Math.abs(values[2] - values[0]) < Number.EPSILON) {
- values.pop();
- }
- if (values.length === 2 && Math.abs(values[1] - values[0]) < Number.EPSILON) {
- values.pop();
- }
+ await Promise.all(proms);
- const inputUnit = el.dataset.unit;
- const defaultValue = el.dataset.defaultValue;
- const convertedDefaultValue = this._convertValueToUnit(defaultValue, inputUnit, mainCssProp);
- if (values.length === 1 && Math.abs(values[0] - convertedDefaultValue) < Number.EPSILON) {
- values.pop();
- }
-
- values = values.filter(v => isFinite(v));
- values = values.map(v => parseFloat(v.toFixed(3)));
- $inputs[i].value = (values.length ? values.join(' ') : '');
- });
- });
- },
- /**
- * @private
- */
- _updateUI: function () {
- this._setActive();
-
- this.el.querySelectorAll('we-select').forEach(selectEl => {
- const activeEl = selectEl.querySelector('we-button.active');
- const valueEl = selectEl.querySelector('we-toggler');
- if (valueEl) {
- valueEl.textContent = activeEl ? activeEl.textContent : "/";
- }
+ // Refresh the UI of all widgets (after all the current values they
+ // hold have been updated).
+ return widget.updateUI();
});
+ return Promise.all(proms);
},
//--------------------------------------------------------------------------
@@ -876,63 +1375,6 @@ var SnippetOption = Widget.extend({
//--------------------------------------------------------------------------
/**
- * @private
- * @param {Event} ev
- */
- _onOptionInputBlur: function (ev) {
- // Sometimes, an input is focusout for internal reason (like an undo
- // recording) then focused again manually in the same JS stack
- // execution. In that case, the blur should not trigger an option
- // selection as the user did not leave the input. We thus defer the blur
- // handling to then check that the target is indeed still blurred before
- // executing the actual option selection.
- setTimeout(() => {
- if (ev.currentTarget === document.activeElement) {
- return;
- }
- this._onOptionSelection(ev);
- });
- },
- /**
- * @private
- * @param {Event} ev
- */
- _onOptionInputClick: function (ev) {
- const inputEl = ev.currentTarget.querySelector('input');
- if (inputEl) {
- inputEl.select();
- }
- },
- /**
- * @private
- * @param {Event} ev
- */
- _onOptionInputKeydown: function (ev) {
- const input = ev.currentTarget;
- let value = parseFloat(input.value || input.placeholder);
- if (isNaN(value)) {
- return;
- }
-
- let step = parseFloat(input.parentNode.dataset.step);
- if (isNaN(step)) {
- step = 1.0;
- }
- switch (ev.which) {
- case $.ui.keyCode.UP:
- value += step;
- break;
- case $.ui.keyCode.DOWN:
- value -= step;
- break;
- default:
- return;
- }
-
- input.value = parseFloat(value.toFixed(3));
- $(input).trigger('input');
- },
- /**
* Called when a option link is entered or an option input content is being
* modified -> activates the related option in preview mode.
*
@@ -940,17 +1382,12 @@ var SnippetOption = Widget.extend({
* @param {Event} ev
*/
_onOptionPreview: function (ev) {
- var $opt = $(ev.target).closest('we-button, we-input');
- if (!$opt.length) {
- return;
- }
-
- if (!$opt.is(':hasData')) {
- return;
- }
- this.__click = false;
- this._select(true, $opt);
- this.$target.trigger('snippet-option-preview', [this]);
+ ev.stopPropagation();
+ this._selectEventMutex.exec(() => {
+ return this._select(true, ev.data.widget).then(() => {
+ this.$target.trigger('snippet-option-preview', [this]);
+ });
+ });
},
/**
* Called when an option link is clicked or an option input content is
@@ -960,38 +1397,32 @@ var SnippetOption = Widget.extend({
* @param {Event} ev
*/
_onOptionSelection: function (ev) {
- var $opt = $(ev.target).closest('we-button, we-input');
- if (ev.isDefaultPrevented() || !$opt.length || !$opt.is(':hasData')) {
- return;
- }
-
- ev.preventDefault();
- this.__click = true;
- this._select(false, $opt);
- this.$target.trigger('snippet-option-change', [this]);
+ ev.stopPropagation();
+ this._selectEventMutex.exec(() => {
+ return this._select(false, ev.data.widget).then(() => {
+ this.$target.trigger('snippet-option-change', [this]);
+ });
+ });
},
/**
* Called when an option link/menu is left -> reactivate the options that
* were activated before previews.
*
* @private
+ * @param {Event} ev
*/
- _onOptionCancel: function () {
- if (this.__click) {
- return;
- }
- this._reset();
+ _onOptionCancel: function (ev) {
+ ev.stopPropagation();
+ this._selectEventMutex.exec(() => {
+ return this._select('reset', ev.data.widget);
+ });
},
});
+const registry = {};
//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
-/**
- * The registry object contains the list of available options.
- */
-var registry = {};
-
-registry.sizing = SnippetOption.extend({
+registry.sizing = SnippetOptionWidget.extend({
/**
* @override
*/
@@ -1127,8 +1558,8 @@ registry.sizing = SnippetOption.extend({
/**
* @override
*/
- setTarget: function () {
- this._super.apply(this, arguments);
+ setTarget: async function () {
+ await this._super(...arguments);
this._onResize();
},
@@ -1243,7 +1674,7 @@ registry['sizing_y'] = registry.sizing.extend({
/**
* Handles the edition of snippet's background color classes.
*/
-registry.colorpicker = SnippetOption.extend({
+registry.colorpicker = SnippetOptionWidget.extend({
xmlDependencies: ['/web_editor/static/src/xml/snippets.xml'],
custom_events: {
'color_picked': '_onColorPicked',
@@ -1276,14 +1707,15 @@ 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 selectWidget = new SelectUserValueWidget(this, this.data.string, {
classes: ['o_we_so_color_palette'],
childNodes: [this.colorPalette.el],
valueEl: this.colorPreviewEl,
});
+ await selectWidget.appendTo(document.createDocumentFragment());
// Replace the colorpicker UI with the select element
- this.$el.empty().append(selectEl);
+ this.$el.empty().append(selectWidget.el);
return _super(...args);
},
/**
@@ -1315,8 +1747,8 @@ registry.colorpicker = SnippetOption.extend({
/**
* @override
*/
- _updateUI: function () {
- this._super.apply(this, arguments);
+ _updateUI: async function () {
+ await this._super.apply(this, arguments);
this.colorPreviewEl.style.backgroundColor = this.$target.css('background-color');
},
@@ -1331,9 +1763,10 @@ registry.colorpicker = SnippetOption.extend({
* @param {Event} ev
*/
_onColorPicked: function () {
- this._updateUI();
- this.$target.closest('.o_editable').trigger('content_changed');
- this.$target.trigger('background-color-event', false);
+ this._updateUI().then(() => {
+ this.$target.closest('.o_editable').trigger('content_changed');
+ this.$target.trigger('background-color-event', false);
+ });
},
/**
* Called when a custom color is selected
@@ -1371,46 +1804,25 @@ registry.colorpicker = SnippetOption.extend({
*/
_onColorReset: function () {
this._changeTargetColor('');
- this._updateUI();
- this.$target.trigger('content_changed');
+ this._updateUI().then(() => {
+ this.$target.trigger('content_changed');
+ });
},
});
/**
* Handles the edition of snippet's background image.
*/
-registry.background = SnippetOption.extend({
+registry.background = SnippetOptionWidget.extend({
/**
* @override
*/
start: function () {
- // Build option UI controls
- const editBgEl = document.createElement('we-button');
- editBgEl.dataset.chooseImage = 'true';
- editBgEl.dataset.noPreview = 'true';
- const iconEl = document.createElement('i');
- iconEl.classList.add('fa', 'fa-fw', 'fa-edit');
- this.editBgTextEl = document.createElement('span');
- editBgEl.appendChild(this.editBgTextEl);
- editBgEl.appendChild(iconEl);
-
- this.removeBgEl = document.createElement('we-button');
- this.removeBgEl.classList.add('fa', 'fa-fw', 'fa-times');
- this.removeBgEl.title = _t("Remove the background");
- this.removeBgEl.dataset.background = '';
- this.removeBgEl.dataset.noPreview = 'true';
-
- this.$el.append(this._buildRowElement(this.data.string, {
- childNodes: [editBgEl, this.removeBgEl],
- }));
-
- var res = this._super.apply(this, arguments);
-
// Initialize background and events
this.bindBackgroundEvents();
this.__customImageSrc = this._getSrcFromCssValue();
- return res;
+ return this._super(...arguments);
},
//--------------------------------------------------------------------------
@@ -1422,16 +1834,16 @@ registry.background = SnippetOption.extend({
*
* @see this.selectClass for parameters
*/
- background: function (previewMode, value, $opt) {
- if (previewMode === 'reset' && value === undefined) {
+ background: function (previewMode, widgetValue, params) {
+ if (previewMode === 'reset') {
// No background has been selected and we want to reset back to the
// original custom image
- this._setCustomBackground(this.__customImageSrc);
+ this._setCustomBackground(this.__customImageSrc); // FIXME this is async...
return;
}
- if (value && value.length) {
- this.$target.css('background-image', 'url(\'' + value + '\')');
+ if (widgetValue) {
+ this.$target.css('background-image', 'url(\'' + widgetValue + '\')');
this.$target.removeClass('oe_custom_bg').addClass('oe_img_bg');
} else {
this.$target.css('background-image', '');
@@ -1439,25 +1851,19 @@ registry.background = SnippetOption.extend({
}
},
/**
- * @override
- */
- selectClass: function (previewMode, value, $opt) {
- this.background(previewMode, '', $opt);
- this._super(previewMode, value ? (value + ' oe_img_bg') : value, $opt);
- },
- /**
* Opens a media dialog to add a custom background image.
*
* @see this.selectClass for parameters
*/
- chooseImage: function (previewMode, value, $opt) {
+ chooseImage: function (previewMode, widgetValue, params) {
var options = this._getMediaDialogOptions();
var media = this._getEditableMedia();
var _editor = new weWidgets.MediaDialog(this, options, media).open();
_editor.on('save', this, data => {
- this._onSaveMediaDialog(data);
- this.$target.trigger('content_changed');
+ this._onSaveMediaDialog(data).then(() => {
+ this.$target.trigger('content_changed');
+ });
});
_editor.on('closed', this, () => {
if (media.classList.contains('o_we_fake_image')) {
@@ -1484,8 +1890,8 @@ registry.background = SnippetOption.extend({
/**
* @override
*/
- setTarget: function () {
- this._super.apply(this, arguments);
+ setTarget: async function () {
+ await this._super(...arguments);
// TODO should be automatic for all options as equal to the start method
this.bindBackgroundEvents();
this.__customImageSrc = this._getSrcFromCssValue();
@@ -1551,10 +1957,10 @@ registry.background = SnippetOption.extend({
/**
* @override
*/
- _updateUI: function () {
- this._super.apply(this, arguments);
+ _updateUI: async function () {
+ await this._super.apply(this, arguments);
var src = this._getSrcFromCssValue();
- this.removeBgEl.classList.toggle('d-none', !src);
+ this.removeBgWidget.el.classList.toggle('d-none', !src);
if (src) {
var split = src.split('/');
this.editBgTextEl.textContent = split[split.length - 1];
@@ -1563,16 +1969,48 @@ registry.background = SnippetOption.extend({
}
},
/**
+ * @override
+ */
+ _renderCustomWidgets: async function (uiFragment) {
+ // Build option UI controls
+ this.editBgTextEl = document.createElement('span');
+ const iconEl = document.createElement('i');
+ iconEl.classList.add('fa', 'fa-fw', 'fa-edit');
+ const editBgWidget = this._registerUserValueWidget('we-button', this, '', {
+ dataAttributes: {
+ chooseImage: 'true',
+ noPreview: 'true',
+ },
+ childNodes: [this.editBgTextEl, iconEl],
+ });
+ await editBgWidget.appendTo(document.createDocumentFragment());
+
+ this.removeBgWidget = this._registerUserValueWidget('we-button', this, '', {
+ classes: ['fa', 'fa-fw', 'fa-times'],
+ dataAttributes: {
+ background: '',
+ noPreview: 'true',
+ },
+ });
+ await this.removeBgWidget.appendTo(document.createDocumentFragment());
+ this.removeBgWidget.el.title = _t("Remove the background");
+
+ return uiFragment.appendChild(_buildRowElement(this.data.string, {
+ childNodes: [editBgWidget.el, this.removeBgWidget.el],
+ }));
+ },
+ /**
* Sets the given value as custom background image.
*
* @private
* @param {string} value
+ * @returns {Promise}
*/
- _setCustomBackground: function (value) {
+ _setCustomBackground: async function (value) {
this.__customImageSrc = value;
- this.background(false, this.__customImageSrc);
+ this.background(false, this.__customImageSrc, {});
this.$target.toggleClass('oe_custom_bg', !!value);
- this._updateUI();
+ await this._updateUI();
this.$target.trigger('snippet-option-change', [this]);
},
@@ -1607,16 +2045,17 @@ registry.background = SnippetOption.extend({
*
* @private
* @param {Object} data
+ * @returns {Promise}
*/
- _onSaveMediaDialog: function (data) {
- this._setCustomBackground(data.src);
+ _onSaveMediaDialog: async function (data) {
+ await this._setCustomBackground(data.src);
},
});
/**
* Handles the edition of snippets' background image position.
*/
-registry.BackgroundPosition = SnippetOption.extend({
+registry.BackgroundPosition = SnippetOptionWidget.extend({
xmlDependencies: ['/web_editor/static/src/xml/editor.xml'],
/**
@@ -1665,8 +2104,8 @@ registry.BackgroundPosition = SnippetOption.extend({
*
* @see this.selectClass for params
*/
- backgroundType: function (previewMode, value, $opt) {
- this.$target.toggleClass('o_bg_img_opt_repeat', value === 'repeat-pattern');
+ backgroundType: function (previewMode, widgetValue, params) {
+ this.$target.toggleClass('o_bg_img_opt_repeat', widgetValue === 'repeat-pattern');
this.$target.css('background-position', '');
this.$target.css('background-size', '');
},
@@ -1675,7 +2114,7 @@ registry.BackgroundPosition = SnippetOption.extend({
*
* @see this.selectClass for params
*/
- backgroundPositionOverlay: function (previewMode, value, $opt) {
+ backgroundPositionOverlay: function (previewMode, widgetValue, params) {
const position = this.$target.css('background-position').split(' ').map(v => parseInt(v));
// Convert % values to pixels (because mouse movement is in pixels)
const delta = this._getBackgroundDelta();
@@ -1687,12 +2126,33 @@ registry.BackgroundPosition = SnippetOption.extend({
this._toggleBgOverlay(true);
},
+ /**
+ * @override
+ */
+ selectStyle: function (previewMode, widgetValue, params) {
+ if (params.cssProperty === 'background-size'
+ && !this.$target.hasClass('o_bg_img_opt_repeat')) {
+ // Disable the option when the image is in cover mode, otherwise
+ // the background-size: auto style may be forced.
+ return;
+ }
+ this._super(...arguments);
+ },
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
+ * @override
+ */
+ _computeWidgetState: function (methodName, params) {
+ if (methodName === 'backgroundType') {
+ return this.$target.css('background-repeat') === 'repeat' ? 'repeat-pattern' : 'cover';
+ }
+ return this._super(...arguments);
+ },
+ /**
* Initializes the overlay, binds events to the buttons, inserts it in
* the DOM.
*
@@ -1787,17 +2247,14 @@ registry.BackgroundPosition = SnippetOption.extend({
},
/**
* Disables background position if no background image, disables size inputs
- * in cover mode, and activates the proper select option.
+ * in cover mode.
*
* @override
*/
- _setActive: function () {
+ _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');
- this.$el.find('[data-background-type]').removeClass('active')
- .filter(`[data-background-type=${this.$target.css('background-repeat') === 'repeat' ? 'repeat-pattern' : 'cover'}]`).addClass('active');
-
- this._super.apply(this, arguments);
},
/**
* Returns the src value from a css value related to a background image
@@ -1914,7 +2371,7 @@ registry.BackgroundPosition = SnippetOption.extend({
* Allows to replace a text value with the name of a database record.
* @todo replace this mechanism with real backend m2o field ?
*/
-registry.many2one = SnippetOption.extend({
+registry.many2one = SnippetOptionWidget.extend({
xmlDependencies: ['/web_editor/static/src/xml/snippets.xml'],
/**
* @override
@@ -2074,9 +2531,19 @@ registry.many2one = SnippetOption.extend({
});
return {
- Class: SnippetOption,
+ SnippetOptionWidget: SnippetOptionWidget,
+ snippetOptionRegistry: registry,
+
+ UserValueWidget: UserValueWidget,
+ userValueWidgetsRegistry: userValueWidgetsRegistry,
+
+ addTitleAndAllowedAttributes: _addTitleAndAllowedAttributes,
+ buildElement: _buildElement,
+ buildTitleElement: _buildTitleElement,
+ buildRowElement: _buildRowElement,
+
+ // Other names for convenience
+ Class: SnippetOptionWidget,
registry: registry,
- CSS_SHORTHANDS: CSS_SHORTHANDS,
- CSS_UNITS_CONVERSION: CSS_UNITS_CONVERSION,
};
});
diff --git a/addons/web_editor/static/src/scss/wysiwyg_snippets.scss b/addons/web_editor/static/src/scss/wysiwyg_snippets.scss
index f1f69a952d6..404bccc7b53 100644
--- a/addons/web_editor/static/src/scss/wysiwyg_snippets.scss
+++ b/addons/web_editor/static/src/scss/wysiwyg_snippets.scss
@@ -398,6 +398,14 @@ body.editor_enable.editor_has_snippets {
}
}
+ we-row we-input {
+ flex-wrap: wrap;
+
+ > we-title {
+ min-width: 95%;
+ }
+ }
+
.dropdown-menu {
// FIXME temporary fix for m2o option for example
position: static !important;
diff --git a/addons/web_editor/views/editor.xml b/addons/web_editor/views/editor.xml
index e775132ca87..e61833e4e77 100644
--- a/addons/web_editor/views/editor.xml
+++ b/addons/web_editor/views/editor.xml
@@ -131,6 +131,7 @@
<xpath expr="script[last()]" position="after">
<script type="text/javascript" src="/web_editor/static/lib/vkbeautify/vkbeautify.0.99.00.beta.js"/>
<script type="text/javascript" src="/web_editor/static/src/js/common/ace.js"/>
+ <script type="text/javascript" src="/web_editor/static/src/js/common/utils.js"/>
<script type="text/javascript" src="/web_editor/static/src/js/wysiwyg/root.js"/>
</xpath>
</template>
diff --git a/addons/website/static/src/js/content/snippets.animation.js b/addons/website/static/src/js/content/snippets.animation.js
index c42c11b738c..2975e89ae95 100644
--- a/addons/website/static/src/js/content/snippets.animation.js
+++ b/addons/website/static/src/js/content/snippets.animation.js
@@ -881,7 +881,7 @@ registry.gallery = publicWidget.Widget.extend({
srcs: urls,
index: idx,
dim: dimensions,
- interval: milliseconds,
+ interval: milliseconds || 0,
id: _.uniqueId('slideshow_'),
}));
$modal.modal({
diff --git a/addons/website/static/src/js/editor/snippets.options.js b/addons/website/static/src/js/editor/snippets.options.js
index 6edbc840aa4..6eb413558e8 100644
--- a/addons/website/static/src/js/editor/snippets.options.js
+++ b/addons/website/static/src/js/editor/snippets.options.js
@@ -19,14 +19,7 @@ options.Class.include({
/**
* @override
*/
- setDataAttribute: function (previewMode, dataName, $opt) {
- this._super(...arguments);
- this._refreshPublicWidgets();
- },
- /**
- * @override
- */
- selectDataAttribute: function (previewMode, value, $opt) {
+ selectDataAttribute: function (previewMode, widgetValue, params) {
this._super(...arguments);
this._refreshPublicWidgets();
},
@@ -86,19 +79,20 @@ options.registry.background.include({
/**
* @override
*/
- _setActive: function () {
- this._super(...arguments);
- if (this._hasBgvideo()) {
- this.$el.find('[data-choose-image]').addClass('active');
+ _computeWidgetState: function (methodName, params) {
+ if (methodName === 'chooseImage') {
+ return this._hasBgvideo() ? 'true' : '';
}
+ return this._super(...arguments);
},
/**
* Updates the background video used by the snippet.
*
* @private
* @see this.selectClass for parameters
+ * @returns {Promise}
*/
- _setBgVideo: function (previewMode, value) {
+ _setBgVideo: async function (previewMode, value) {
this.$('> .o_bg_video_container').toggleClass('d-none', previewMode === true);
if (previewMode !== false) {
@@ -113,7 +107,7 @@ options.registry.background.include({
delete target.dataset.bgVideoSrc;
}
this._refreshPublicWidgets();
- this._updateUI();
+ await this._updateUI();
},
/**
* Returns whether the current target has a background video or not.
@@ -142,15 +136,16 @@ options.registry.background.include({
/**
* @override
*/
- _onSaveMediaDialog: function (data) {
+ _onSaveMediaDialog: async function (data) {
if (!data.bgVideoSrc) {
- this._setBgVideo(false);
- this._super(...arguments);
- return;
+ const _super = this._super.bind(this);
+ const args = arguments;
+ await this._setBgVideo(false);
+ return _super(...args);
}
// if the user chose a video, only add the video without removing the
// background
- this._setBgVideo(false, data.bgVideoSrc);
+ await this._setBgVideo(false, data.bgVideoSrc);
},
});
@@ -416,8 +411,8 @@ options.registry.CarouselItem = options.Class.extend({
*
* @see this.selectClass for parameters
*/
- slide: function (previewMode, value) {
- switch (value) {
+ slide: function (previewMode, widgetValue, params) {
+ switch (widgetValue) {
case 'left':
this.$controls.filter('.carousel-control-prev')[0].click();
break;
@@ -458,7 +453,7 @@ options.registry.navTabs = options.Class.extend({
*
* @see this.selectClass for parameters
*/
- addTab: function (previewMode, value, $opt) {
+ addTab: function (previewMode, widgetValue, params) {
var $activeItem = this.$navLinks.filter('.active').parent();
var $activePane = this.$tabPanes.filter('.active');
@@ -477,7 +472,7 @@ options.registry.navTabs = options.Class.extend({
*
* @see this.selectClass for parameters
*/
- removeTab: function (previewMode, value, $opt) {
+ removeTab: function (previewMode, widgetValue, params) {
var self = this;
var $activeLink = this.$navLinks.filter('.active');
@@ -531,8 +526,8 @@ options.registry.navTabs = options.Class.extend({
* @private
* @override
*/
- _updateUI: function () {
- this._super.apply(this, arguments);
+ _updateUI: async function () {
+ await this._super.apply(this, arguments);
this.$el.filter('[data-remove-tab]').toggleClass('d-none', this.$tabPanes.length <= 2);
},
});
@@ -607,8 +602,9 @@ options.registry.layout_column = options.Class.extend({
*
* @see this.selectClass for parameters
*/
- selectCount: function (previewMode, value, $opt) {
- this._updateColumnCount(value - this.$target.children().length);
+ selectCount: function (previewMode, widgetValue, params) {
+ const nbColumns = parseInt(widgetValue);
+ this._updateColumnCount(nbColumns - this.$target.children().length);
},
//--------------------------------------------------------------------------
@@ -616,6 +612,15 @@ options.registry.layout_column = options.Class.extend({
//--------------------------------------------------------------------------
/**
+ * @override
+ */
+ _computeWidgetState: function (methodName, params) {
+ if (methodName === 'selectCount') {
+ return '' + this.$target.children().length;
+ }
+ return this._super(...arguments);
+ },
+ /**
* Adds new columns which are clones of the last column or removes the
* last x columns.
*
@@ -664,14 +669,6 @@ options.registry.layout_column = options.Class.extend({
$columns.first().addClass('offset-lg-' + colOffset);
}
},
- /**
- * @override
- */
- _setActive: function () {
- this._super.apply(this, arguments);
- this.$el.find('[data-select-count]').removeClass('active')
- .filter('[data-select-count=' + this.$target.children().length + ']').addClass('active');
- },
});
options.registry.parallax = options.Class.extend({
@@ -931,7 +928,7 @@ options.registry.ul = options.Class.extend({
/**
* @override
*/
- toggleClass: function () {
+ selectClass: function () {
this._super.apply(this, arguments);
this.trigger_up('widgets_stop_request', {
@@ -1121,8 +1118,8 @@ options.registry.gallery = options.Class.extend({
'data-index': ++index,
}).appendTo($container);
}
- self._reset();
- self.trigger_up('cover_update');
+ this.mode('reset', this.getMode());
+ this.trigger_up('cover_update');
this._updateUI();
});
dialog.open();
@@ -1133,11 +1130,11 @@ options.registry.gallery = options.Class.extend({
*
* @see this.selectClass for parameters
*/
- columns: function (previewMode, value) {
- this.$target.attr('data-columns', value);
+ columns: function (previewMode, widgetValue, params) {
+ const nbColumns = parseInt(widgetValue || '1');
+ this.$target.attr('data-columns', nbColumns);
- var $activeMode = this.$el.find('.active[data-mode]');
- this.mode(null, $activeMode.data('mode'), $activeMode);
+ this.mode(previewMode, this.getMode(), {}); // TODO improve
},
/**
* Get the image target's layout mode (slideshow, masonry, grid or nomode).
@@ -1182,8 +1179,8 @@ options.registry.gallery = options.Class.extend({
* Allows to changes the interval of automatic slideshow (not active in
* edit mode).
*/
- interval: function (previewMode, value) {
- this.$target.find('.carousel:first').attr('data-interval', value);
+ interval: function (previewMode, widgetValue) {
+ this.$target.find('.carousel:first').attr('data-interval', widgetValue || '0');
},
/**
* Displays the images with the "masonry" layout.
@@ -1226,12 +1223,13 @@ options.registry.gallery = options.Class.extend({
*
* @see this.selectClass for parameters
*/
- mode: function (previewMode, value, $opt) {
+ mode: function (previewMode, widgetValue, params) {
+ widgetValue = widgetValue || 'slideshow'; // FIXME should not be needed
this.$target.css('height', '');
- this[value]();
+ this[widgetValue]();
this.$target
.removeClass('o_nomode o_masonry o_grid o_slideshow')
- .addClass('o_' + value);
+ .addClass('o_' + widgetValue);
this.trigger_up('cover_update');
},
/**
@@ -1322,11 +1320,11 @@ options.registry.gallery = options.Class.extend({
*
* @override
*/
- notify: function (name, data) {
- this._super.apply(this, arguments);
+ notify: async function (name, data) {
+ await this._super(...arguments);
if (name === 'image_removed') {
data.$image.remove(); // Force the removal of the image before reset
- this._reset();
+ this.mode('reset', this.getMode());
} else if (name === 'image_index_request') {
var imgs = this._getImages();
var position = _.indexOf(imgs, data.$image[0]);
@@ -1351,7 +1349,7 @@ options.registry.gallery = options.Class.extend({
// indexes were not the same as positions.
$(img).attr('data-index', index);
});
- this._reset();
+ this.mode('reset', this.getMode());
}
},
@@ -1360,6 +1358,44 @@ options.registry.gallery = options.Class.extend({
//--------------------------------------------------------------------------
/**
+ * @override
+ */
+ _computeWidgetState: function (methodName, params) {
+ switch (methodName) {
+ case 'mode': {
+ let activeModeName = 'slideshow';
+ for (const modeName of params.possibleValues) {
+ if (this.$target.hasClass(`o_${modeName}`)) {
+ activeModeName = modeName;
+ break;
+ }
+ }
+ this.activeMode = activeModeName;
+ return activeModeName;
+ }
+ case 'interval': {
+ const carousel = this.$target[0].querySelector('.carousel');
+ return (carousel && carousel.dataset.interval || '0');
+ }
+ case 'columns': {
+ return `${this._getColumns()}`;
+ }
+ case 'styling': {
+ const img = this.$target[0].querySelector('img');
+ if (!img) {
+ return '';
+ }
+ for (const className of params.possibleValues) {
+ if (className && img.classList.contains(className)) {
+ return className;
+ }
+ }
+ return '';
+ }
+ }
+ return this._super(...arguments);
+ },
+ /**
* Returns the images, sorted by index.
*
* @private
@@ -1407,57 +1443,17 @@ options.registry.gallery = options.Class.extend({
/**
* @override
*/
- _setActive: function () {
- this._super(...arguments);
+ _updateUI: async function () {
+ await this._super(...arguments);
- var activeModeSelectors = [];
- for (const className of this.$target[0].classList) {
- if (className.startsWith('o_')) {
- activeModeSelectors.push('[data-mode="' + className.substring(2) + '"]');
- }
- }
- var activeMode = this.$el.find('[data-mode]')
- .removeClass('active')
- .filter(activeModeSelectors.join(', '))
- .addClass('active')
- .data('mode');
-
- var carousel = this.$target[0].querySelector('.carousel');
- var activeInterval = carousel ? (carousel.dataset.interval || 0) : undefined;
- var $intervalOptions = this.$el.find('[data-interval]');
- $intervalOptions.removeClass('active')
- .filter('[data-interval="' + activeInterval + '"]')
- .addClass('active');
- $intervalOptions.closest('we-select')[0]
- .classList.toggle('d-none', activeMode !== 'slideshow');
+ this.$el.find('[data-interval]').closest('we-select')[0]
+ .classList.toggle('d-none', this.activeMode !== 'slideshow');
- var columns = this._getColumns();
- var $columnOptions = this.$el.find('[data-columns]');
- $columnOptions.removeClass('active')
- .filter('[data-columns="' + columns + '"]')
- .addClass('active');
- $columnOptions.closest('we-select')[0]
- .classList.toggle('d-none', !(activeMode === 'grid' || activeMode === 'masonry'));
+ 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', activeMode === 'slideshow');
-
- var $stylingOptions = this.$el.find('[data-styling]');
- $stylingOptions.removeClass('active');
- var img = this.$target[0].querySelector('img');
- var activeStyleSelectors = [];
- if (img) {
- for (const className of img.classList) {
- activeStyleSelectors.push('[data-styling="' + className + '"]');
- }
- }
- var $toEnable = activeStyleSelectors.length
- ? $stylingOptions.filter(activeStyleSelectors.join(', '))
- : null;
- if (!$toEnable || !$toEnable.length) {
- $toEnable = $stylingOptions.first();
- }
- $toEnable.addClass('active');
+ .classList.toggle('d-none', this.activeMode === 'slideshow');
},
});
@@ -1486,14 +1482,14 @@ options.registry.gallery_img = options.Class.extend({
*
* @see this.selectClass for parameters
*/
- position: function (previewMode, value) {
+ position: function (previewMode, widgetValue, params) {
this.trigger_up('deactivate_snippet');
this.trigger_up('option_update', {
optionName: 'gallery',
name: 'image_index_request',
data: {
$image: this.$target,
- position: value,
+ position: widgetValue,
},
});
},
@@ -1510,7 +1506,7 @@ options.registry.topMenuTransparency = options.Class.extend({
*
* @see this.selectClass for params
*/
- transparent: function (previewMode, value, $opt) {
+ transparent: function (previewMode, widgetValue, params) {
var self = this;
this.trigger_up('action_demand', {
actionName: 'toggle_page_option',
@@ -1531,16 +1527,17 @@ options.registry.topMenuTransparency = options.Class.extend({
/**
* @override
*/
- _setActive: function () {
- this._super.apply(this, arguments);
-
- this.trigger_up('action_demand', {
- actionName: 'get_page_option',
- params: ['header_overlay'],
- onSuccess: value => {
- this.$el.find('[data-transparent]').toggleClass('active', !!value);
- },
- });
+ _computeWidgetState: function (methodName, params) {
+ if (methodName === 'transparent') {
+ return new Promise(resolve => {
+ this.trigger_up('action_demand', {
+ actionName: 'get_page_option',
+ params: ['header_overlay'],
+ onSuccess: v => resolve(v ? 'true' : ''),
+ });
+ });
+ }
+ return this._super(...arguments);
},
});
@@ -1630,7 +1627,7 @@ options.registry.anchor = options.Class.extend({
/**
* @see this.selectClass for parameters
*/
- openAnchorDialog: function (previewMode, value, $opt) {
+ openAnchorDialog: function (previewMode, widgetValue, params) {
var self = this;
var buttons = [{
text: _t("Save & copy"),
@@ -1757,14 +1754,14 @@ options.registry.CoverProperties = options.Class.extend({
/**
* @see this.selectClass for parameters
*/
- clear: function (previewMode, value, $opt) {
- this.selectClass(previewMode, '', $());
+ clear: function (previewMode, widgetValue, params) {
+ this.$target.removeClass('o_record_has_cover');
this.$image.css('background-image', '');
},
/**
* @see this.selectClass for parameters
*/
- change: function (previewMode, value, $opt) {
+ change: function (previewMode, widgetValue, params) {
var $image = $('<img/>');
var background = this.$image.css('background-image');
if (background && background !== 'none') {
@@ -1780,8 +1777,7 @@ options.registry.CoverProperties = options.Class.extend({
var src = image.src;
this.$image.css('background-image', src ? ('url(' + src + ')') : '');
if (!this.$target.hasClass('o_record_has_cover')) {
- var $opt = this.$el.find('.o_record_cover_opt_size_default[data-select-class]');
- this.selectClass(previewMode, $opt.data('selectClass'), $opt);
+ this.$el.find('.o_record_cover_opt_size_default[data-select-class]').click();
}
this._updateUI();
});
@@ -1789,16 +1785,16 @@ options.registry.CoverProperties = options.Class.extend({
/**
* @see this.selectClass for parameters
*/
- filterValue: function (previewMode, value, $opt) {
- this.$filter.css('opacity', value);
+ filterValue: function (previewMode, widgetValue, params) {
+ this.$filter.css('opacity', widgetValue || 0);
},
/**
* @see this.selectClass for parameters
*/
- filterColor: function (previewMode, value, $opt) {
+ filterColor: function (previewMode, widgetValue, params) {
this.$filter.removeClass(this.filterColorClasses);
- if (value) {
- this.$filter.addClass(value);
+ if (widgetValue) {
+ this.$filter.addClass(widgetValue);
}
var $firstVisibleFilterOpt = this.$filterValueOpts.eq(1);
@@ -1812,12 +1808,32 @@ options.registry.CoverProperties = options.Class.extend({
//--------------------------------------------------------------------------
/**
- * @private
* @override
*/
- _setActive: function () {
- this._super.apply(this, arguments);
+ _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 () {
+ await this._super(...arguments);
+ // Only show options which are useful to the current cover
_.each(this.$el.children(), el => {
var $el = $(el);
@@ -1834,27 +1850,14 @@ options.registry.CoverProperties = options.Class.extend({
});
}
});
-
this.$el.find('[data-clear]').toggleClass('d-none', !this.$target.hasClass('o_record_has_cover'));
- this.$filterValueOpts.removeClass('active');
- this.$filterColorOpts.removeClass('active');
-
- var activeFilterValue = this.$filterValueOpts
- .filter((i, el) => {
- return (parseFloat($(el).data('filterValue')).toFixed(1) === parseFloat(this.$filter.css('opacity')).toFixed(1));
- }).addClass('active').data('filterValue');
-
- var activeFilterColor = this.$filterColorOpts
- .filter((i, el) => {
- return this.$filter.hasClass($(el).data('filterColor'));
- }).addClass('active').data('filterColor');
-
+ // Update saving dataset
this.$target[0].dataset.coverClass = this.$el.find('.active[data-cover-opt="size"]').data('selectClass') || '';
this.$target[0].dataset.textSizeClass = this.$el.find('.active[data-cover-opt="text_size"]').data('selectClass') || '';
this.$target[0].dataset.textAlignClass = this.$el.find('.active[data-cover-opt="text_align"]').data('selectClass') || '';
- this.$target[0].dataset.filterValue = activeFilterValue || 0.0;
- this.$target[0].dataset.filterColor = activeFilterColor || '';
+ this.$target[0].dataset.filterValue = this.$filterValueOpts.filter('.active').data('filterValue') || 0.0;
+ this.$target[0].dataset.filterColor = this.$filterColorOpts.filter('.active').data('filterColor') || '';
},
});
@@ -1878,35 +1881,6 @@ options.registry.SectionStretch = options.Class.extend({
return this._super.apply(this, arguments);
},
-
- //--------------------------------------------------------------------------
- // Options
- //--------------------------------------------------------------------------
-
- /**
- * @see this.selectClass for parameters
- */
- toggleContainerFluid: function (previewMode, value, $opt) {
- var isFluid = this.$el.find('[data-toggle-container-fluid]').hasClass('active');
- this.$target.toggleClass('container', !isFluid)
- .toggleClass('container-fluid', isFluid);
- if (previewMode !== 'reset') {
- this.$target.toggleClass('container').toggleClass('container-fluid');
- }
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * @private
- */
- _setActive: function () {
- this._super.apply(this, arguments);
- var isFluid = this.$target.hasClass('container-fluid');
- this.$el.find('[data-toggle-container-fluid]').toggleClass('active', isFluid);
- },
});
/**
@@ -1922,10 +1896,6 @@ options.registry.SnippetMove = options.Class.extend({
$overlayArea.prepend($buttons[0]);
$overlayArea.append($buttons[1]);
- // TODO this is kinda hack but not worth a complex system while no
- // other use case is implemented.
- $buttons.on('click', this._onOptionSelection.bind(this));
-
return this._super(...arguments);
},
@@ -1938,8 +1908,8 @@ options.registry.SnippetMove = options.Class.extend({
*
* @see this.selectClass for parameters
*/
- moveSnippet: function (previewMode, value, $opt) {
- switch (value) {
+ moveSnippet: function (previewMode, widgetValue, params) {
+ switch (widgetValue) {
case 'prev':
this.$target.prev().before(this.$target);
break;
diff --git a/addons/website/views/snippets.xml b/addons/website/views/snippets.xml
index c8c89c0bebe..a4caf8333c3 100644
--- a/addons/website/views/snippets.xml
+++ b/addons/website/views/snippets.xml
@@ -1108,7 +1108,7 @@
<we-divider/>
<we-button data-select-class="s_carousel_default">Default</we-button>
</we-select>
- <we-input string="Speed" data-set-data-attribute="interval" data-unit="s" data-save-unit="ms" data-step="0.1"/>
+ <we-input string="Speed" data-select-data-attribute="0s" data-attribute-name="interval" data-unit="s" data-save-unit="ms" data-step="0.1"/>
</div>
<div data-js="CarouselItem"
@@ -1238,8 +1238,10 @@
<we-button class="fas fa-fw fa-expand-arrows-alt" title="Background Position"
data-background-position-overlay="true" data-no-preview="true"/>
</we-row>
- <we-input string="Background width" data-set-style="background-size" data-default-value="auto"/>
- <we-input string="Background height" data-set-style="background-size" data-default-value="auto"/>
+ <we-multi data-css-property="background-size">
+ <we-input string="Width" data-select-style="auto" data-unit="px"/>
+ <we-input string="Height" data-select-style="auto" data-unit="px"/>
+ </we-multi>
</div>
<!-- Parallax -->
@@ -1275,9 +1277,9 @@
<div data-selector="section .row > div"
data-exclude=".s_col_no_bgcolor, .s_col_no_bgcolor.row > div, .o_gallery .row > div">
- <we-input string="Border" data-set-style="border-width" data-extra-class="border"/>
- <we-input string="Rounded" data-set-style="border-radius" data-extra-class="rounded"/>
- <we-checkbox string="Shadow" data-toggle-class="shadow"/>
+ <we-input string="Border" data-select-style="0" data-css-property="border-width" data-unit="px" data-extra-class="border"/>
+ <we-input string="Rounded" data-select-style="0" data-css-property="border-radius" data-unit="px" data-extra-class="rounded"/>
+ <we-checkbox string="Shadow" data-select-class="shadow"/>
</div>
<div data-js="sizing_y"
@@ -1301,7 +1303,7 @@
<div data-js="ul"
data-selector=":not(li) > ul:has(ul,ol), :not(li) > ol:has(ul,ol)">
- <we-checkbox string="Folded list" data-toggle-class="o_ul_folded"/>
+ <we-checkbox string="Folded list" data-select-class="o_ul_folded"/>
</div>
<div data-js="menu_data"
@@ -1391,7 +1393,7 @@
<div data-js="SectionStretch"
data-selector="section"
data-target="> .container, > .container-fluid">
- <we-checkbox string="Stretch" data-toggle-container-fluid=""/>
+ <we-checkbox string="Stretch" data-select-class="container|container-fluid" data-no-preview="true"/>
</div>
</template>
</odoo>
diff --git a/addons/website_blog/static/src/js/s_latest_posts_editor.js b/addons/website_blog/static/src/js/s_latest_posts_editor.js
index 6000b2d27a7..af6235f2e98 100644
--- a/addons/website_blog/static/src/js/s_latest_posts_editor.js
+++ b/addons/website_blog/static/src/js/s_latest_posts_editor.js
@@ -5,59 +5,29 @@ var sOptions = require('web_editor.snippets.options');
var wUtils = require('website.utils');
sOptions.registry.js_get_posts_selectBlog = sOptions.Class.extend({
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
/**
* @override
*/
- start: function () {
- var def = this._rpc({
+ _renderCustomXML: function (uiFragment) {
+ return this._rpc({
model: 'blog.blog',
method: 'search_read',
args: [wUtils.websiteDomain(this), ['name']],
}).then(blogs => {
- var allBlogsEl = this.el.querySelector('[data-filter-by-blog-id="0"]');
- var menuEl = allBlogsEl.parentNode;
+ const menuEl = uiFragment.querySelector('[name="blog_selection"]');
for (const blog of blogs) {
- let el = allBlogsEl.cloneNode();
- el.dataset.filterByBlogId = blog.id;
+ const el = document.createElement('we-button');
+ el.dataset.selectDataAttribute = blog.id;
el.textContent = blog.name;
menuEl.appendChild(el);
}
- this._updateUI();
+ return this._updateUI();
});
-
- return Promise.all([this._super.apply(this, arguments), def]);
- },
-
- //--------------------------------------------------------------------------
- // Options
- //--------------------------------------------------------------------------
-
- /**
- * @see this.selectClass for parameters
- */
- filterByBlogId: function (previewMode, value, $opt) {
- value = parseInt(value);
- this.$target.attr('data-filter-by-blog-id', value).data('filterByBlogId', value);
- this.trigger_up('widgets_start_request', {
- editableMode: true,
- $target: this.$target,
- });
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * @override
- */
- _setActive: function () {
- this._super.apply(this, arguments);
-
- var activeBlogId = this.$target.data('filterByBlogId') || 0;
-
- this.$el.find('[data-filter-by-blog-id]').removeClass('active');
- this.$el.find('[data-filter-by-blog-id=' + activeBlogId + ']').addClass('active');
},
});
});
diff --git a/addons/website_blog/static/src/js/website_blog.editor.js b/addons/website_blog/static/src/js/website_blog.editor.js
index 4e2c136b0f1..a066451e764 100644
--- a/addons/website_blog/static/src/js/website_blog.editor.js
+++ b/addons/website_blog/static/src/js/website_blog.editor.js
@@ -111,8 +111,8 @@ options.registry.CoverProperties.include({
/**
* @override
*/
- _updateUI: function () {
- this._super(...arguments);
+ _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"]');
var $coverMid = this.$el.find('[data-select-class*="cover_mid"]');
diff --git a/addons/website_blog/views/snippets.xml b/addons/website_blog/views/snippets.xml
index 388bf6e0fbb..3fe49ad3912 100644
--- a/addons/website_blog/views/snippets.xml
+++ b/addons/website_blog/views/snippets.xml
@@ -11,8 +11,8 @@
<template id="s_latest_posts_options" inherit_id="website.snippet_options">
<xpath expr="//div[@data-js='background']" position="before">
<div data-js="js_get_posts_selectBlog" data-selector=".s_latest_posts, .s_latest_posts_big_picture" data-target=".js_get_posts" data-no-check="true">
- <we-select string="Choose a blog" data-no-preview="true">
- <we-button data-filter-by-blog-id="0">All blogs</we-button>
+ <we-select string="Choose a blog" data-no-preview="true" name="blog_selection" data-attribute-name="filterByBlogId">
+ <we-button data-select-data-attribute="0">All blogs</we-button>
<!-- the blog list will be generated in js -->
</we-select>
</div>
diff --git a/addons/website_form/static/src/js/website_form_editor.js b/addons/website_form/static/src/js/website_form_editor.js
index b3960224e04..84545628a16 100644
--- a/addons/website_form/static/src/js/website_form_editor.js
+++ b/addons/website_form/static/src/js/website_form_editor.js
@@ -91,7 +91,7 @@ odoo.define('website_form_editor', function (require) {
},
// Choose a model modal
- website_form_model_modal: function (previewMode, value, $li) {
+ website_form_model_modal: function (previewMode, widgetValue, params) {
var self = this;
this._rpc({
model: "ir.model",
@@ -224,7 +224,7 @@ odoo.define('website_form_editor', function (require) {
},
// Choose a field modal
- website_form_field_modal: function (previewMode, value, $li) {
+ website_form_field_modal: function (previewMode, widgetValue, params) {
var self = this;
this.fields().then(function (fields) {
@@ -261,13 +261,13 @@ odoo.define('website_form_editor', function (require) {
},
// Create a custom field
- website_form_custom_field: function (previewMode, value, $li) {
- var default_field_name = 'Custom ' + $li.text();
+ website_form_custom_field: function (previewMode, widgetValue, params) {
+ var default_field_name = 'Custom ' + this.el.querySelector(`[data-website_form_custom_field="${widgetValue}"]`).textContent; // TODO improve
this.append_field({
name: default_field_name,
string: default_field_name,
custom: true,
- type: value,
+ type: widgetValue,
// Default values for x2many fields
records: [
{
@@ -302,7 +302,7 @@ odoo.define('website_form_editor', function (require) {
},
// Re-render the field and replace the current one
- // website_form_editor_field_reset: function(previewMode, value, $li) {
+ // website_form_editor_field_reset: function(previewMode, widgetValue, params) {
// var self = this;
// var target_field_name = this.$target.find('.col-form-label').attr('for');
// this.fields().then(function(fields){
@@ -476,9 +476,9 @@ odoo.define('website_form_editor', function (require) {
/**
* @see this.selectClass for parameters
*/
- website_form_choice_field_display: function (previewMode, value, $opt) {
- this.$target.toggleClass('o_website_form_flex_fw', value === 'vertical');
- this.$target[0].dataset.display = value;
+ website_form_choice_field_display: function (previewMode, widgetValue, params) {
+ this.$target.toggleClass('o_website_form_flex_fw', widgetValue === 'vertical');
+ this.$target[0].dataset.display = widgetValue;
},
//----------------------------------------------------------------------
@@ -488,12 +488,11 @@ odoo.define('website_form_editor', function (require) {
/**
* @override
*/
- _setActive: function () {
- this._super(...arguments);
- this.$el.find('[data-website_form_choice_field_display]')
- .removeClass('active')
- .filter('[data-website_form_choice_field_display=' + this.$target.attr('data-display') + ']')
- .addClass('active');
+ _computeWidgetState: function (methodName, params) {
+ if (methodName === 'website_form_choice_field_display') {
+ return this.$target.attr('data-display');
+ }
+ return this._super(...arguments);
},
});
@@ -502,7 +501,7 @@ odoo.define('website_form_editor', function (require) {
xmlDependencies: ['/website_form/static/src/xml/website_form_editor.xml'],
// Option to toggle inputs required attribute
- website_form_field_require: function (previewMode, value, $li) {
+ website_form_field_require: function (previewMode, widgetValue, params) {
this.$target.find('.o_website_form_input').each(function (index, input) {
input.required = !input.required;
});
diff --git a/addons/website_form/static/tests/tours/website_form_editor.js b/addons/website_form/static/tests/tours/website_form_editor.js
index 4d28b45ab1b..ceb5f9a4efb 100644
--- a/addons/website_form/static/tests/tours/website_form_editor.js
+++ b/addons/website_form/static/tests/tours/website_form_editor.js
@@ -115,7 +115,7 @@ odoo.define('website_form_editor.tour', function(require) {
},
{
content: "Click on Hidden",
- trigger: "[data-toggle-class='o_website_form_field_hidden']"
+ trigger: "[data-select-class='o_website_form_field_hidden']"
},
{
content: "Check the resulting field",
diff --git a/addons/website_form/views/snippets.xml b/addons/website_form/views/snippets.xml
index e558f6c5eab..0b4ef73b623 100644
--- a/addons/website_form/views/snippets.xml
+++ b/addons/website_form/views/snippets.xml
@@ -51,7 +51,7 @@
<!-- Field -->
<div data-js='website_form_editor' data-selector=".form-field" data-drop-near=".form-field">
<we-checkbox string="Hidden" data-no-preview="true"
- data-toggle-class="o_website_form_field_hidden"/>
+ data-select-class="o_website_form_field_hidden"/>
</div>
<!-- Add an option to display radio buttons / checkboxes horizontally or vertically -->
@@ -72,7 +72,7 @@
<div data-js='website_form_editor_field' data-selector=".form-field:not(.o_website_form_required)">
<we-checkbox string="Required" data-no-preview="true"
data-website_form_field_require=""
- data-toggle-class="o_website_form_required_custom"/>
+ data-select-class="o_website_form_required_custom"/>
</div>
<!-- Remove the duplicate options of model fields -->
diff --git a/addons/website_sale/static/src/js/website_sale.editor.js b/addons/website_sale/static/src/js/website_sale.editor.js
index 4f35b1bf00e..bbcca2baf74 100644
--- a/addons/website_sale/static/src/js/website_sale.editor.js
+++ b/addons/website_sale/static/src/js/website_sale.editor.js
@@ -113,8 +113,8 @@ options.registry.WebsiteSaleGridLayout = options.Class.extend({
/**
* @see this.selectClass for params
*/
- setPpg: function (previewMode, value, $opt) {
- const ppg = parseInt($opt.find('input').val());
+ setPpg: function (previewMode, widgetValue, params) {
+ const ppg = parseInt(widgetValue);
if (!ppg || ppg < 1) {
return false;
}
@@ -129,12 +129,12 @@ options.registry.WebsiteSaleGridLayout = options.Class.extend({
/**
* @see this.selectClass for params
*/
- setPpr: function (previewMode, value, $opt) {
- this.ppr = parseInt(value);
+ setPpr: function (previewMode, widgetValue, params) {
+ this.ppr = parseInt(widgetValue);
this._rpc({
route: '/shop/change_ppr',
params: {
- 'ppr': value,
+ 'ppr': this.ppr,
},
}).then(reload);
},
@@ -146,18 +146,16 @@ options.registry.WebsiteSaleGridLayout = options.Class.extend({
/**
* @override
*/
- _setActive: function () {
- this._super(...arguments);
-
- this.$el.find('[data-set-ppr]')
- .removeClass('active')
- .filter((i, el) => {
- var nbColumns = parseInt($(el).data('setPpr'));
- return nbColumns === this.ppr;
- })
- .addClass('active');
-
- this.$el.find('[data-set-ppg] > input').val(this.ppg);
+ _computeWidgetState: function (methodName, params) {
+ switch (methodName) {
+ case 'setPpg': {
+ return this.ppg;
+ }
+ case 'setPpr': {
+ return this.ppr;
+ }
+ }
+ return this._super(...arguments);
},
});
@@ -172,29 +170,11 @@ options.registry.WebsiteSaleProductsItem = options.Class.extend({
/**
* @override
*/
- start: function () {
+ willStart: function () {
this.ppr = this.$target.closest('[data-ppr]').data('ppr');
this.productTemplateID = parseInt(this.$target.find('[data-oe-model="product.template"]').data('oe-id'));
- var defs = [this._super.apply(this, arguments)];
-
- defs.push(this._rpc({
- model: 'product.style',
- method: 'search_read',
- }).then(data => {
- var $menu = this.$el.find('[name="style"]');
- for (var k in data) {
- $menu.append(this._buildCheckboxElement(data[k]['name'], {
- dataAttributes: {
- 'style': data[k]['id'],
- 'toggleClass': data[k]['html_class'],
- },
- }));
- }
- this._updateUI();
- }));
-
- return $.when.apply($, defs);
+ return this._super(...arguments);
},
/**
* @override
@@ -212,24 +192,24 @@ options.registry.WebsiteSaleProductsItem = options.Class.extend({
/**
* @see this.selectClass for params
*/
- style: function (previewMode, value, $opt) {
+ style: function (previewMode, widgetValue, params) {
this._rpc({
route: '/shop/change_styles',
params: {
'id': this.productTemplateID,
- 'style_id': value,
+ 'style_id': params.possibleValues[params.possibleValues.length - 1],
},
});
},
/**
* @see this.selectClass for params
*/
- changeSequence: function (previewMode, value, $opt) {
+ changeSequence: function (previewMode, widgetValue, params) {
this._rpc({
route: '/shop/change_sequence',
params: {
id: this.productTemplateID,
- sequence: value,
+ sequence: widgetValue,
},
}).then(reload);
},
@@ -241,8 +221,34 @@ options.registry.WebsiteSaleProductsItem = options.Class.extend({
/**
* @override
*/
- _updateUI: function () {
- this._super.apply(this, arguments);
+ _renderCustomWidgets: async function (uiFragment) {
+ const checkboxes = [];
+ return this._rpc({
+ model: 'product.style',
+ method: 'search_read',
+ }).then(async data => {
+ for (var k in data) {
+ const checkboxWidget = this._registerUserValueWidget('we-checkbox', this, data[k]['name'], {
+ dataAttributes: {
+ 'style': data[k]['id'],
+ 'selectClass': data[k]['html_class'],
+ },
+ });
+ checkboxes.push(checkboxWidget);
+ await checkboxWidget.appendTo(document.createDocumentFragment());
+ }
+ }).then(() => {
+ const menuEl = uiFragment.querySelector('[name="style"]');
+ for (const checkbox of checkboxes) {
+ menuEl.appendChild(checkbox.el);
+ }
+ });
+ },
+ /**
+ * @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);
@@ -277,8 +283,8 @@ options.registry.WebsiteSaleProductsItem = options.Class.extend({
_onTableItemMouseEnter: function (ev) {
var $td = $(ev.currentTarget);
var $table = $td.closest("table");
- var x = $td.index()+1;
- var y = $td.parent().index()+1;
+ var x = $td.index() + 1;
+ var y = $td.parent().index() + 1;
var tr = [];
for (var yi = 0; yi < y; yi++) {
@@ -334,7 +340,7 @@ options.registry.ProductsSearchBar = options.Class.extend({
/**
* @see this.selectClass for parameters
*/
- openSearchbarSettings: function (previewMode, value, $opt) {
+ openSearchbarSettings: function (previewMode, widgetValue, params) {
var self = this;
new Dialog(this, {
title: _t("Products Search Bar"),
diff --git a/addons/website_sale/views/snippets.xml b/addons/website_sale/views/snippets.xml
index 0d6303a2a39..57034d96e89 100644
--- a/addons/website_sale/views/snippets.xml
+++ b/addons/website_sale/views/snippets.xml
@@ -60,7 +60,7 @@
<div data-js="WebsiteSaleGridLayout"
data-selector="#products_grid .o_wsale_products_grid_table_wrapper > table"
data-no-check="true">
- <we-input string="Number of products" data-set-ppg="" data-unit="" data-no-preview="true"/>
+ <we-input string="Number of products" data-set-ppg="" data-no-preview="true"/>
<we-select string="Number of Columns" class="o_wsale_ppr_submenu" data-no-preview="true">
<we-button data-set-ppr="2">2</we-button>
<we-button data-set-ppr="3">3</we-button>