From a48a30f954afcb6ff3a59c4f32b05fd0c2cfcd2b Mon Sep 17 00:00:00 2001 From: qsm-odoo Date: Tue, 13 Jul 2021 09:01:41 +0000 Subject: [PATCH] [IMP] website, web_editor, *: allow to use pre-configured gradients *: web After this commit pre-configured gradients can be used as snippet backgrounds, snippet filters or as text effects / highlight effects. The background colors (and now gradients) are now possible to add *alongside* a color combination class (editing one does not remove the other). Background colors and gradients are mutually exclusive. Part of https://github.com/odoo/odoo/pull/73611 task-2599770 closes odoo/odoo#73611 Signed-off-by: Quentin Smetz (qsm) Co-authored-by: Benoit Socias --- addons/web/static/src/legacy/scss/utils.scss | 1 - .../static/lib/odoo-editor/src/OdooEditor.js | 69 +++- .../lib/odoo-editor/src/commands/commands.js | 28 +- .../static/lib/odoo-editor/src/utils/utils.js | 9 + .../web_editor/static/src/js/common/utils.js | 72 ++++- .../static/src/js/editor/snippets.options.js | 301 +++++++++++++----- .../src/js/wysiwyg/widgets/color_palette.js | 129 ++++++-- .../static/src/js/wysiwyg/wysiwyg.js | 22 +- .../static/src/scss/secondary_variables.scss | 3 + .../static/src/scss/web_editor.common.scss | 8 + .../static/src/scss/web_editor.variables.scss | 19 +- .../web_editor/static/src/scss/wysiwyg.scss | 11 +- addons/web_editor/static/src/xml/snippets.xml | 14 +- .../static/tests/field_html_tests.js | 2 +- addons/web_editor/views/editor.xml | 15 + addons/web_editor/views/snippets.xml | 28 +- addons/website/models/assets.py | 7 + .../static/src/js/editor/snippets.options.js | 109 +++++-- .../static/src/scss/primary_variables.scss | 80 ++++- .../static/src/scss/secondary_variables.scss | 24 +- addons/website/static/src/scss/website.scss | 27 +- .../tests/tours/snippet_background_edition.js | 262 +++++++++++++++ .../tours/snippet_empty_parent_autoremove.js | 6 - addons/website/tests/test_ui.py | 10 + .../website/views/snippets/s_blockquote.xml | 1 + .../website/views/snippets/s_media_list.xml | 1 + addons/website/views/snippets/snippets.xml | 41 ++- 27 files changed, 1094 insertions(+), 205 deletions(-) create mode 100644 addons/website/static/tests/tours/snippet_background_edition.js diff --git a/addons/web/static/src/legacy/scss/utils.scss b/addons/web/static/src/legacy/scss/utils.scss index 721607a1140..0608ea491b0 100644 --- a/addons/web/static/src/legacy/scss/utils.scss +++ b/addons/web/static/src/legacy/scss/utils.scss @@ -449,4 +449,3 @@ } } } - diff --git a/addons/web_editor/static/lib/odoo-editor/src/OdooEditor.js b/addons/web_editor/static/lib/odoo-editor/src/OdooEditor.js index 77e8ff0c6b6..78f95043204 100644 --- a/addons/web_editor/static/lib/odoo-editor/src/OdooEditor.js +++ b/addons/web_editor/static/lib/odoo-editor/src/OdooEditor.js @@ -22,6 +22,7 @@ import { getListMode, getOuid, insertText, + isColorGradient, nodeSize, preserveCursor, setCursor, @@ -999,27 +1000,73 @@ export class OdooEditor extends EventTarget { } } + /** + * Displays the text colors (foreground ink and background highlight) + * based on the current text cursor position. For gradients, displays + * the average color of the gradient. + * + * @param {object} [params] + * @param {string} [params.foreColor] - forces the 'foreColor' in the + * toolbar instead of determining it from the cursor position + * @param {string} [params.hiliteColor] - forces the 'hiliteColor' in the + * toolbar instead of determining it from the cursor position + */ updateColorpickerLabels(params = {}) { - const foreColor = params.foreColor || rgbToHex(document.queryCommandValue('foreColor')); + function hexFromColor(color) { + if (isColorGradient(color)) { + // For gradients, compute the average color + color = color.match(/gradient(.*)/)[0]; + let r = 0, g = 0, b = 0, count = 0; + for (const entry of color.matchAll(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)/g)) { + count++; + r += parseInt(entry[1], 10); + g += parseInt(entry[2], 10); + b += parseInt(entry[3], 10); + } + color = `rgb(${Math.round(r / count)}, ${Math.round(g / count)}, ${Math.round(b / count)})`; + } + return rgbToHex(color); + } + let foreColor = params.foreColor; + let hiliteColor = params.hiliteColor; + + // Determine colors at cursor position + const sel = this.document.getSelection(); + if (sel.rangeCount && (!foreColor || !hiliteColor)) { + const endContainer = closestElement(sel.getRangeAt(0).endContainer); + const computedStyle = getComputedStyle(endContainer); + const backgroundImage = computedStyle.backgroundImage; + const hasGradient = isColorGradient(backgroundImage); + const hasTextGradientClass = endContainer.classList.contains('text-gradient'); + if (!foreColor) { + if (hasGradient && hasTextGradientClass) { + foreColor = backgroundImage; + } else { + foreColor = document.queryCommandValue('foreColor'); + } + } + if (!hiliteColor) { + if (hasGradient && !hasTextGradientClass) { + hiliteColor = backgroundImage; + } else { + hiliteColor = computedStyle.backgroundColor; + } + } + } + + // display colors in toolbar buttons + foreColor = hexFromColor(foreColor); this.toolbar.style.setProperty('--fore-color', foreColor); const foreColorInput = this.toolbar.querySelector('#foreColor input'); if (foreColorInput) { foreColorInput.value = foreColor; } - let hiliteColor = params.hiliteColor; - if (!hiliteColor) { - const sel = this.document.getSelection(); - if (sel.rangeCount) { - const endContainer = closestElement(sel.getRangeAt(0).endContainer); - const hiliteColorRgb = getComputedStyle(endContainer).backgroundColor; - hiliteColor = rgbToHex(hiliteColorRgb); - } - } + hiliteColor = hexFromColor(hiliteColor); this.toolbar.style.setProperty('--hilite-color', hiliteColor); const hiliteColorInput = this.toolbar.querySelector('#hiliteColor input'); if (hiliteColorInput) { - hiliteColorInput.value = hiliteColor.length <= 7 ? hiliteColor : rgbToHex(hiliteColor); + hiliteColorInput.value = hiliteColor.length <= 7 ? hiliteColor : hexFromColor(hiliteColor); } } diff --git a/addons/web_editor/static/lib/odoo-editor/src/commands/commands.js b/addons/web_editor/static/lib/odoo-editor/src/commands/commands.js index 74c92273109..d2c5cb96443 100644 --- a/addons/web_editor/static/lib/odoo-editor/src/commands/commands.js +++ b/addons/web_editor/static/lib/odoo-editor/src/commands/commands.js @@ -18,6 +18,7 @@ import { insertText, isBlock, isBold, + isColorGradient, isContentTextNode, isShrunkBlock, isVisible, @@ -138,11 +139,25 @@ function align(editor, mode) { function colorElement(element, color, mode) { const newClassName = element.className .replace(mode === 'color' ? TEXT_CLASSES_REGEX : BG_CLASSES_REGEX, '') + .replace(/\btext-gradient\b/g, '') // cannot be combined with setting a background .replace(/\s+/, ' '); element.className !== newClassName && (element.className = newClassName); + element.style['background-image'] = ''; + if (mode === 'backgroundColor') { + element.style['background'] = ''; + } if (color.startsWith('text') || color.startsWith('bg-')) { element.style[mode] = ''; - element.className += ' ' + color; + element.classList.add(color); + } else if (isColorGradient(color)) { + element.style[mode] = ''; + if (mode === 'color') { + element.style['background'] = ''; + element.style['background-image'] = color; + element.classList.add('text-gradient'); + } else { + element.style['background-image'] = color; + } } else { element.style[mode] = color; } @@ -160,6 +175,17 @@ function hasColor(element, mode) { const style = element.style; const parent = element.parentNode; const classRegex = mode === 'color' ? TEXT_CLASSES_REGEX : BG_CLASSES_REGEX; + if (isColorGradient(style['background-image'])) { + if (element.classList.contains('text-gradient')) { + if (mode === 'color') { + return true; + } + } else { + if (mode !== 'color') { + return true; + } + } + } return ( (style[mode] && style[mode] !== 'inherit' && style[mode] !== parent.style[mode]) || (classRegex.test(element.className) && diff --git a/addons/web_editor/static/lib/odoo-editor/src/utils/utils.js b/addons/web_editor/static/lib/odoo-editor/src/utils/utils.js index 68ffd2f8535..cd4eb60cb7c 100644 --- a/addons/web_editor/static/lib/odoo-editor/src/utils/utils.js +++ b/addons/web_editor/static/lib/odoo-editor/src/utils/utils.js @@ -1216,6 +1216,15 @@ export function isShrunkBlock(blockEl) { return isEmptyBlock(blockEl) && !blockEl.querySelector('br'); } +/** + * @param {string} [value] + * @returns {boolean} + */ +export function isColorGradient(value) { + // FIXME duplicated in @web_editor/utils.js + return value && value.includes('-gradient('); +} + //------------------------------------------------------------------------------ // DOM Modification //------------------------------------------------------------------------------ diff --git a/addons/web_editor/static/src/js/common/utils.js b/addons/web_editor/static/src/js/common/utils.js index a473f56b147..d4a9bac607e 100644 --- a/addons/web_editor/static/src/js/common/utils.js +++ b/addons/web_editor/static/src/js/common/utils.js @@ -159,6 +159,31 @@ function _areCssValuesEqual(value1, value2, cssProp, $target) { return true; } + // They may be gradients + const value1IsGradient = _isColorGradient(value1); + const value2IsGradient = _isColorGradient(value2); + if (value1IsGradient !== value2IsGradient) { + return false; + } + if (value1IsGradient) { + // Kinda hacky and probably inneficient but probably the easiest way: + // applied the value as background-image of two fakes elements and + // compare their computed value. + const temp1El = document.createElement('div'); + temp1El.style.backgroundImage = value1; + document.body.appendChild(temp1El); + value1 = getComputedStyle(temp1El).backgroundImage; + document.body.removeChild(temp1El); + + const temp2El = document.createElement('div'); + temp2El.style.backgroundImage = value2; + document.body.appendChild(temp2El); + value2 = getComputedStyle(temp2El).backgroundImage; + document.body.removeChild(temp2El); + + return value1 === value2; + } + // Convert the second value in the unit of the first one and compare // floating values const data = _getNumericAndUnit(value1); @@ -237,7 +262,8 @@ function _normalizeColor(color) { * @returns {string|false} the src of the image or false if not parsable */ function _getBgImageURL(el) { - const string = $(el).css('background-image'); + const parts = _backgroundImageCssToParts($(el).css('background-image')); + const string = parts.url || ''; const match = string.match(/^url\((['"])(.*?)\1\)$/); if (!match) { return ''; @@ -250,6 +276,47 @@ function _getBgImageURL(el) { } return matchedURL; } +/** + * Extracts url and gradient parts from the background-image CSS property. + * + * @param {string} CSS 'background-image' property value + * @returns {Object} contains the separated 'url' and 'gradient' parts + */ +function _backgroundImageCssToParts(css) { + const parts = {}; + css = css || ''; + if (css.startsWith('url(')) { + const urlEnd = css.indexOf(')') + 1; + parts.url = css.substring(0, urlEnd).trim(); + const commaPos = css.indexOf(',', urlEnd); + css = commaPos > 0 ? css.substring(commaPos + 1) : ''; + } + if (_isColorGradient(css)) { + parts.gradient = css.trim(); + } + return parts; +} +/** + * Combines url and gradient parts into a background-image CSS property value + * + * @param {Object} contains the separated 'url' and 'gradient' parts + * @returns {string} CSS 'background-image' property value + */ +function _backgroundImagePartsToCss(parts) { + let css = parts.url || ''; + if (parts.gradient) { + css += (css ? ', ' : '') + parts.gradient; + } + return css || 'none'; +} +/** + * @param {string} [value] + * @returns {boolean} + */ +function _isColorGradient(value) { + // FIXME duplicated in odoo-editor/utils.js + return value && value.includes('-gradient('); +} return { CSS_SHORTHANDS: CSS_SHORTHANDS, @@ -261,9 +328,12 @@ return { getNumericAndUnit: _getNumericAndUnit, areCssValuesEqual: _areCssValuesEqual, isColorCombinationName: _isColorCombinationName, + isColorGradient: _isColorGradient, computeColorClasses: _computeColorClasses, getCSSVariableValue: _getCSSVariableValue, normalizeColor: _normalizeColor, getBgImageURL: _getBgImageURL, + backgroundImageCssToParts: _backgroundImageCssToParts, + backgroundImagePartsToCss: _backgroundImagePartsToCss, }; }); 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 2aeb71d8b07..677fe4aacff 100644 --- a/addons/web_editor/static/src/js/editor/snippets.options.js +++ b/addons/web_editor/static/src/js/editor/snippets.options.js @@ -12,6 +12,8 @@ const weUtils = require('web_editor.utils'); const { normalizeColor, getBgImageURL, + backgroundImageCssToParts, + backgroundImagePartsToCss, DEFAULT_PALETTE, } = weUtils; var weWidgets = require('wysiwyg.widgets'); @@ -480,6 +482,13 @@ const UserValueWidget = Widget.extend({ const uniqArr = arr.filter((v, i, arr) => i === arr.indexOf(v)); this._methodsParams.optionsPossibleValues[methodName] = uniqArr; } + + // Method names come from the widget's dataset whose keys' order cannot + // be relied on. We explicitely sort them by alphabetical order allowing + // consistent behavior, while relying on order for such methods should + // not be done when possible (the methods should be independent from + // each other when possible). + this._methodsNames.sort(); }, /** * @param {boolean} [previewMode=false] @@ -1329,7 +1338,7 @@ const ColorpickerUserValueWidget = SelectUserValueWidget.extend({ */ open: function () { if (this.colorPalette.setSelectedColor) { - this.colorPalette.setSelectedColor(this._value); + this.colorPalette.setSelectedColor(this._ccValue, this._value); } else { // TODO review in master, this does async stuff. Maybe the open // method should now be async. This is not really robust as the @@ -1363,14 +1372,23 @@ const ColorpickerUserValueWidget = SelectUserValueWidget.extend({ * @override */ getValue: function (methodName) { - if (typeof this._previewColor === 'string') { - return this._previewColor; - } - if (typeof this._customColorValue === 'string') { - return this._customColorValue; - } + const isCCMethod = (this._methodsParams.withCombinations === methodName); let value = this._super(...arguments); + if (isCCMethod) { + value = this._ccValue; + } else if (typeof this._customColorValue === 'string') { + value = this._customColorValue; + } + + // TODO strange there is some processing below for the normal value but + // not for the preview value? To check in older stable versions as well. + if (typeof this._previewColor === 'string') { + return isCCMethod ? this._previewCC : this._previewColor; + } + if (value) { + // TODO probably something to be done to handle gradients properly + // in this code. const useCssColor = this.options.dataAttributes.hasOwnProperty('useCssColor'); const cssCompatible = this.options.dataAttributes.hasOwnProperty('cssCompatible'); if ((useCssColor || cssCompatible) && !ColorpickerWidget.isCSSColor(value)) { @@ -1393,34 +1411,46 @@ const ColorpickerUserValueWidget = SelectUserValueWidget.extend({ * @override */ isActive: function () { - return !weUtils.areCssValuesEqual(this._value, 'rgba(0, 0, 0, 0)'); + return !!this._ccValue + || !weUtils.areCssValuesEqual(this._value, 'rgba(0, 0, 0, 0)'); }, /** * Updates the color preview + re-render the whole color palette widget. * * @override */ - async setValue(color) { - await this._super(...arguments); + async setValue(color, methodName, ...rest) { + // The colorpicker widget can hold two values: a color combination and + // a normal color or a gradient. The base `_value` will hold the normal + // color or the gradient value. The color combination one will be + // available in `_ccValue`. + const isCCMethod = (this._methodsParams.withCombinations === methodName); + // Always call _super but don't change _value if meant for the CC value. + await this._super(isCCMethod ? this._value : color, methodName, ...rest); + if (isCCMethod) { + this._ccValue = color; + } await this._colorPaletteRenderPromise; const classes = weUtils.computeColorClasses(this.colorPalette.getColorNames()); this.colorPreviewEl.classList.remove(...classes); this.colorPreviewEl.style.removeProperty('background-color'); - + this.colorPreviewEl.style.removeProperty('background-image'); + if (this._ccValue) { + this.colorPreviewEl.classList.add('o_cc', `o_cc${this._ccValue}`); + } if (this._value) { if (ColorpickerWidget.isCSSColor(this._value)) { this.colorPreviewEl.style.backgroundColor = this._value; - } else if (weUtils.isColorCombinationName(this._value)) { - this.colorPreviewEl.classList.add('o_cc', `o_cc${this._value}`); - } else { + } else if (!weUtils.isColorGradient(this._value)) { this.colorPreviewEl.classList.add(`bg-${this._value}`); + } else { + this.colorPreviewEl.style.backgroundImage = this._value; } } }, - //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- @@ -1431,14 +1461,21 @@ const ColorpickerUserValueWidget = SelectUserValueWidget.extend({ */ _renderColorPalette: function () { const options = { + selectedCC: this._ccValue, selectedColor: this._value, }; if (this.options.dataAttributes.excluded) { options.excluded = this.options.dataAttributes.excluded.replace(/ /g, '').split(','); } + if (this.options.dataAttributes.opacity) { + options.opacity = this.options.dataAttributes.opacity; + } if (this.options.dataAttributes.withCombinations) { options.withCombinations = !!this.options.dataAttributes.withCombinations; } + if (this.options.dataAttributes.withGradients) { + options.withGradients = !!this.options.dataAttributes.withGradients; + } if (this.options.dataAttributes.selectedTab) { options.selectedTab = this.options.dataAttributes.selectedTab; } @@ -1479,9 +1516,13 @@ const ColorpickerUserValueWidget = SelectUserValueWidget.extend({ * @param {Event} ev */ _onColorPicked: function (ev) { + this._previewCC = false; this._previewColor = false; this._customColorValue = false; + + this._ccValue = ev.data.ccValue; this._value = ev.data.color; + this._onUserValueChange(ev); }, /** @@ -1491,6 +1532,7 @@ const ColorpickerUserValueWidget = SelectUserValueWidget.extend({ * @param {Event} ev */ _onColorHovered: function (ev) { + this._previewCC = ev.data.ccValue; this._previewColor = ev.data.color; this._onUserValuePreview(ev); }, @@ -1501,6 +1543,7 @@ const ColorpickerUserValueWidget = SelectUserValueWidget.extend({ * @param {Event} ev */ _onColorLeft: function (ev) { + this._previewCC = false; this._previewColor = false; this._onUserValueReset(ev); }, @@ -3031,31 +3074,42 @@ const SnippetOptionWidget = Widget.extend({ this.$target.trigger('background-color-event', previewMode); } - const cssProps = weUtils.CSS_SHORTHANDS[params.cssProperty] || [params.cssProperty]; + // Always reset the inline style first to not put inline style on an + // element which already have this style through css stylesheets. + let cssProps = weUtils.CSS_SHORTHANDS[params.cssProperty] || [params.cssProperty]; 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, ''); } if (params.extraClass) { this.$target.removeClass(params.extraClass); } + // Plain color and gradient are mutually exclusive as background so in + // case we edit a background-color we also have to reset the gradient + // part of the background-image property (the opposite is handled by the + // fact that editing a gradient as background is done by calling this + // method with background-color as property too, so it is automatically + // reset anyway). + let bgImageParts = undefined; + if (params.cssProperty === 'background-color') { + const styles = getComputedStyle(this.$target[0]); + bgImageParts = backgroundImageCssToParts(styles['background-image']); + delete bgImageParts.gradient; + const combined = backgroundImagePartsToCss(bgImageParts); + this.$target[0].style.setProperty('background-image', ''); + applyCSS.call(this, 'background-image', combined, styles); + } // Only allow to use a color name as a className if we know about the // other potential color names (to remove) and if we know about a prefix // (otherwise we suppose that we should use the actual related color). + // Note: color combinations classes are handled by a dedicated method, + // as they can be combined with normal classes. if (params.colorNames && params.colorPrefix) { - const classes = weUtils.computeColorClasses(params.colorNames, params.colorPrefix); + const colorNames = params.colorNames.filter(name => !weUtils.isColorCombinationName(name)); + const classes = weUtils.computeColorClasses(colorNames, params.colorPrefix); this.$target[0].classList.remove(...classes); - if (weUtils.isColorCombinationName(widgetValue)) { - // Those are the special color combinations classes. Just have - // to add it (and adding the potential extra class) then leave. - this.$target[0].classList.add('o_cc', `o_cc${widgetValue}`, params.extraClass); - _restoreTransitions(); - return; - } - if (params.colorNames.includes(widgetValue)) { + if (colorNames.includes(widgetValue)) { const originalCSSValue = window.getComputedStyle(this.$target[0])[cssProps[0]]; const className = params.colorPrefix + widgetValue; this.$target[0].classList.add(className); @@ -3074,6 +3128,8 @@ const SnippetOptionWidget = Widget.extend({ } } + const styles = window.getComputedStyle(this.$target[0]); + // At this point, the widget value is either a property/color name or // an actual css property value. If it is a property/color name, we will // apply a css variable as style value. @@ -3082,6 +3138,21 @@ const SnippetOptionWidget = Widget.extend({ widgetValue = `var(--${widgetValue})`; } + // In case of background-color edition, we could receive a gradient, in + // which case the value has to be combined with the potential background + // image (real image). + if (params.cssProperty === 'background-color' && weUtils.isColorGradient(widgetValue)) { + cssProps = ['background-image']; + bgImageParts.gradient = widgetValue; + widgetValue = backgroundImagePartsToCss(bgImageParts); + + // Also force the background-color to transparent as otherwise it + // won't act as a "gradient replacing the color combination + // background" but be applied over it (which would be the opposite + // of what happens when editing the background color). + applyCSS.call(this, 'background-color', 'rgba(0, 0, 0, 0)', styles); + } + // replacing ', ' by ',' to prevent attributes with internal space separators from being split: // eg: "rgba(55, 12, 47, 1.9) 47px" should be split as ["rgba(55,12,47,1.9)", "47px"] const values = widgetValue.replace(/,\s/g, ',').split(/\s+/g); @@ -3102,7 +3173,6 @@ const SnippetOptionWidget = Widget.extend({ } } - 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; @@ -3123,6 +3193,22 @@ const SnippetOptionWidget = Widget.extend({ _restoreTransitions(); }, + /** + * Sets a color combination. + * + * @see this.selectClass for parameters + */ + async selectColorCombination(previewMode, widgetValue, params) { + if (params.colorNames) { + const names = params.colorNames.filter(weUtils.isColorCombinationName); + const classes = weUtils.computeColorClasses(names); + this.$target[0].classList.remove(...classes); + + if (widgetValue) { + this.$target[0].classList.add('o_cc', `o_cc${widgetValue}`); + } + } + }, //-------------------------------------------------------------------------- // Public @@ -3398,10 +3484,15 @@ const SnippetOptionWidget = Widget.extend({ return attrValue || params.attributeDefaultValue || ''; } case 'selectStyle': { + let usedCC = undefined; if (params.colorPrefix && params.colorNames) { for (const c of params.colorNames) { const className = weUtils.computeColorClasses([c], params.colorPrefix)[0]; if (this.$target[0].classList.contains(className)) { + if (weUtils.isColorCombinationName(c)) { + usedCC = c; + continue; + } return c; } } @@ -3414,6 +3505,18 @@ const SnippetOptionWidget = Widget.extend({ const _restoreTransitions = () => this.$target[0].classList.remove('o_we_force_no_transition'); const styles = window.getComputedStyle(this.$target[0]); + + if (params.cssProperty === 'background-color') { + // Check if there is a gradient, in that case this is the + // value to be returned, we normally not allow color and + // gradient at the same time (the option would remove one + // if editing the other). + const parts = backgroundImageCssToParts(styles['background-image']); + if (parts.gradient) { + return parts.gradient; + } + } + const cssProps = weUtils.CSS_SHORTHANDS[params.cssProperty] || [params.cssProperty]; const cssValues = cssProps.map(cssProp => { let value = styles[cssProp].trim(); @@ -3438,7 +3541,44 @@ const SnippetOptionWidget = Widget.extend({ _restoreTransitions(); - return cssValues.join(' '); + const value = cssValues.join(' '); + + if (params.cssProperty === 'background-color' && params.withCombinations) { + if (usedCC) { + const ccValue = weUtils.getCSSVariableValue(`o-cc${usedCC}-bg`).trim(); + if (weUtils.areCssValuesEqual(value, ccValue)) { + // Prevent to consider that a color is used as CC + // override in case that color is the same as the + // one used in that CC. + return ''; + } + } else { + const rgba = ColorpickerWidget.convertCSSColorToRgba(value); + if (rgba && rgba.opacity < 0.001) { + // Prevent to consider a transparent color is + // applied as background unless it is to override a + // CC. Simply allows to add a CC on a transparent + // snippet in the first place. + return ''; + } + } + } + + return value; + } + case 'selectColorCombination': { + if (params.colorNames) { + for (const c of params.colorNames) { + if (!weUtils.isColorCombinationName(c)) { + continue; + } + const className = weUtils.computeColorClasses([c])[0]; + if (this.$target[0].classList.contains(className)) { + return c; + } + } + } + return ''; } } }, @@ -5038,7 +5178,10 @@ registry.BackgroundOptimize = ImageHandlerOption.extend({ * @override */ _applyImage(img) { - this.$target.css('background-image', `url('${img.getAttribute('src')}')`); + const parts = backgroundImageCssToParts(this.$target.css('background-image')); + parts.url = `url('${img.getAttribute('src')}')`; + const combined = backgroundImagePartsToCss(parts); + this.$target.css('background-image', combined); }, //-------------------------------------------------------------------------- @@ -5060,20 +5203,6 @@ registry.BackgroundOptimize = ImageHandlerOption.extend({ }); registry.BackgroundToggler = SnippetOptionWidget.extend({ - /** - * @override - */ - start() { - this.$target.on('content_changed.BackgroundToggler', this._onExternalUpdate.bind(this)); - return this._super(...arguments); - }, - /** - * @override - */ - destroy() { - this._super(...arguments); - this.$target.off('.BackgroundToggler'); - }, //-------------------------------------------------------------------------- // Options @@ -5086,6 +5215,7 @@ registry.BackgroundToggler = SnippetOptionWidget.extend({ */ toggleBgImage(previewMode, widgetValue, params) { if (!widgetValue) { + this.$target.find('> .o_we_bg_filter').remove(); // TODO: use setWidgetValue instead of calling background directly when possible const [bgImageWidget] = this._requestUserValueWidgets('bg_image_opt'); const bgImageOpt = bgImageWidget.getParent(); @@ -5108,23 +5238,39 @@ registry.BackgroundToggler = SnippetOptionWidget.extend({ return shapeOption._toggleShape(); }, /** - * Toggles background filter on or off. + * Sets a color filter. * * @see this.selectClass for parameters */ - toggleBgFilter(previewMode, widgetValue, params) { - if (widgetValue) { - const bgFilterEl = document.createElement('div'); - bgFilterEl.classList.add('o_we_bg_filter', 'bg-black-50'); + async selectFilterColor(previewMode, widgetValue, params) { + // Find the filter element. + let filterEl = this.$target[0].querySelector(':scope > .o_we_bg_filter'); + + // If the filter would be transparent, remove it / don't create it. + const rgba = widgetValue && ColorpickerWidget.convertCSSColorToRgba(widgetValue); + if (!widgetValue || rgba && rgba.opacity < 0.001) { + if (filterEl) { + filterEl.remove(); + } + return; + } + + // Create the filter if necessary. + if (!filterEl) { + filterEl = document.createElement('div'); + filterEl.classList.add('o_we_bg_filter'); const lastBackgroundEl = this._getLastPreFilterLayerElement(); if (lastBackgroundEl) { - $(lastBackgroundEl).after(bgFilterEl); + $(lastBackgroundEl).after(filterEl); } else { - this.$target.prepend(bgFilterEl); + this.$target.prepend(filterEl); } - } else { - this.$target.find('.o_we_bg_filter').remove(); } + + // Apply the color on the filter. + const obj = createPropertyProxy(this, '$target', $(filterEl)); + params.cssProperty = 'background-color'; + return this.selectStyle.call(obj, previewMode, widgetValue, params); }, //-------------------------------------------------------------------------- @@ -5141,14 +5287,20 @@ registry.BackgroundToggler = SnippetOptionWidget.extend({ const bgImageOpt = bgImageWidget.getParent(); return !!bgImageOpt._computeWidgetState('background', bgImageWidget.getMethodsParams('background')); } - case 'toggleBgFilter': { - return this._hasBgFilter(); - } case 'toggleBgShape': { const [shapeWidget] = this._requestUserValueWidgets('bg_shape_opt'); const shapeOption = shapeWidget.getParent(); return !!shapeOption._computeWidgetState('shape', shapeWidget.getMethodsParams('shape')); } + case 'selectFilterColor': { + const filterEl = this.$target[0].querySelector(':scope > .o_we_bg_filter'); + if (!filterEl) { + return ''; + } + const obj = createPropertyProxy(this, '$target', $(filterEl)); + params.cssProperty = 'background-color'; + return this._computeWidgetState.call(obj, 'selectStyle', params); + } } return this._super(...arguments); }, @@ -5158,32 +5310,6 @@ registry.BackgroundToggler = SnippetOptionWidget.extend({ _getLastPreFilterLayerElement() { return null; }, - /** - * @private - * @returns {Boolean} - */ - _hasBgFilter() { - return !!this.$target.find('> .o_we_bg_filter').length; - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * @private - */ - _onExternalUpdate() { - if (this._hasBgFilter() - && !this._getLastPreFilterLayerElement() - && !getBgImageURL(this.$target)) { - // No 'pre-filter' background layout anymore and no more background - // image: remove the background filter option. - // TODO there probably is a better system to implement to do that - const widget = this._requestUserValueWidgets('bg_filter_toggle_opt')[0]; - widget.enable(); - } - }, }); /** @@ -5235,14 +5361,14 @@ registry.BackgroundImage = SnippetOptionWidget.extend({ this.previousSrc = currentSrc; break; case 'reset': - this.$target.css('background-image', `url('${this.previousSrc}')`); + this._setBackground(this.previousSrc); return; } const newURL = new URL(currentSrc, window.location.origin); newURL.searchParams.set(params.colorName, normalizeColor(widgetValue)); const src = newURL.pathname + newURL.search; await loadImage(src); - this.$target.css('background-image', `url('${src}')`); + this._setBackground(src); if (!previewMode) { this.previousSrc = src; } @@ -5313,13 +5439,16 @@ registry.BackgroundImage = SnippetOptionWidget.extend({ * @param {string} backgroundURL */ _setBackground(backgroundURL) { + const parts = backgroundImageCssToParts(this.$target.css('background-image')); if (backgroundURL) { - this.$target.css('background-image', `url('${backgroundURL}')`); + parts.url = `url('${backgroundURL}')`; this.$target.addClass('oe_img_bg o_bg_img_center'); } else { - this.$target.css('background-image', ''); + delete parts.url; this.$target.removeClass('oe_img_bg o_bg_img_center'); } + const combined = backgroundImagePartsToCss(parts); + this.$target.css('background-image', combined); }, }); diff --git a/addons/web_editor/static/src/js/wysiwyg/widgets/color_palette.js b/addons/web_editor/static/src/js/wysiwyg/widgets/color_palette.js index ee83068772e..b6989d42559 100644 --- a/addons/web_editor/static/src/js/wysiwyg/widgets/color_palette.js +++ b/addons/web_editor/static/src/js/wysiwyg/widgets/color_palette.js @@ -34,6 +34,10 @@ const ColorPaletteWidget = Widget.extend({ * @param {boolean} [options.resetButton=true] Whether to display or not the reset button. * @param {string[]} [options.excluded=[]] Sections not to display. * @param {string[]} [options.excludeSectionOf] Extra section to exclude: the one containing the named color. + * @param {boolean} [options.withCombinations=false] Enable color combinations selection. + * @param {float} [options.opacity=1] Specify a default opacity (predefined gradients & color). + * @param {string} [options.selectedTab='theme-colors'] Tab initially selected. + * @param {boolean} [options.withGradients=false] Enable gradient selection. * @param {JQuery} [options.$editable=$()] Editable content from which the custom colors are retrieved. */ init: function (parent, options) { @@ -46,9 +50,10 @@ const ColorPaletteWidget = Widget.extend({ excludeSectionOf: null, $editable: $(), withCombinations: false, + opacity: 1, selectedTab: 'theme-colors', + withGradients: false, }, options || {}); - this.selectedColor = ''; this.resetButton = this.options.resetButton; this.withCombinations = this.options.withCombinations; @@ -69,6 +74,10 @@ const ColorPaletteWidget = Widget.extend({ 'transparent_grayscale', 'common_grays', ], + }, + { + id: 'gradients', + pickers: this.options.withGradients ? ['predefined_gradients'] : [], }]; this.sections = {}; @@ -118,15 +127,18 @@ const ColorPaletteWidget = Widget.extend({ pickerEl.dataset.name = 'custom'; break; default: - pickerEl = colorpickerEl.querySelector(`[data-name="${pickerId}"]`).cloneNode(true); + pickerEl = colorpickerEl.querySelector(`[data-name="${pickerId}"]`); + pickerEl = pickerEl && pickerEl.cloneNode(true); } - sectionEl.appendChild(pickerEl); + if (pickerEl) { + sectionEl.appendChild(pickerEl); - if (!this.options.excluded.includes(pickerId)) { - sectionIsEmpty = false; + if (!this.options.excluded.includes(pickerId)) { + sectionIsEmpty = false; + } + + this.pickers[pickerId] = pickerEl; } - - this.pickers[pickerId] = pickerEl; }); // If the section is empty, hide it and @@ -141,6 +153,16 @@ const ColorPaletteWidget = Widget.extend({ this.sections[tab.id] = sectionEl; }); + // Predefined gradient opacity + if (this.options.withGradients && this.options.opacity !== 1) { + this.pickers['predefined_gradients'].querySelectorAll('button').forEach(elem => { + let gradient = elem.dataset.color; + gradient = gradient.replaceAll(/rgba?(\(\s*\d+\s*,\s*\d+\s*,\s*\d+)(?:\s*,.+?)?\)/g, + `rgba$1, ${this.options.opacity})`); + elem.dataset.color = gradient.replaceAll(/\s+/g, ''); + }); + } + // Switch to the correct tab const selectedButtonIndex = this.tabs.map(tab => tab.id).indexOf(this.options.selectedTab); this._selectTabFromButton(this.el.querySelectorAll('button')[selectedButtonIndex]); @@ -183,6 +205,9 @@ const ColorPaletteWidget = Widget.extend({ this.colorToColorNames = {}; this.el.querySelectorAll('button[data-color]').forEach(elem => { const colorName = elem.dataset.color; + if (weUtils.isColorGradient(colorName)) { + return; + } const $color = $(elem); const isCCName = weUtils.isColorCombinationName(colorName); if (isCCName) { @@ -199,6 +224,9 @@ const ColorPaletteWidget = Widget.extend({ // Select selected Color and build customColors. // If no color is selected selectedColor is an empty string (transparent is interpreted as no color) + if (this.options.selectedCC) { + this.selectedCC = this.options.selectedCC; + } if (this.options.selectedColor) { let selectedColor = this.options.selectedColor; if (compatibilityColorNames.includes(selectedColor)) { @@ -218,12 +246,14 @@ const ColorPaletteWidget = Widget.extend({ if (defaultColor && !ColorpickerWidget.isCSSColor(defaultColor)) { defaultColor = weUtils.getCSSVariableValue(defaultColor, this.style); } + if (!defaultColor && this.options.opacity !== 1) { + defaultColor = 'rgba(0, 0, 0, ' + this.options.opacity + ')'; + } this.colorPicker = new ColorpickerWidget(this, { defaultColor: defaultColor, }); await this.colorPicker.appendTo(this.sections['custom-colors']); } - return res; }, /** @@ -233,12 +263,33 @@ const ColorPaletteWidget = Widget.extend({ return this.colorNames; }, /** - * Sets the currently selected color + * Gets the currently selected colors. * + * @returns {Object} ccValue and color (plain color or gradient). + */ + getSelectedColors() { + return { + ccValue: this.selectedCC, + color: this.selectedColor, + }; + }, + /** + * Sets the currently selected colors + * + * @param {string|number} ccValue * @param {string} color rgb[a] */ - setSelectedColor: function (color) { - this._selectColor({color: color}); + setSelectedColor: function (ccValue, color) { + if (color === 'rgba(0, 0, 0, 0)' && this.options.opacity !== 1) { + color = 'rgba(0, 0, 0, ' + this.options.opacity + ')'; + } + this._selectColor({ + ccValue: ccValue, + color: color, + }); + // This is called on open, restore default tab selection + const selectedButtonIndex = this.tabs.map(tab => tab.id).indexOf(this.options.selectedTab); + this._selectTabFromButton(this.el.querySelectorAll('button')[selectedButtonIndex]); }, //-------------------------------------------------------------------------- @@ -330,18 +381,30 @@ const ColorPaletteWidget = Widget.extend({ */ _getButtonInfo: function (buttonEl) { const bgColor = buttonEl.style.backgroundColor; - return { - color: bgColor ? ColorpickerWidget.normalizeCSSColor(bgColor) : buttonEl.dataset.color || '', + const value = buttonEl.dataset.color || (bgColor && bgColor !== 'initial' ? ColorpickerWidget.normalizeCSSColor(bgColor) : '') || ''; + const info = { target: buttonEl, }; + if (!value) { + info.ccValue = ''; + info.color = ''; + } else if (weUtils.isColorCombinationName(value)) { + info.ccValue = value; + } else { + info.color = value; + } + return info; }, /** * Set the selectedColor and trigger an event * - * @param {Object} color + * @param {Object} colorInfo + * @param {string} [colorInfo.ccValue] + * @param {string} [colorInfo.color] * @param {string} [eventName] */ _selectColor: function (colorInfo, eventName) { + this.selectedCC = colorInfo.ccValue; this.selectedColor = colorInfo.color = this.colorToColorNames[colorInfo.color] || colorInfo.color; if (eventName) { this.trigger_up(eventName, colorInfo); @@ -353,15 +416,18 @@ const ColorPaletteWidget = Widget.extend({ } }, /** - * Mark the selected color + * Marks the selected colors. * * @private */ _markSelectedColor: function () { - this.el.querySelectorAll('button.selected').forEach(el => el.classList.remove('selected')); - const buttonToSelectEls = this.el.querySelectorAll(`button[data-color="${this.selectedColor}"], button[style*="background-color:${this.selectedColor};"]`); - for (const buttonToSelectEl of buttonToSelectEls) { - buttonToSelectEl.classList.add('selected'); + for (const buttonEl of this.el.querySelectorAll('button')) { + // TODO buttons should only be search by data-color value + // instead of style but seems necessary for custom colors right + // now... + const value = buttonEl.dataset.color || buttonEl.style.backgroundColor; + buttonEl.classList.toggle('selected', value + && (this.selectedCC === value || weUtils.areCssValuesEqual(this.selectedColor, value))); } }, /** @@ -370,7 +436,10 @@ const ColorPaletteWidget = Widget.extend({ * @private * @param {HTMLElement} buttonEl */ - _selectTabFromButton(buttonEl) { + _selectTabFromButton(buttonEl) { + this.el.querySelectorAll('.o_we_colorpicker_switch_pane_btn').forEach(el => { + el.classList.remove('active'); + }); buttonEl.classList.add('active'); this.el.querySelectorAll('.o_colorpicker_sections').forEach(el => { el.classList.toggle('d-none', el.dataset.colorTab !== buttonEl.dataset.target); @@ -389,7 +458,7 @@ const ColorPaletteWidget = Widget.extend({ */ _onColorButtonClick: function (ev) { const buttonEl = ev.currentTarget; - const colorInfo = this._getButtonInfo(buttonEl); + const colorInfo = Object.assign(this.getSelectedColors(), this._getButtonInfo(buttonEl)); this._selectColor(colorInfo, 'color_picked'); }, /** @@ -400,7 +469,7 @@ const ColorPaletteWidget = Widget.extend({ */ _onColorButtonEnter: function (ev) { ev.stopPropagation(); - this.trigger_up('color_hover', this._getButtonInfo(ev.currentTarget)); + this.trigger_up('color_hover', Object.assign(this.getSelectedColors(), this._getButtonInfo(ev.currentTarget))); }, /** * Called when a color button is left the data color is the color currently selected. @@ -410,10 +479,9 @@ const ColorPaletteWidget = Widget.extend({ */ _onColorButtonLeave: function (ev) { ev.stopPropagation(); - this.trigger_up('color_leave', { - color: this.selectedColor, + this.trigger_up('color_leave', Object.assign(this.getSelectedColors(), { target: ev.target, - }); + })); }, /** * Called when an update is made on the colorpicker. @@ -422,10 +490,10 @@ const ColorPaletteWidget = Widget.extend({ * @param {Event} ev */ _onColorPickerPreview: function (ev) { - this.trigger_up('color_hover', { + this.trigger_up('color_hover', Object.assign(this.getSelectedColors(), { color: ev.data.cssColor, target: this.colorPicker.el, - }); + })); }, /** * Called when a color is selected on the colorpicker (mouseup). @@ -434,10 +502,10 @@ const ColorPaletteWidget = Widget.extend({ * @param {Event} ev */ _onColorPickerSelect: function (ev) { - this._selectColor({ + this._selectColor(Object.assign(this.getSelectedColors(), { color: ev.data.cssColor, target: this.colorPicker.el, - }, 'custom_color_picked'); + }), 'custom_color_picked'); }, /** * @private @@ -445,9 +513,6 @@ const ColorPaletteWidget = Widget.extend({ */ _onSwitchPaneButtonClick(ev) { ev.stopPropagation(); - this.el.querySelectorAll('.o_we_colorpicker_switch_pane_btn').forEach(el => { - el.classList.remove('active'); - }); this._selectTabFromButton(ev.currentTarget); }, }); diff --git a/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js b/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js index 7d8f727aac1..394203f0bbc 100644 --- a/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js +++ b/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js @@ -14,6 +14,7 @@ const snippetsEditor = require('web_editor.snippet.editor'); const Toolbar = require('web_editor.toolbar'); const weWidgets = require('wysiwyg.widgets'); const wysiwygUtils = require('@web_editor/js/wysiwyg/wysiwyg_utils'); +const weUtils = require('web_editor.utils'); var _t = core._t; @@ -393,7 +394,10 @@ const Wysiwyg = Widget.extend({ }, }); if (isBackground) { - $(el).css('background-image', `url('${newAttachmentSrc}')`); + const parts = weUtils.backgroundImageCssToParts($(el).css('background-image')); + parts.url = `url('${newAttachmentSrc}')`; + const combined = weUtils.backgroundImagePartsToCss(parts); + $(el).css('background-image', combined); delete el.dataset.bgSrc; } else { el.setAttribute('src', newAttachmentSrc); @@ -832,10 +836,19 @@ const Wysiwyg = Widget.extend({ const targetElement = targetNode && targetNode.nodeType === Node.ELEMENT_NODE ? targetNode : targetNode && targetNode.parentNode; + const backgroundImage = $(targetElement).css('background-image'); + let backgroundGradient = false; + if (weUtils.isColorGradient(backgroundImage)) { + const textGradient = targetElement.classList.contains('text-gradient'); + if (eventName === "foreColor" && textGradient || eventName !== "foreColor" && !textGradient) { + backgroundGradient = backgroundImage; + } + } colorpicker = new ColorPaletteWidget(this, { excluded: ['transparent_grayscale'], $editable: $(this.odooEditor.editable), // Our parent is the root widget, we can't retrieve the editable section from it... - selectedColor: $(targetElement).css(eventName === "foreColor" ? 'color' : 'backgroundColor'), + selectedColor: backgroundGradient || $(targetElement).css(eventName === "foreColor" ? 'color' : 'backgroundColor'), + withGradients: true, }); colorpicker.on('custom_color_picked color_picked', null, ev => { this._processAndApplyColor(eventName, ev.data.color); @@ -869,7 +882,7 @@ const Wysiwyg = Widget.extend({ _processAndApplyColor: function (eventName, color) { if (!color) { color = 'inherit'; - } else if (!ColorpickerWidget.isCSSColor(color)) { + } else if (!ColorpickerWidget.isCSSColor(color) && !weUtils.isColorGradient(color)) { color = (eventName === "foreColor" ? 'text-' : 'bg-') + color; } this.odooEditor.execCommand('applyColor', color, eventName === 'foreColor' ? 'color' : 'backgroundColor', this.lastMediaClicked); @@ -881,6 +894,9 @@ const Wysiwyg = Widget.extend({ _colorToHex: function (color) { if (color.startsWith('#')) { return color; + } else if (weUtils.isColorGradient(color)) { + // return gradient the way it is: updateColorpickerLabels will handle it + return color; } else { let rgbColor; if (color.startsWith('rgb')) { diff --git a/addons/web_editor/static/src/scss/secondary_variables.scss b/addons/web_editor/static/src/scss/secondary_variables.scss index 5721398fbaa..33d5019ec82 100644 --- a/addons/web_editor/static/src/scss/secondary_variables.scss +++ b/addons/web_editor/static/src/scss/secondary_variables.scss @@ -21,6 +21,7 @@ $o-color-palette: map-get($o-color-palettes, $o-color-palette-name) or (); // Original color palette can contain override of the default combinations (so keep 'null' values for this merge) $o-color-palette: map-merge($-combination-additions, $o-color-palette); $o-color-palette: map-merge($o-base-color-palette, o-map-omit($o-color-palette)); +$o-color-palette: o-map-force-nulls($o-color-palette); // Theme colors $o-theme-color-palette: map-get($o-theme-color-palettes, $o-theme-color-palette-name) or () !default; @@ -42,6 +43,7 @@ $o-theme-color-palette: map-merge(( 'dark': darken(desaturate($-main-color, 80%), min(70%, max(0%, $-main-color-lightness - 10%))), // Does not lower under 10% lightness ), o-map-omit($o-theme-color-palette)); $o-theme-color-palette: map-merge($o-base-theme-color-palette, o-map-omit($o-theme-color-palette)); +$o-theme-color-palette: o-map-force-nulls($o-theme-color-palette); // Gray colors // Extend grays with transparent ones (for some reason, BS4 create black-50 and @@ -49,6 +51,7 @@ $o-theme-color-palette: map-merge($o-base-theme-color-palette, o-map-omit($o-the $o-gray-color-palette: map-get($o-gray-color-palettes, $o-gray-color-palette-name) or () !default; $o-gray-color-palette: map-merge($o-transparent-grays, o-map-omit($o-gray-color-palette)); $o-gray-color-palette: map-merge($o-base-gray-color-palette, o-map-omit($o-gray-color-palette)); +$o-gray-color-palette: o-map-force-nulls($o-gray-color-palette); $o-color-system-initialized: false; diff --git a/addons/web_editor/static/src/scss/web_editor.common.scss b/addons/web_editor/static/src/scss/web_editor.common.scss index f973d776b56..f89ced84c4d 100644 --- a/addons/web_editor/static/src/scss/web_editor.common.scss +++ b/addons/web_editor/static/src/scss/web_editor.common.scss @@ -787,3 +787,11 @@ section, .oe_img_bg, [data-oe-shape-data] { .o_we_bg_filter { @extend %o-we-background-layer; } + +// Gradient +// TODO should be in the editor lib since it is handled there... but could not +// find the right place for it. +.text-gradient { + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} diff --git a/addons/web_editor/static/src/scss/web_editor.variables.scss b/addons/web_editor/static/src/scss/web_editor.variables.scss index 2add0579ddd..91f12e6b63c 100644 --- a/addons/web_editor/static/src/scss/web_editor.variables.scss +++ b/addons/web_editor/static/src/scss/web_editor.variables.scss @@ -446,6 +446,21 @@ $o-we-sidebar-content-field-toggle-control-shadow: 0 2px 3px 0 $o-we-bg-darkest // Edited content //------------------------------------------------------------------------------ +// Useful to force null values (in website, null values set by the user +// are removed to act as "removing the user custo and resetting to theme +// default": ideally, we should review that system to actually removing the +// custos in that case instead of setting a null value but this cannot be +// migrated at the moment). +@function o-map-force-nulls($map) { + $-map: (); + @each $key, $value in $map { + $-map: map-merge($-map, ( + $key: if($value == 'NULL', null, $value), + )); + } + @return $-map; +} + $o-support-13-0-color-system: false !default; $o-checklist-margin-left: 20px; @@ -482,10 +497,6 @@ $o-color-palettes: ( 'o-color-3': #F8F9FA, 'o-color-4': #FFFFFF, 'o-color-5': #343a40, - - 'menu': 2, - 'footer': 2, - 'copyright': 5, ), ) !default; $o-color-palette-name: 'base-1' !default; diff --git a/addons/web_editor/static/src/scss/wysiwyg.scss b/addons/web_editor/static/src/scss/wysiwyg.scss index fc9ac21e649..5bfbf05f60d 100644 --- a/addons/web_editor/static/src/scss/wysiwyg.scss +++ b/addons/web_editor/static/src/scss/wysiwyg.scss @@ -117,8 +117,15 @@ $o-we-zindex: $o-we-overlay-zindex + 1 !default; } // Specific elements - #colorInputButtonGroup label:last-of-type .btn { - margin: 0 1px 0 -1px; + #colorInputButtonGroup { + label:last-of-type .btn { + margin: 0 1px 0 -1px; + } + + .note-back-color-preview.dropup .dropdown-menu { + // Highlight palette alignment + left: -52px; + } } .oe-tablepicker { margin: 1px 2px -8px 2px !important; diff --git a/addons/web_editor/static/src/xml/snippets.xml b/addons/web_editor/static/src/xml/snippets.xml index b8ca8082c24..36045537b79 100644 --- a/addons/web_editor/static/src/xml/snippets.xml +++ b/addons/web_editor/static/src/xml/snippets.xml @@ -51,12 +51,17 @@ Theme - + + NoneReset + +
@@ -68,6 +73,7 @@
+
+
+ + +
diff --git a/addons/web_editor/views/snippets.xml b/addons/web_editor/views/snippets.xml index 85216b32511..a632ffdf700 100644 --- a/addons/web_editor/views/snippets.xml +++ b/addons/web_editor/views/snippets.xml @@ -123,10 +123,12 @@ @@ -145,13 +147,6 @@ data-no-preview="true"> - - - -
- + + + data-opacity="0.5" + data-with-gradients="1" + data-selected-tab="gradients" + data-excluded="theme, common" + />
v || 'null'), + 'values': _.mapObject(values, v => v || defaultValue), }, }); }, @@ -700,6 +709,7 @@ options.registry.BackgroundToggler.include({ */ toggleBgVideo(previewMode, widgetValue, params) { if (!widgetValue) { + this.$target.find('> .o_we_bg_filter').remove(); // TODO: use setWidgetValue instead of calling background directly when possible const [bgVideoWidget] = this._requestUserValueWidgets('bg_video_opt'); const bgVideoOpt = bgVideoWidget.getParent(); @@ -1946,6 +1956,54 @@ options.registry.collapse = options.Class.extend({ }, }); +options.registry.WebsiteLevelColor = options.Class.extend({ + specialCheckAndReloadMethodsNames: options.Class.prototype.specialCheckAndReloadMethodsNames + .concat(['customizeWebsiteLayer2Color']), + + /** + * @see this.selectClass for parameters + */ + async customizeWebsiteLayer2Color(previewMode, widgetValue, params) { + if (previewMode) { + return; + } + params.color = params.layerColor; + params.variable = params.layerGradient; + let color = undefined; + let gradient = undefined; + if (weUtils.isColorGradient(widgetValue)) { + color = ''; + gradient = widgetValue; + } else { + color = widgetValue; + gradient = ''; + } + await this.customizeWebsiteVariable(previewMode, gradient, params); + params.noBundleReload = false; + return this.customizeWebsiteColor(previewMode, color, params); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + async _computeWidgetState(methodName, params) { + if (methodName === 'customizeWebsiteLayer2Color') { + params.variable = params.layerGradient; + const gradient = await this._computeWidgetState('customizeWebsiteVariable', params); + if (gradient) { + return gradient.substring(1, gradient.length - 1); // Unquote + } + params.color = params.layerColor; + return this._computeWidgetState('customizeWebsiteColor', params); + } + return this._super(...arguments); + }, +}); + options.registry.HeaderNavbar = options.Class.extend({ /** * Particular case: we want the option to be associated on the header navbar @@ -2633,10 +2691,23 @@ options.registry.CoverProperties = options.Class.extend({ name: 'bg_color_opt', onSuccess: _widget => colorPickerWidget = _widget, }); - const color = colorPickerWidget._value; - const isCSSColor = ColorpickerWidget.isCSSColor(color); - this.$target[0].dataset.bgColorClass = isCSSColor ? '' : weUtils.computeColorClasses([color])[0]; - this.$target[0].dataset.bgColorStyle = isCSSColor ? `background-color: ${color};` : ''; + // TODO there is probably a better way and this should be refactored to + // use more standard colorpicker+imagepicker structure + const ccValue = colorPickerWidget._ccValue; + const colorOrGradient = colorPickerWidget._value; + const isGradient = weUtils.isColorGradient(colorOrGradient); + const isCSSColor = !isGradient && ColorpickerWidget.isCSSColor(colorOrGradient); + const colorNames = []; + if (ccValue) { + colorNames.push(ccValue); + } + if (!isGradient && !isCSSColor) { + colorNames.push(colorOrGradient); + } + this.$target[0].dataset.bgColorClass = weUtils.computeColorClasses(colorNames).join(' '); + this.$target[0].dataset.bgColorStyle = + isCSSColor ? `background-color: ${colorOrGradient};` : + isGradient ? `background-color: rgba(0, 0, 0, 0); background-image: ${colorOrGradient};` : ''; }, //-------------------------------------------------------------------------- diff --git a/addons/website/static/src/scss/primary_variables.scss b/addons/website/static/src/scss/primary_variables.scss index 1652d53f2f1..2b85ef6056b 100644 --- a/addons/website/static/src/scss/primary_variables.scss +++ b/addons/website/static/src/scss/primary_variables.scss @@ -6,10 +6,14 @@ $o-base-color-palette: map-merge($o-base-color-palette, ( 'body': $o-portal-default-body-bg, 'menu': 1, // o_cc1 + 'menu-custom': null, 'menu-border-color': null, // Default to classes used on the template - 'header-boxed': '200', + 'header-boxed': null, + 'header-boxed-custom': '200', 'footer': 5, // o_cc5 - 'copyright': 'black-15', + 'footer-custom': null, + 'copyright': null, + 'copyright-custom': null, )); // By default, all user color palette values are null. Each null value is @@ -42,6 +46,14 @@ $o-theme-figcaption-opacity: 0.6; $o-color-palettes: map-merge($o-color-palettes, ( + 'base-1': map-merge(map-get($o-color-palettes, 'base-1'), ( + 'copyright-custom': 'black-15', + )), + 'base-2': map-merge(map-get($o-color-palettes, 'base-2'), ( + 'menu': 2, + 'footer': 2, + 'copyright': 5, + )), 'generic-1': ( 'o-color-1': #984c46, 'o-color-2': #23323b, @@ -51,6 +63,7 @@ $o-color-palettes: map-merge($o-color-palettes, 'menu': 3, 'footer': 3, + 'copyright-custom': 'black-15', ), 'generic-2': ( 'o-color-1': #B99932, @@ -68,6 +81,8 @@ $o-color-palettes: map-merge($o-color-palettes, 'o-color-3': #fdf8ef, 'o-color-4': #FFFFFF, 'o-color-5': #212c39, + + 'copyright-custom': 'black-15', ), 'generic-4': ( 'o-color-1': #6E7993, @@ -77,6 +92,7 @@ $o-color-palettes: map-merge($o-color-palettes, 'o-color-5': #313347, 'menu': 5, + 'copyright-custom': 'black-15', ), 'generic-5': ( 'o-color-1': #F7CF41, @@ -87,6 +103,7 @@ $o-color-palettes: map-merge($o-color-palettes, 'menu': 3, 'footer': 3, + 'copyright-custom': 'black-15', ), 'generic-6': ( 'o-color-1': #45859A, @@ -139,6 +156,7 @@ $o-color-palettes: map-merge($o-color-palettes, 'o-color-5': #000000, 'menu': 5, + 'copyright-custom': 'black-15', ), 'generic-10': ( 'o-color-1': #b56355, @@ -148,6 +166,7 @@ $o-color-palettes: map-merge($o-color-palettes, 'o-color-5': #343733, 'footer': 2, + 'copyright-custom': 'black-15', ), 'generic-11': ( 'o-color-1': #01ACAB, @@ -157,6 +176,7 @@ $o-color-palettes: map-merge($o-color-palettes, 'o-color-5': #000000, 'footer': 1, + 'copyright-custom': 'black-15', ), 'generic-12': ( 'o-color-1': #926190, @@ -172,6 +192,8 @@ $o-color-palettes: map-merge($o-color-palettes, 'o-cc5-headings': 'o-color-4', 'o-cc5-link': 'o-color-4', 'o-cc5-text': rgba(#fff, .6), + + 'copyright-custom': 'black-15', ), 'generic-13': ( 'o-color-1': #478FA2, @@ -240,6 +262,7 @@ $o-color-palettes: map-merge($o-color-palettes, 'menu': 5, 'footer': 5, + 'copyright-custom': 'black-15', ), 'anelusia-1': ( 'o-color-1': #000000, @@ -284,6 +307,8 @@ $o-color-palettes: map-merge($o-color-palettes, 'o-cc5-link': #218bdf, 'o-cc5-btn-primary': #218bdf, + + 'copyright-custom': 'black-15', ), 'anelusia-4': ( 'o-color-1': #dc143c, @@ -291,6 +316,8 @@ $o-color-palettes: map-merge($o-color-palettes, 'o-color-3': #faf7e1, 'o-color-4': #ffffff, 'o-color-5': #222222, + + 'copyright-custom': 'black-15', ), 'anelusia-5': ( 'o-color-1': #32c0c9, @@ -300,6 +327,7 @@ $o-color-palettes: map-merge($o-color-palettes, 'o-color-5': #2e2e2e, 'footer': 1, + 'copyright-custom': 'black-15', ), 'anelusia-6': ( 'o-color-1': #e27b92, @@ -313,6 +341,7 @@ $o-color-palettes: map-merge($o-color-palettes, 'menu': 2, 'footer': 2, + 'copyright-custom': 'black-15', ), 'artists-1': ( 'o-color-1': #ab44ff, @@ -324,6 +353,7 @@ $o-color-palettes: map-merge($o-color-palettes, 'o-cc5-text': 'o-color-3', 'menu': 5, + 'copyright-custom': 'black-15', ), 'artists-2': ( 'o-color-1': #1ad68f, @@ -333,6 +363,7 @@ $o-color-palettes: map-merge($o-color-palettes, 'o-color-5': #000000, 'menu': 2, + 'copyright-custom': 'black-15', ), 'artists-3': ( 'o-color-1': #007882, @@ -355,6 +386,7 @@ $o-color-palettes: map-merge($o-color-palettes, 'menu': 2, 'footer': 2, + 'copyright-custom': 'black-15', ), 'artists-5': ( 'o-color-1': #f48847, @@ -364,6 +396,7 @@ $o-color-palettes: map-merge($o-color-palettes, 'o-color-5': #000000, 'footer': 3, + 'copyright-custom': 'black-15', ), 'artists-6': ( 'o-color-1': #95d2e5, @@ -374,6 +407,7 @@ $o-color-palettes: map-merge($o-color-palettes, 'menu': 2, 'footer': 2, + 'copyright-custom': 'black-15', ), 'avantgarde-1': ( 'o-color-1': #ee4980, @@ -403,6 +437,7 @@ $o-color-palettes: map-merge($o-color-palettes, 'menu': 4, 'footer': 4, + 'copyright-custom': 'black-15', ), 'avantgarde-3': ( 'o-color-1': #f08e80, @@ -465,6 +500,8 @@ $o-color-palettes: map-merge($o-color-palettes, 'o-cc2-headings': 'o-color-2', 'o-cc3-headings': 'o-color-3', 'o-cc5-headings': 'o-color-1', + + 'copyright-custom': 'black-15', ), 'avantgarde-7': ( 'o-color-1': #f48847, @@ -475,6 +512,8 @@ $o-color-palettes: map-merge($o-color-palettes, 'o-cc2-headings': 'o-color-1', 'o-cc5-headings': 'o-color-1', + + 'copyright-custom': 'black-15', ), 'avantgarde-8': ( 'o-color-1': #278f84, @@ -485,6 +524,8 @@ $o-color-palettes: map-merge($o-color-palettes, 'o-cc2-headings': 'o-color-1', 'o-cc5-headings': 'o-color-1', + + 'copyright-custom': 'black-15', ), 'beauty-1': ( 'o-color-1': #df699c, @@ -553,6 +594,8 @@ $o-color-palettes: map-merge($o-color-palettes, 'o-color-5': #112625, 'o-cc2-link': 'o-color-5', + + 'copyright-custom': 'black-15', ), 'bewise-1': ( 'o-color-1': #162238, @@ -861,7 +904,7 @@ $o-color-palettes: map-merge($o-color-palettes, 'header': 1, 'footer': 3, - 'copyright': 'black-15', + 'copyright-custom': 'black-15', ), 'enark-1': ( 'o-color-1': #7b97af, @@ -873,6 +916,8 @@ $o-color-palettes: map-merge($o-color-palettes, 'o-cc1-text': 'o-color-2', 'o-cc1-btn-primary': 'o-color-2', 'o-cc5-link': 'o-color-4', + + 'copyright-custom': 'black-15', ), 'enark-2': ( 'o-color-1': #41cb7a, @@ -936,7 +981,7 @@ $o-color-palettes: map-merge($o-color-palettes, 'body': 'black-15', 'menu': 1, 'footer': 5, - 'copyright': 'black-25', + 'copyright-custom': 'black-25', ), 'graphene-2': ( 'o-color-1': #c8a47e, @@ -950,6 +995,8 @@ $o-color-palettes: map-merge($o-color-palettes, 'o-cc3-headings': 'o-color-4', 'o-cc4-headings': 'o-color-5', 'o-cc5-text': #b2b2b2, + + 'copyright-custom': 'black-15', ), 'graphene-3': ( 'o-color-1': #39588e, @@ -962,6 +1009,7 @@ $o-color-palettes: map-merge($o-color-palettes, 'menu': 2, 'footer': 2, + 'copyright-custom': 'black-15', ), 'graphene-4': ( 'o-color-1': #f48747, @@ -971,6 +1019,7 @@ $o-color-palettes: map-merge($o-color-palettes, 'o-color-5': #282d30, 'menu': 5, + 'copyright-custom': 'black-15', ), 'kea-1': ( 'o-color-1': #e991a3, @@ -1137,6 +1186,8 @@ $o-color-palettes: map-merge($o-color-palettes, 'o-color-3': #f5f5f5, 'o-color-4': #ffffff, 'o-color-5': #111111, + + 'copyright-custom': 'black-15', ), 'monglia-2': ( 'o-color-1': #8f8747, @@ -1144,6 +1195,8 @@ $o-color-palettes: map-merge($o-color-palettes, 'o-color-3': #f0ecdb, 'o-color-4': #ffffff, 'o-color-5': #21263a, + + 'copyright-custom': 'black-15', ), 'monglia-3': ( 'o-color-1': #c0ce75, @@ -1151,6 +1204,8 @@ $o-color-palettes: map-merge($o-color-palettes, 'o-color-3': #dedede, 'o-color-4': #ffffff, 'o-color-5': #3e3e3e, + + 'copyright-custom': 'black-15', ), 'monglia-4': ( 'o-color-1': #1ad68f, @@ -1158,6 +1213,8 @@ $o-color-palettes: map-merge($o-color-palettes, 'o-color-3': #cfcfcf, 'o-color-4': #ffffff, 'o-color-5': #000000, + + 'copyright-custom': 'black-15', ), 'monglia-5': ( 'o-color-1': #c5ca60, @@ -1506,6 +1563,7 @@ $o-color-palettes: map-merge($o-color-palettes, 'o-cc5-link': #70c7cf, 'menu': 5, + 'copyright-custom': 'black-15', ), 'treehouse-1': ( 'o-color-1': #68b581, @@ -1868,6 +1926,12 @@ $o-base-website-values-palette: ( 'navbar-font': null, 'buttons-font': null, + // Gradients + 'menu-gradient': null, + 'header-boxed-gradient': null, + 'footer-gradient': null, + 'copyright-gradient': null, + 'header-template': 'default', // 'default' / 'hamburger' / 'vertical' / 'sidebar' 'header-font-size': null, // Default to BS (normal font-size) 'header-links-style': 'default', // 'default' / 'fill' / 'outline' / 'pills' / 'block' / 'border-bottom' @@ -1985,3 +2049,11 @@ $o-theme-font-configs: ( @include o-position-absolute($top: 0, $left: 0); padding: 0.25rem 1rem; }; + +@mixin o-add-gradient($key) { + $-gradient: o-website-value($key); + @if $-gradient { + background-color: rgba(0, 0, 0, 0); + background-image: $-gradient; + } +}; diff --git a/addons/website/static/src/scss/secondary_variables.scss b/addons/website/static/src/scss/secondary_variables.scss index a8ca40bc8f3..f3e48ff02c2 100644 --- a/addons/website/static/src/scss/secondary_variables.scss +++ b/addons/website/static/src/scss/secondary_variables.scss @@ -67,7 +67,7 @@ $o-website-values-palettes: append($o-website-values-palettes, $-actual-user-web // Enable last website values palette, which is now the user customized one $o-website-values-palette-number: length($o-website-values-palettes); -$o-website-values: $-actual-user-website-values-palette !default; +$o-website-values: o-map-force-nulls($-actual-user-website-values-palette) !default; @function o-website-value($key) { @return map-get($o-website-values, $key); } @@ -134,6 +134,27 @@ $o-has-customized-colors: // palette. $-palette-default: map-get($o-color-palettes, $o-color-palette-name) or (); $-actual-user-color-palette: map-merge($-palette-default, o-map-omit($o-user-color-palette)); +// Compatibility with old values in old names +@each $name, $custom-name in ( + // Each of those values were either a number for a color combination, a + // string for a color name or a color. Now they should only be a number for + // a color combination and the other value types for the color name/value + // are handled by another variable. That variable can also contain a string + // for a color gradient. + 'menu': 'menu-custom', + 'header-boxed': 'header-boxed-custom', + 'footer': 'footer-custom', + 'copyright': 'copyright-custom' +) { + $-base-value: map-get($-actual-user-color-palette, $name); + @if $-base-value and $-base-value != 'NULL' and type-of($-base-value) != 'number' { + $-base-custom-value: map-get($-actual-user-color-palette, $custom-name); + $-actual-user-color-palette: map-merge($-actual-user-color-palette, ( + $name: 1, + $custom-name: $-base-custom-value or $-base-value, + )); + } +} $o-color-palettes: map-merge($o-color-palettes, ('user-palette': $-actual-user-color-palette)); // Gray palette @@ -174,6 +195,7 @@ $-actual-user-theme-color-palette: map-remove($-actual-user-theme-color-palette, 'secondary' ); $o-theme-color-palettes: map-merge($o-theme-color-palettes, ('user-palette': $-actual-user-theme-color-palette)); + // --- // Enable last color and theme color palettes, which are now the user customized diff --git a/addons/website/static/src/scss/website.scss b/addons/website/static/src/scss/website.scss index c2389d2e867..a9023af7581 100644 --- a/addons/website/static/src/scss/website.scss +++ b/addons/website/static/src/scss/website.scss @@ -136,7 +136,8 @@ $-seen-urls: (); } // Vertical alignment when top-menu has visually "no background" - @if o-color('menu') == o-color('body') { + $-menu-color: o-color('menu-custom') or o-color('menu'); + @if (not $-menu-color or $-menu-color == o-color('body')) { > header { .navbar, .container { padding-left: 0; @@ -226,9 +227,12 @@ $-seen-urls: (); // Note: this only works because navbar-light is defined before navbar-dark, // we may want to use a safest way when possible. @include o-apply-colors('menu'); - @if (color-yiq(o-color('menu')) != $yiq-text-dark) { + @include o-apply-colors('menu-custom'); + $-menu-color: o-color('menu-custom') or o-color('menu'); + @if ($-menu-color and color-yiq($-menu-color) != $yiq-text-dark) { @extend .navbar-dark; } + @include o-add-gradient('menu-gradient'); } $-header-nav-link-height: $nav-link-height; @@ -258,9 +262,14 @@ $-header-nav-link-height: $nav-link-height; .o_footer { @include o-apply-colors('footer'); + @include o-apply-colors('footer-custom'); + @include o-add-gradient('footer-gradient'); .o_footer_copyright { - @include o-apply-colors('copyright', $background: o-color('footer')); + $-footer-color: o-color('footer-custom') or o-color('footer'); + @include o-apply-colors('copyright', $background: $-footer-color); + @include o-apply-colors('copyright-custom', $background: $-footer-color); + @include o-add-gradient('copyright-gradient'); } } @@ -286,7 +295,7 @@ h6 { } // Texts -font[style*='background'], +font[style*='background']:not(.text-gradient), font[class*='bg-'] { padding: 2px 6px 4px; } @@ -946,7 +955,8 @@ header { } min-width: 250px; margin: 0 !important; - background-color: o-color('menu'); + background-color: o-color('menu-custom') or o-color('menu'); + @include o-add-gradient('menu-gradient'); transition: transform $-transition-duration cubic-bezier(.694, .0482, .335, 1); @if $-hamburger-center { @@ -1102,6 +1112,8 @@ header { } @else if o-website-value('header-template') == 'boxed' { #wrapwrap:not(.o_header_overlay) .o_header_boxed_background { @include o-apply-colors('header-boxed'); + @include o-apply-colors('header-boxed-custom'); + @include o-add-gradient('header-boxed-gradient'); } } @else if o-website-value('header-template') == 'centered_logo' { header .o_header_centered_logo { @@ -1280,8 +1292,8 @@ header { // Footer scrolltop button @if o-website-value('footer-scrolltop') { #o_footer_scrolltop { - $-footer-color: o-color('footer'); - $-copyright-color: o-color('copyright'); + $-footer-color: o-color('footer-custom') or o-color('footer'); + $-copyright-color: o-color('copyright-custom') or o-color('copyright'); $-copyright-color: mix(rgba($-copyright-color, 1.0), $-footer-color, percentage(alpha($-copyright-color))); box-sizing: content-box; @@ -1290,6 +1302,7 @@ header { border: 0; padding: 0; @include o-apply-colors('footer', $with-extras: false, $background: $-footer-color); + @include o-apply-colors('footer-custom', $with-extras: false, $background: $-footer-color); text-decoration: none; @if $-footer-color == $-copyright-color { diff --git a/addons/website/static/tests/tours/snippet_background_edition.js b/addons/website/static/tests/tours/snippet_background_edition.js new file mode 100644 index 00000000000..123ae91be58 --- /dev/null +++ b/addons/website/static/tests/tours/snippet_background_edition.js @@ -0,0 +1,262 @@ +/** @odoo-module */ + +import tour from 'web_tour.tour'; +import weUtils from 'web_editor.utils'; +import wTourUtils from 'website.tour_utils'; + +const snippets = [ + { + id: 's_text_image', + name: 'Text - Image', + }, +]; +const gradients = [ + 'linear-gradient(135deg, rgb(203, 94, 238) 0%, rgb(75, 225, 236) 100%)', + 'linear-gradient(135deg, rgb(255, 222, 202) 0%, rgb(202, 115, 69) 100%)', +]; + +function checkAndUpdateBackgroundColor({ + checkCC, checkNoCC, + checkBg, checkNoBg, + checkGradient, checkNoGradient, + changeType, change, + finalSelector, finalRun +}) { + const steps = [ + wTourUtils.changeBackgroundColor(), + ]; + + function typeToName(xType) { + return xType === 'cc' ? 'color combinations' : xType === 'bg' ? 'background colors' : 'gradients'; + } + + function switchTo(type, _name) { + const target = type === 'cc' ? 'color-combinations' : type === 'bg' ? 'custom-colors' : 'gradients'; + const name = _name || typeToName(type); + steps.push({ + trigger: `.o_we_colorpicker_switch_pane_btn[data-target="${target}"]`, + content: `Switch to ${name}`, + }); + } + + function check(checkX, checkNoX, xType, noSwitch = false) { + if (!checkX && !checkNoX) { + return; + } + + const name = typeToName(xType); + const selectorCheckX = checkX && `.o_we_color_btn[data-color="${checkX}"].selected`; + const selectorCheckNoX = checkNoX && `.o_we_color_btn[data-color="${checkNoX}"]:not(.selected)`; + const step = { + trigger: selectorCheckX || selectorCheckNoX, + content: `The correct ${name} is marked as selected`, + position: 'bottom', + run: () => null, + }; + if (!selectorCheckX && selectorCheckNoX) { + step.extra_trigger = selectorCheckNoX; + } + + if (!noSwitch) { + switchTo(xType, name); + } + steps.push(step); + } + + check(checkCC, checkNoCC, 'cc', true); + check(checkBg, checkNoBg, 'bg'); + check(checkGradient, checkNoGradient, 'gradient'); + + if (changeType) { + switchTo(changeType); + steps.push(wTourUtils.changeOption('ColoredLevelBackground', `.o_we_color_btn[data-color="${change}"]`)); + steps.push({ + trigger: finalSelector, + content: "The selected colors have been applied (CC AND (BG or GRADIENT))", + position: 'bottom', + run: finalRun, + }); + } + + return steps; +} + +tour.register('snippet_background_edition', { + test: true, + url: '/?enable_editor=1', +}, +[ +wTourUtils.dragNDrop(snippets[0]), +wTourUtils.clickOnSnippet(snippets[0]), + +// Add a color combination +...checkAndUpdateBackgroundColor({ + changeType: 'cc', + change: 3, + finalSelector: `.${snippets[0].id}.o_cc.o_cc3:not([class*=bg-]):not([style*="background"])`, +}), + +// Change the color combination + Check the previous one was marked as selected +...checkAndUpdateBackgroundColor({ + checkCC: 3, + changeType: 'cc', + change: 2, + finalSelector: `.${snippets[0].id}.o_cc.o_cc2:not(.o_cc3):not([class*=bg-])`, +}), + +// Check the color combination was marked as selected + Edit the bg color +...checkAndUpdateBackgroundColor({ + checkCC: 2, + checkNoCC: 3, + changeType: 'bg', + change: 'black-50', + finalSelector: `.${snippets[0].id}.o_cc.o_cc2.bg-black-50`, +}), + +// Check the current color palette selection + Change the bg color +...checkAndUpdateBackgroundColor({ + checkCC: 2, + checkBg: 'black-50', + changeType: 'bg', + change: '800', + finalSelector: `.${snippets[0].id}.o_cc.o_cc2.bg-800:not(.bg-black-50)`, +}), + +// Check the current color palette selection + Change the color combination +// again. It should keep the bg color class. +...checkAndUpdateBackgroundColor({ + checkCC: 2, + checkBg: '800', + checkNoBg: 'black-50', + changeType: 'cc', + change: 4, + finalSelector: `.${snippets[0].id}.o_cc.o_cc4:not(.o_cc2).bg-800`, +}), + +// Check the current color palette status + Replace the bg color by a gradient +...checkAndUpdateBackgroundColor({ + checkCC: 4, + checkNoCC: 2, + checkBg: '800', + changeType: 'gradient', + change: gradients[0], + finalSelector: `.${snippets[0].id}.o_cc.o_cc4:not(.bg-800)[style*="background-image: ${gradients[0]}"]`, +}), + +// Check the current color palette status + Replace the gradient +...checkAndUpdateBackgroundColor({ + checkCC: 4, + checkNoBg: '800', + checkGradient: gradients[0], + changeType: 'gradient', + change: gradients[1], + finalSelector: `.${snippets[0].id}.o_cc.o_cc4[style*="background-image: ${gradients[1]}"]:not([style*="background-image: ${gradients[0]}"])`, +}), + +// Check the current color palette selection + Change the color combination +// again. It should keep the gradient. +...checkAndUpdateBackgroundColor({ + checkCC: 4, + checkGradient: gradients[1], + checkNoGradient: gradients[0], + changeType: 'cc', + change: 1, + finalSelector: `.${snippets[0].id}.o_cc.o_cc1:not(.o_cc4)[style*="background-image: ${gradients[1]}"]`, +}), + +// Final check of the color status in the color palette +...checkAndUpdateBackgroundColor({ + checkCC: 1, + checkNoCC: 4, + checkGradient: gradients[1], +}), + +// Now, add an image on top of that color combination + gradient +wTourUtils.changeOption('ColoredLevelBackground', '[data-name="bg_image_toggle_opt"]'), +{ + trigger: '.o_existing_attachment_cell img', + content: "Select an image in the media dialog", +}, +{ + trigger: `.${snippets[0].id}.o_cc.o_cc1`, + run: function () { + const parts = weUtils.backgroundImageCssToParts(this.$anchor.css('background-image')); + if (!parts.url || !parts.url.startsWith('url(')) { + console.error('An image should have been added as background.'); + } + if (parts.gradient !== gradients[1]) { + console.error('The gradient should have been kept when adding the background image'); + } + }, +}, + +// Replace the gradient while there is a background-image +...checkAndUpdateBackgroundColor({ + checkCC: 1, + checkGradient: gradients[1], + changeType: 'gradient', + change: gradients[0], + finalSelector: `.${snippets[0].id}.o_cc.o_cc1:not([style*="${gradients[1]}"])`, + finalRun: function () { + const parts = weUtils.backgroundImageCssToParts(this.$anchor.css('background-image')); + if (!parts.url || !parts.url.startsWith('url(')) { + console.error('The image should have been kept when changing the gradient'); + } + if (parts.gradient !== gradients[0]) { + console.error('The gradient should have been changed'); + } + }, +}), + +// Replace the gradient by a bg color +...checkAndUpdateBackgroundColor({ + checkCC: 1, + checkGradient: gradients[0], + checkNoGradient: gradients[1], + changeType: 'bg', + change: 'black-75', + finalSelector: `.${snippets[0].id}.o_cc.o_cc1.bg-black-75[style^="background-image: url("]:not([style*="${gradients[0]}"])` +}), + +// Re-add a gradient +...checkAndUpdateBackgroundColor({ + checkCC: 1, + checkBg: 'black-75', + checkNoGradient: gradients[0], + changeType: 'gradient', + change: gradients[1], + finalSelector: `.${snippets[0].id}.o_cc.o_cc1:not(.bg-black-75)`, + finalRun: function () { + const parts = weUtils.backgroundImageCssToParts(this.$anchor.css('background-image')); + if (!parts.url || !parts.url.startsWith('url(')) { + console.error('The image should have been kept when re-adding the gradient'); + } + if (parts.gradient !== gradients[1]) { + console.error('The gradient should have been re-added'); + } + }, +}), + +// Final check of color selection and removing the image +...checkAndUpdateBackgroundColor({ + checkCC: 1, + checkNoBg: 'black-75', + checkGradient: gradients[1], +}), +wTourUtils.changeOption('ColoredLevelBackground', '[data-name="bg_image_toggle_opt"]'), +{ + trigger: `.${snippets[0].id}.o_cc.o_cc1[style*="background-image: ${gradients[1]}"]`, + run: () => null, +}, + +// Now removing all colors via the 'None' button (note: colorpicker still opened) +{ + trigger: '.o_colorpicker_reset', + content: "Click on the None button of the color palette", +}, +{ + trigger: `.${snippets[0].id}:not(.o_cc):not(.o_cc1):not([style*="background-image"])`, + content: "All color classes and properties should have been removed", + run: () => null, +} +]); diff --git a/addons/website/static/tests/tours/snippet_empty_parent_autoremove.js b/addons/website/static/tests/tours/snippet_empty_parent_autoremove.js index 6ddc1fa48b4..2b3671b705e 100644 --- a/addons/website/static/tests/tours/snippet_empty_parent_autoremove.js +++ b/addons/website/static/tests/tours/snippet_empty_parent_autoremove.js @@ -55,12 +55,6 @@ tour.register('snippet_empty_parent_autoremove', { trigger: '#wrap .s_banner .o_we_shape', run: () => null, }, - wTourUtils.changeOption('ColoredLevelBackground', 'Filter'), - { - content: "Check that background-filter is present", - trigger: '#wrap .s_banner .o_we_bg_filter', - run: () => null, - }, { content: "Click on first column", trigger: '#wrap .s_banner .row > :first-child', diff --git a/addons/website/tests/test_ui.py b/addons/website/tests/test_ui.py index 4aed9d6758e..c42647ba849 100644 --- a/addons/website/tests/test_ui.py +++ b/addons/website/tests/test_ui.py @@ -206,3 +206,13 @@ class TestUi(odoo.tests.HttpCase): def test_10_website_conditional_visibility(self): self.start_tour('/', 'conditional_visibility_1', login='admin') self.start_tour('/', 'conditional_visibility_2', login='admin') + + def test_11_website_snippet_background_edition(self): + self.env['ir.attachment'].create({ + 'public': True, + 'type': 'url', + 'url': '/web/image/123/test.png', + 'name': 'test.png', + 'mimetype': 'image/png', + }) + self.start_tour('/', 'snippet_background_edition', login='admin') diff --git a/addons/website/views/snippets/s_blockquote.xml b/addons/website/views/snippets/s_blockquote.xml index 75eb66d2cff..2e5d74b38d3 100644 --- a/addons/website/views/snippets/s_blockquote.xml +++ b/addons/website/views/snippets/s_blockquote.xml @@ -31,6 +31,7 @@ + diff --git a/addons/website/views/snippets/s_media_list.xml b/addons/website/views/snippets/s_media_list.xml index b786ab95f51..76e3499783d 100644 --- a/addons/website/views/snippets/s_media_list.xml +++ b/addons/website/views/snippets/s_media_list.xml @@ -62,6 +62,7 @@ +
diff --git a/addons/website/views/snippets/snippets.xml b/addons/website/views/snippets/snippets.xml index a9cf02988a9..bc99ba1f80d 100644 --- a/addons/website/views/snippets/snippets.xml +++ b/addons/website/views/snippets/snippets.xml @@ -190,8 +190,8 @@ - - + + @@ -384,6 +384,7 @@ +
+ @@ -489,6 +491,7 @@ + @@ -552,7 +555,8 @@ data-selector="[data-oe-expression='res_company.partner_id']" data-no-check="true"/> -
@@ -628,10 +632,18 @@ + data-customize-website-layer2-color="" data-layer-color="menu-custom" data-layer-gradient="menu-gradient" + data-no-bundle-reload="true" + data-null-value="'NULL'" + data-with-combinations="customizeWebsiteColor" + data-with-gradients="true"/> + data-customize-website-layer2-color="" data-layer-color="header-boxed-custom" data-layer-gradient="header-boxed-gradient" + data-no-bundle-reload="true" + data-null-value="'NULL'" + data-with-combinations="customizeWebsiteColor" + data-with-gradients="true"/>
@@ -834,7 +846,8 @@
-
@@ -878,7 +891,11 @@ + data-customize-website-layer2-color="" data-layer-color="footer-custom" data-layer-gradient="footer-gradient" + data-no-bundle-reload="true" + data-null-value="'NULL'" + data-with-combinations="customizeWebsiteColor" + data-with-gradients="true"/> -
+ data-customize-website-layer2-color="" data-layer-color="copyright-custom" data-layer-gradient="copyright-gradient" + data-no-bundle-reload="true" + data-null-value="'NULL'" + data-with-combinations="customizeWebsiteColor" + data-with-gradients="true"/> None @@ -985,6 +1007,7 @@ +