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 7ce827f485f..6ca6263fbe2 100644 --- a/addons/web_editor/static/src/js/editor/snippets.editor.js +++ b/addons/web_editor/static/src/js/editor/snippets.editor.js @@ -25,6 +25,8 @@ var SnippetEditor = Widget.extend({ xmlDependencies: ['/web_editor/static/src/xml/snippets.xml'], custom_events: { 'option_update': '_onOptionUpdate', + 'user_value_widget_request': '_onUserValueWidgetRequest', + 'snippet_option_update': '_onSnippetOptionUpdate', }, /** @@ -676,6 +678,44 @@ var SnippetEditor = Widget.extend({ this.trigger_up('request_history_undo_record', {$target: this.$target}); this.removeSnippet(); }, + /** + * @private + * @param {OdooEvent} + */ + _onSnippetOptionUpdate: async function (ev) { + if (ev.data.previewMode) { + ev.data.onSuccess(); + return; + } + + const proms1 = Object.keys(this.styles).map(key => { + return this.styles[key].updateUI({ + forced: ev.data.widget, + noVisibility: true, + }); + }); + await Promise.all(proms1); + + const proms2 = Object.keys(this.styles).map(key => { + return this.styles[key].updateUIVisibility(); + }); + await Promise.all(proms2); + + ev.data.onSuccess(); + }, + /** + * @private + * @param {Event} ev + */ + _onUserValueWidgetRequest: function (ev) { + for (const key of Object.keys(this.styles)) { + const widget = this.styles[key].findWidget(ev.data.name); + if (widget) { + ev.data.onSuccess(widget); + 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 812cd4bd7fa..4a465ffe3df 100644 --- a/addons/web_editor/static/src/js/editor/snippets.options.js +++ b/addons/web_editor/static/src/js/editor/snippets.options.js @@ -148,6 +148,22 @@ const UserValueWidget = Widget.extend({ close: function () { this._userValueWidgets.forEach(widget => widget.close()); }, + /** + * @param {string} name + * @returns {UserValueWidget|null} + */ + findWidget: function (name) { + for (const widget of this._userValueWidgets) { + if (widget.getName() === name) { + return widget; + } + const depWidget = widget.findWidget(name); + if (depWidget) { + return depWidget; + } + } + return null; + }, /** * Returns the value that the widget would hold if it was active, by default * the internal value it holds. @@ -169,6 +185,12 @@ const UserValueWidget = Widget.extend({ const possibleValues = this._methodsParams.optionsPossibleValues[methodName]; return possibleValues && possibleValues[0] || ''; }, + /** + * @returns {string[]} + */ + getDependencies: function () { + return this._dependencies; + }, /** * Returns the names of the option methods associated to the widget. Those * are loaded with @see loadMethodsData. @@ -194,6 +216,12 @@ const UserValueWidget = Widget.extend({ } return params; }, + /** + * @returns {string} empty string if no name is used by the widget + */ + getName: function () { + return this._methodsParams.name || ''; + }, /** * 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 @@ -254,11 +282,14 @@ const UserValueWidget = Widget.extend({ this._methodsNames = []; this._methodsParams = _.extend({}, extraParams); this._methodsParams.optionsPossibleValues = {}; + this._dependencies = []; for (const key in this.el.dataset) { const dataValue = this.el.dataset[key].trim(); - if (validMethodNames.includes(key)) { + if (key === 'dependencies') { + this._dependencies.push(...dataValue.split(/\s*,\s*/g)); + } else if (validMethodNames.includes(key)) { this._methodsNames.push(key); this._methodsParams.optionsPossibleValues[key] = dataValue.split(/\s*\|\s*/g); } else { @@ -313,6 +344,12 @@ const UserValueWidget = Widget.extend({ setValue: function (value, methodName) { this._value = value; }, + /** + * @param {boolean} show + */ + toggleVisibility: function (show) { + this.el.classList.toggle('d-none', !show); + }, /** * Updates the UI to match the user value the widget currently holds, only * if the UI can currently be updated. @@ -1427,6 +1464,22 @@ const SnippetOptionWidget = Widget.extend({ closeWidgets: function () { this._userValueWidgets.forEach(widget => widget.close()); }, + /** + * @param {string} name + * @returns {UserValueWidget|null} + */ + findWidget: function (name) { + for (const widget of this._userValueWidgets) { + if (widget.getName() === name) { + return widget; + } + const depWidget = widget.findWidget(name); + if (depWidget) { + return depWidget; + } + } + return null; + }, /** * Sometimes, options may need to notify other options, even in parent * editors. This can be done thanks to the 'option_update' event, which @@ -1453,16 +1506,18 @@ const SnippetOptionWidget = Widget.extend({ setTarget: async function ($target) { this.$target = $target; await this.updateUI(); - this.$target.trigger('snippet-option-change', [this]); }, /** * Updates the UI. For widget update, @see _computeWidgetState. * * @param {UserValueWidget} [forced=null] * Only non-previewed widgets are updated, except for the one given here + * @param {boolean} [noVisibility=false] + * If true, only update widget values and their UI, not their visibility + * -> @see updateUIVisibility for toggling visibility only * @returns {Promise} */ - updateUI: async function (forced) { + updateUI: async function ({forced, noVisibility} = {}) { // 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. @@ -1494,6 +1549,49 @@ const SnippetOptionWidget = Widget.extend({ // hold have been updated). return widget.updateUI(widget === forced); }); + + if (!noVisibility) { + await this.updateUIVisibility(); + } + + return Promise.all(proms); + }, + /** + * Updates the UI visibility - @see _computeVisibility. For widget update, + * @see _computeWidgetVisibility. + * + * @returns {Promise} + */ + updateUIVisibility: async function () { + const proms = this._userValueWidgets.map(async widget => { + const params = widget.getMethodsParams(); + const show = await this._computeWidgetVisibility(widget.getName(), params); + if (!show) { + widget.toggleVisibility(false); + return; + } + + const dependencies = widget.getDependencies(); + const dependenciesOK = !dependencies.length || dependencies.some(depName => { + const toBeActive = (depName[0] !== '!'); + if (!toBeActive) { + depName = depName.substr(1); + } + + let widget = null; + this.trigger_up('user_value_widget_request', { + name: depName, + onSuccess: _widget => widget = _widget, + }); + return widget && (widget.isActive() === toBeActive); + }); + + widget.toggleVisibility(dependenciesOK); + }); + + const showUI = await this._computeVisibility(); + this.el.classList.toggle('d-none', !showUI); + return Promise.all(proms); }, @@ -1501,6 +1599,13 @@ const SnippetOptionWidget = Widget.extend({ // Private //-------------------------------------------------------------------------- + /** + * @private + * @returns {Promise|boolean} + */ + _computeVisibility: async function () { + return true; + }, /** * Returns the string value that should be hold by the widget which is * related to the given method name. @@ -1565,6 +1670,15 @@ const SnippetOptionWidget = Widget.extend({ } } }, + /** + * @private + * @param {string} widgetName + * @param {Object} params + * @returns {Promise|boolean} + */ + _computeWidgetVisibility: async function (widgetName, params) { + return true; + }, /** * @private * @param {HTMLElement} el @@ -1598,25 +1712,25 @@ const SnippetOptionWidget = Widget.extend({ return; } + // If it is not preview mode, the user selected the option for good + // (so record the action) + if (!previewMode) { + this.trigger_up('request_history_undo_record', {$target: this.$target}); + } + // Call widget option methods and update $target await this._select(previewMode, widget); - - // Trigger the related events notifying snippet changes - let evName = 'snippet-option-reset'; - if (previewMode === false) { - evName = 'snippet-option-change'; - } else if (previewMode === true) { - evName = 'snippet-option-preview'; - } - this.$target.trigger(evName, [this]); this.$target.trigger('content_changed'); - // Update the UI of the correct widgets - if (!previewMode) { - await this.updateUI(widget); - } else { - await this.updateUI(); - } + await new Promise(resolve => { + // Will update the UI of the correct widgets for all options + // related to the same $target/editor if necessary + this.trigger_up('snippet_option_update', { + widget: widget, + previewMode: previewMode, + onSuccess: () => resolve(), + }); + }); }}); }, /** @@ -1754,12 +1868,6 @@ const SnippetOptionWidget = Widget.extend({ * @returns {Promise} */ _select: async function (previewMode, widget) { - // If it is not preview mode, the user selected the option for good - // (so record the action) - if (!previewMode) { - this.trigger_up('request_history_undo_record', {$target: this.$target}); - } - // Call each option method sequentially for (const methodName of widget.getMethodsNames()) { const widgetValue = widget.getValue(methodName); @@ -2099,7 +2207,7 @@ registry.background = SnippetOptionWidget.extend({ if (previewMode === 'reset') { // No background has been selected and we want to reset back to the // original custom image - await this._setCustomBackground(this.__customImageSrc); + await this._setCustomBackground(this.__customImageSrc, previewMode); return; } @@ -2120,16 +2228,21 @@ registry.background = SnippetOptionWidget.extend({ 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).then(() => { - this.$target.trigger('content_changed'); + return new Promise(resolve => { + const _editor = new weWidgets.MediaDialog(this, options, media).open(); + let _saving = false; + _editor.on('save', this, data => { + _saving = true; + this._onSaveMediaDialog(data).then(() => resolve()); + }); + _editor.on('closed', this, () => { + if (media.classList.contains('o_we_fake_image')) { + media.parentNode.removeChild(media); + } + if (!_saving) { + resolve(); + } }); - }); - _editor.on('closed', this, () => { - if (media.classList.contains('o_we_fake_image')) { - media.parentNode.removeChild(media); - } }); }, @@ -2163,7 +2276,6 @@ registry.background = SnippetOptionWidget.extend({ updateUI: async function () { await this._super(...arguments); var src = this._getSrcFromCssValue(); - this.removeBgWidget.el.classList.toggle('d-none', !src); if (src) { var split = src.split('/'); this.editBgTextEl.textContent = split[split.length - 1]; @@ -2171,6 +2283,14 @@ registry.background = SnippetOptionWidget.extend({ this.editBgTextEl.textContent = this._getDefaultTextContent(); } }, + /** + * @override + */ + updateUIVisibility: async function () { + await this._super(...arguments); + var src = this._getSrcFromCssValue(); + this.removeBgWidget.el.classList.toggle('d-none', !src); + }, //-------------------------------------------------------------------------- // Private @@ -2267,12 +2387,18 @@ registry.background = SnippetOptionWidget.extend({ * @param {string} value * @returns {Promise} */ - _setCustomBackground: async function (value) { + _setCustomBackground: async function (value, previewMode) { this.__customImageSrc = value; this.background(false, this.__customImageSrc, {}); this.$target.toggleClass('oe_custom_bg', !!value); - await this.updateUI(); - this.$target.trigger('snippet-option-change', [this]); + await new Promise(resolve => { + // Will update the UI of the correct widgets for all options + // related to the same $target/editor + this.trigger_up('snippet_option_update', { + previewMode: previewMode, + onSuccess: () => resolve(), + }); + }); }, //-------------------------------------------------------------------------- @@ -2326,15 +2452,6 @@ registry.BackgroundPosition = SnippetOptionWidget.extend({ this._super.apply(this, arguments); this._initOverlay(); - this.img = document.createElement('img'); - this.img.src = this._getSrcFromCssValue(); - - this.$target.on('snippet-option-change', () => { - // Hides option if the bg image is removed in favor of a bg color - this.updateUI(); - // this.img is used to compute dragging speed - this.img.src = this._getSrcFromCssValue(); - }); // Resize overlay content on window resize because background images // change size, and on carousel slide because they sometimes take up @@ -2369,7 +2486,14 @@ registry.BackgroundPosition = SnippetOptionWidget.extend({ * * @see this.selectClass for params */ - backgroundPositionOverlay: function (previewMode, widgetValue, params) { + backgroundPositionOverlay: async function (previewMode, widgetValue, params) { + // Updates the internal image + await new Promise(resolve => { + this.img = document.createElement('img'); + this.img.addEventListener('load', () => resolve()); + this.img.src = this._getSrcFromCssValue(); + }); + 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(); @@ -2394,25 +2518,16 @@ registry.BackgroundPosition = SnippetOptionWidget.extend({ this._super(...arguments); }, - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - /** - * Disables background position if no background image, disables size inputs - * in cover mode. - * - * @override - */ - updateUI: async function () { - await this._super(...arguments); - this.$el.toggleClass('d-none', this.$target.css('background-image') === 'none'); - this.$el.find('we-input').toggleClass('d-none', this.$target.css('background-repeat') !== 'repeat'); - }, - //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- + /** + * @override + */ + _computeVisibility: function () { + return this._super(...arguments) && (this.$target.css('background-image') !== 'none'); + }, /** * @override */ diff --git a/addons/website/static/src/js/editor/snippets.options.js b/addons/website/static/src/js/editor/snippets.options.js index 1b6329a259a..14005618d11 100644 --- a/addons/website/static/src/js/editor/snippets.options.js +++ b/addons/website/static/src/js/editor/snippets.options.js @@ -503,21 +503,18 @@ options.registry.navTabs = options.Class.extend({ }, //-------------------------------------------------------------------------- - // Public + // Private //-------------------------------------------------------------------------- /** * @override */ - updateUI: async function () { - await this._super(...arguments); - this.$el.filter('[data-remove-tab]').toggleClass('d-none', this.$tabPanes.length <= 2); + _computeWidgetVisibility: async function (widgetName, params) { + if (widgetName === 'remove_tab_opt') { + return (this.$tabPanes.length > 2); + } + return this._super(...arguments); }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - /** * @private */ @@ -1359,21 +1356,6 @@ options.registry.gallery = options.Class.extend({ this.mode('reset', this.getMode()); } }, - /** - * @override - */ - updateUI: async function () { - await this._super(...arguments); - - this.$el.find('[data-interval]').closest('we-select')[0] - .classList.toggle('d-none', this.activeMode !== 'slideshow'); - - this.$el.find('[data-columns]').closest('we-select')[0] - .classList.toggle('d-none', !(this.activeMode === 'grid' || this.activeMode === 'masonry')); - - this.el.querySelector('.o_w_image_spacing_option') - .classList.toggle('d-none', this.activeMode === 'slideshow'); - }, //-------------------------------------------------------------------------- // Private @@ -1527,28 +1509,16 @@ options.registry.countdown = options.Class.extend({ /** * @override */ - updateUI: async function () { + updateUIVisibility: async function () { await this._super(...arguments); const dataset = this.$target[0].dataset; // End Action UI - this.$el.find('[data-attribute-name="redirectUrl"]') - .toggleClass('d-none', dataset.endAction !== 'redirect'); this.$el.find('.toggle-edit-message') .toggleClass('d-none', dataset.endAction !== 'message'); - this.$el.find('[data-toggle-class="hide-countdown"]') - .toggleClass('d-none', dataset.endAction === 'nothing'); // End Message UI this.updateUIEndMessage(); - - // Styling UI - this.$el.find('[data-attribute-name="layoutBackground"], [data-attribute-name="progressBarStyle"]') - .toggleClass('d-none', dataset.layout === 'clean' || dataset.layout === 'text'); - this.$el.find('[data-attribute-name="layoutBackgroundColor"]') - .toggleClass('d-none', dataset.layoutBackground === 'none'); - this.$el.find('[data-attribute-name="progressBarWeight"], [data-attribute-name="progressBarColor"]') - .toggleClass('d-none', dataset.progressBarStyle === 'none'); }, /** * @see this.updateUI @@ -1677,35 +1647,24 @@ options.registry.topMenuTransparency = options.Class.extend({ }); options.registry.topMenuColor = options.Class.extend({ - /** - * @override - */ - start: function () { - var self = this; - var def = this._super.apply(this, arguments); - this.$target.on('snippet-option-change', function () { - self.onFocus(); - }); - return def; - }, //-------------------------------------------------------------------------- - // Public + // Private //-------------------------------------------------------------------------- /** * @override */ - updateUI: async function () { - await this._super(...arguments); - await new Promise(resolve => { + _computeVisibility: async function () { + const show = await this._super(...arguments); + if (!show) { + return false; + } + return new Promise(resolve => { this.trigger_up('action_demand', { actionName: 'get_page_option', params: ['header_overlay'], - onSuccess: value => { - this.$el.toggleClass('d-none', !value); - resolve(); - }, + onSuccess: value => resolve(!!value), }); }); }, @@ -1959,29 +1918,10 @@ options.registry.CoverProperties = options.Class.extend({ 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); - - if (!$el.is('[data-change]')) { - $el.removeClass('d-none'); - - ['size', 'filters', 'text_size', 'text_align'].forEach(optName => { - var $opts = $el.find('[data-cover-opt="' + optName + '"]'); - var notAllowed = (this.$target.data('use_' + optName) !== 'True'); - - if ($opts.length && (!this.$target.hasClass('o_record_has_cover') || notAllowed)) { - $el.addClass('d-none'); - } - }); - } - }); - this.$el.find('[data-clear]').toggleClass('d-none', !this.$target.hasClass('o_record_has_cover')); - // 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.coverClass = this.$el.find('[data-cover-opt-name="size"] .active').data('selectClass') || ''; + this.$target[0].dataset.textSizeClass = this.$el.find('[data-cover-opt-name="text_size"] .active').data('selectClass') || ''; + this.$target[0].dataset.textAlignClass = this.$el.find('[data-cover-opt-name="text_align"] .active').data('selectClass') || ''; this.$target[0].dataset.filterValue = this.$filterValueOpts.filter('.active').data('filterValue') || 0.0; this.$target[0].dataset.filterColor = this.$filterColorOpts.filter('.active').data('filterColor') || ''; }, @@ -2010,6 +1950,19 @@ options.registry.CoverProperties = options.Class.extend({ } return this._super(...arguments); }, + /** + * @override + */ + _computeWidgetVisibility: function (widgetName, params) { + const hasCover = this.$target.hasClass('o_record_has_cover'); + if (widgetName === 'remove_cover_opt') { + return hasCover; + } else if (params.coverOptName) { + var notAllowed = (this.$target.data(`use_${params.coverOptName}`) !== 'True'); + return (hasCover && !notAllowed); + } + return this._super(...arguments); + }, }); /** @@ -2096,9 +2049,8 @@ options.registry.InnerChart = options.Class.extend({ * @override */ start: function () { - // Get the 2 color pickers to hide in updateUI - this.backSelectEl = this.el.querySelector('we-select[data-attribute-name="backgroundColor"]'); - this.borderSelectEl = this.el.querySelector('we-select[data-attribute-name="borderColor"]'); + this.backSelectEl = this.el.querySelector('[data-name="chart_bg_color_opt"]'); + this.borderSelectEl = this.el.querySelector('[data-name="chart_border_color_opt"]'); // Build matrix content this.tableEl = this.el.querySelector('we-matrix table'); @@ -2130,16 +2082,15 @@ options.registry.InnerChart = options.Class.extend({ if (!this.lastEditableSelectedInput.closest('table') || this.colorPaletteSelectedInput && !this.colorPaletteSelectedInput.closest('table')) { this._setDefaultSelectedInput(); } + await this._super(...arguments); + // prevent the columns from becoming too small. this.tableEl.classList.toggle('o_we_matrix_five_col', this.tableEl.querySelectorAll('tr:first-child th').length > 5); - this.el.querySelector('[data-attribute-name="stacked"]').classList.toggle('d-none', !this._isStackableChart() || this._getColumnCount() === 1); - // Disable color on inputs that don't use them - const notDisplayed = !this.colorPaletteSelectedInput; - this.backSelectEl.classList.toggle('d-none', notDisplayed); - this.borderSelectEl.classList.toggle('d-none', notDisplayed); + this.backSelectEl.querySelector('we-title').textContent = this._isPieChart() ? _t("Data Background Color") : _t("Dataset Background Color"); this.borderSelectEl.querySelector('we-title').textContent = this._isPieChart() ? _t("Data Border Color") : _t("Dataset Border Color"); + // Dataset/Cell color this.tableEl.querySelectorAll('input').forEach(el => el.style.border = ''); const selector = this._isPieChart() ? 'td input' : 'tr:first-child input'; @@ -2198,6 +2149,21 @@ options.registry.InnerChart = options.Class.extend({ } return this._super(...arguments); }, + /** + * @override + */ + _computeWidgetVisibility: function (widgetName, params) { + switch (widgetName) { + case 'stacked_chart_opt': { + return this._getColumnCount() > 1; + } + case 'chart_bg_color_opt': + case 'chart_border_color_opt': { + return !!this.colorPaletteSelectedInput; + } + } + return this._super(...arguments); + }, /** * Sets and reloads the data on the canvas if it has changed. * Used in matrix related method. @@ -2365,13 +2331,6 @@ options.registry.InnerChart = options.Class.extend({ _isPieChart: function () { return ['pie', 'doughnut'].includes(this.$target[0].dataset.type); }, - /** - * @private - * @return {boolean} - */ - _isStackableChart: function () { - return ['bar', 'horizontalBar'].includes(this.$target[0].dataset.type); - }, /** * Return the number of column minus header and button * @private diff --git a/addons/website/views/snippets.xml b/addons/website/views/snippets.xml index ad005d7f58a..7f79687acb2 100644 --- a/addons/website/views/snippets.xml +++ b/addons/website/views/snippets.xml @@ -1170,7 +1170,7 @@
Add Tab - Remove Tab + Remove Tab
@@ -1179,11 +1179,11 @@ Float - Masonry - Grid - Slideshow + Masonry + Grid + Slideshow - + 1s 2s 3s @@ -1191,7 +1191,7 @@ 10s Disable autoplay - + 1 2 3 @@ -1199,7 +1199,7 @@ 6 12 - + None Small Medium @@ -1282,12 +1282,12 @@ Cover - Repeat pattern + Repeat pattern - + @@ -1338,12 +1338,14 @@
- - + @@ -1421,41 +1423,41 @@
Change Cover - Remove Cover - - Full Screen - Half Screen - Fit text + Remove Cover + + Full Screen + Half Screen + Fit text - - None - Low - Medium - High + + None + Low + Medium + High - - Black - White - Primary - Secondary - Blue - Yellow - Red - Purple - Green + + Black + White + Primary + Secondary + Blue + Yellow + Red + Purple + Green - - Hero - Huge - Big - Regular - Small - Tiny + + Hero + Huge + Big + Regular + Small + Tiny - - Left - Centered - Right + + Left + Centered + Right
@@ -1471,14 +1473,16 @@ - Nothing - Redirect + Nothing + Redirect Show Message - - + + Small Medium @@ -1491,27 +1495,29 @@ - Circle - Boxes + Circle + Boxes Clean Text Inline - + Inner Plain - None + None - - + + Surrounded Disappearing - None + None - + Thin Thick - +
@@ -1522,14 +1528,15 @@ data-css-property="background-color" data-color-prefix="bg-"/> - Bar Vertical - Bar Horizontal + Bar Vertical + Bar Horizontal Line Pie Doughnut Radar - + None Top @@ -1549,11 +1556,11 @@ - -