[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) <qsm@odoo.com>
Co-authored-by: Benoit Socias <bso@odoo.com>
This commit is contained in:
qsm-odoo
2021-08-17 17:43:50 +00:00
co-authored by Benoit Socias
parent 38754b2485
commit a48a30f954
27 changed files with 1094 additions and 205 deletions
@@ -449,4 +449,3 @@
}
}
}
@@ -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);
}
}
@@ -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) &&
@@ -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
//------------------------------------------------------------------------------
@@ -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,
};
});
@@ -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);
},
});
@@ -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);
},
});
@@ -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')) {
@@ -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;
@@ -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;
}
@@ -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;
@@ -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;
+10 -4
View File
@@ -51,12 +51,17 @@
<span>Theme</span>
</button>
<button type="button" tabindex="2" class="o_we_colorpicker_switch_pane_btn" data-target="custom-colors">
<span>Custom</span>
<span>Solid</span>
</button>
<button t-if="widget.resetButton" type="button" class="my-1 ml-auto o_we_color_btn o_colorpicker_reset">
<t t-if="widget.withCombinations">None</t>
<t t-else="">Reset</t>
<button type="button" tabindex="3" class="o_we_colorpicker_switch_pane_btn" data-target="gradients">
<span>Gradient</span>
</button>
<t t-if="widget.resetButton">
<t t-set="trash_title"><t t-if="widget.withCombinations">None</t><t t-else="">Reset</t></t>
<button type="button" class="my-1 ml-auto o_we_color_btn o_colorpicker_reset" t-att-title="trash_title">
<i class="fa fa-trash"/>
</button>
</t>
</div>
<div class="o_colorpicker_sections pt-2 px-2 pb-3 bg-black-25" data-color-tab="color-combinations">
<!-- List all Presets -->
@@ -68,6 +73,7 @@
</div>
<div class="o_colorpicker_sections py-3 px-2 bg-black-25" data-color-tab="theme-colors"/>
<div class="o_colorpicker_sections py-3 px-2 bg-black-25" data-color-tab="custom-colors"/>
<div class="o_colorpicker_sections py-3 px-2 bg-black-25" data-color-tab="gradients"/>
</div>
<t t-name="web_editor.colorpalette.combination.btn">
<button type="button" class="o_we_color_btn o_we_color_combination_btn"
@@ -297,7 +297,7 @@ QUnit.module('web_editor', {}, function () {
await testUtils.dom.click($('#toolbar .note-back-color-preview .o_we_color_btn.bg-o-color-3'));
assert.strictEqual($field.find('.note-editable').html(),
'<p>t<font style="background-color: rgb(0, 255, 255);">oto t</font><font style="" class=" bg-o-color-3">oto to</font>to</p><p>tata</p>',
'<p>t<font style="background-color: rgb(0, 255, 255);">oto t</font><font style="" class="bg-o-color-3">oto to</font>to</p><p>tata</p>',
"should have rendered the field correctly in edit");
form.destroy();
+15
View File
@@ -60,6 +60,21 @@
<button data-color="white-75"></button>
<button data-color="white-85"></button>
</div>
<div class="o_colorpicker_section" data-name="predefined_gradients">
<t t-set="gradients" t-value="[
'linear-gradient(135deg, rgb(255, 204, 51) 0%, rgb(226, 51, 255) 100%)',
'linear-gradient(135deg, rgb(102, 153, 255) 0%, rgb(255, 51, 102) 100%)',
'linear-gradient(135deg, rgb(47, 128, 237) 0%, rgb(178, 255, 218) 100%)',
'linear-gradient(135deg, rgb(203, 94, 238) 0%, rgb(75, 225, 236) 100%)',
'linear-gradient(135deg, rgb(214, 255, 127) 0%, rgb(0, 179, 204) 100%)',
'linear-gradient(135deg, rgb(255, 222, 69) 0%, rgb(69, 33, 0) 100%)',
'linear-gradient(135deg, rgb(222, 222, 222) 0%, rgb(69, 69, 69) 100%)',
'linear-gradient(135deg, rgb(255, 222, 202) 0%, rgb(202, 115, 69) 100%)'
]"/>
<t t-foreach="gradients" t-as="gradient">
<button class="w-50" t-attf-style="background-image: #{gradient};" t-att-data-color="gradient"/>
</t>
</div>
</colorpicker>
</template>
</odoo>
+15 -13
View File
@@ -123,10 +123,12 @@
<template id="snippet_options_background_color_widget">
<we-colorpicker title="Color"
data-name="bg_color_opt"
t-att-data-select-color-combination="'' if with_color_combinations else None"
data-select-style=""
data-css-property="background-color"
data-color-prefix="bg-"
t-att-data-with-combinations="with_color_combinations and 'true' or None"
t-att-data-with-combinations="with_color_combinations and 'selectColorCombination' or None"
t-att-data-with-gradients="with_gradients and 'true' or None"
t-att-data-css-compatible="css_compatible and 'true' or None"/>
</template>
@@ -145,13 +147,6 @@
data-no-preview="true">
<i class="fa fa-fw fa-camera"/>
</we-button>
<we-button title="Filter"
data-name="bg_filter_toggle_opt"
data-dependencies="bg_image_toggle_opt"
data-toggle-bg-filter="true"
data-no-preview="true">
<i class="fa fa-fw fa-paint-brush"/>
</we-button>
<we-button title="Shape"
data-toggle-bg-shape="true"
t-att-data-dependencies="images_dependencies"
@@ -210,15 +205,22 @@
</div>
<!-- Color filter -->
<div data-option-name="colorFilter"
<div t-att-data-js="with_colors and with_color_combinations and 'ColoredLevelBackground' or 'BackgroundToggler'"
t-att-data-selector="selector"
t-att-data-exclude="exclude"
t-att-data-target="target">
<we-colorpicker string="⌙ Color filter"
data-select-style="true"
data-css-property="background-color"
<t t-set="color_filter_dependencies" t-valuef="bg_image_toggle_opt"/>
<!-- &emsp; -->
<we-colorpicker string=" ⌙ Color filter"
t-att-data-dependencies="color_filter_dependencies"
data-name="bg_filter_opt"
data-select-filter-color=""
data-color-prefix="bg-"
data-apply-to=".o_we_bg_filter"/>
data-opacity="0.5"
data-with-gradients="1"
data-selected-tab="gradients"
data-excluded="theme, common"
/>
</div>
<div data-js="BackgroundShape" string="Shape"
+7
View File
@@ -35,6 +35,13 @@ class Assets(models.AbstractModel):
'warning': 'null',
'danger': 'null',
})
# Also reset gradients which are in the "website" values palette
self.make_scss_customization('/website/static/src/scss/options/user_values.scss', {
'menu-gradient': 'null',
'header-boxed-gradient': 'null',
'footer-gradient': 'null',
'copyright-gradient': 'null',
})
custom_url = self.make_custom_asset_file_url(url, 'web.assets_common')
updatedFileContent = self.get_asset_content(custom_url) or self.get_asset_content(url)
@@ -384,6 +384,7 @@ options.Class.include({
custom_events: _.extend({}, options.Class.prototype.custom_events || {}, {
'google_fonts_custo_request': '_onGoogleFontsCustoRequest',
}),
specialCheckAndReloadMethodsNames: ['customizeWebsiteViews', 'customizeWebsiteVariable', 'customizeWebsiteColor'],
//--------------------------------------------------------------------------
// Options
@@ -422,19 +423,27 @@ options.Class.include({
}
for (const widget of widgets) {
const methodsNames = widget.getMethodsNames();
if (!methodsNames.includes('customizeWebsiteViews')
&& !methodsNames.includes('customizeWebsiteVariable')
&& !methodsNames.includes('customizeWebsiteColor')) {
const specialMethodsNames = [];
for (const methodName of methodsNames) {
if (this.specialCheckAndReloadMethodsNames.includes(methodName)) {
specialMethodsNames.push(methodName);
}
}
if (!specialMethodsNames.length) {
continue;
}
let paramsReload = false;
if (widget.getMethodsParams('customizeWebsiteViews').reload
|| widget.getMethodsParams('customizeWebsiteVariable').reload
|| widget.getMethodsParams('customizeWebsiteColor').reload) {
paramsReload = true;
const isDebugAssets = config.isDebug('assets');
let paramsReload = isDebugAssets;
if (!isDebugAssets) {
for (const methodName of specialMethodsNames) {
if (widget.getMethodsParams(methodName).reload) {
paramsReload = true;
break;
}
}
}
if (paramsReload || config.isDebug('assets')) {
return (config.isDebug('assets') ? _t("It appears you are in debug=assets mode, all theme customization options require a page reload in this mode.") : true);
if (paramsReload) {
return (isDebugAssets ? _t("It appears you are in debug=assets mode, all theme customization options require a page reload in this mode.") : true);
}
}
return false;
@@ -498,7 +507,7 @@ options.Class.include({
}
}
if (params.reload || config.isDebug('assets')) {
if (params.reload || config.isDebug('assets') || params.noBundleReload) {
// Caller will reload the page, nothing needs to be done anymore.
return;
}
@@ -543,7 +552,7 @@ options.Class.include({
}
}
}
return this._makeSCSSCusto(url, finalColors);
return this._makeSCSSCusto(url, finalColors, params.nullValue);
},
/**
* @private
@@ -551,7 +560,7 @@ options.Class.include({
_customizeWebsiteVariable: async function (value, params) {
return this._makeSCSSCusto('/website/static/src/scss/options/user_values.scss', {
[params.variable]: value,
});
}, params.nullValue);
},
/**
* @private
@@ -582,12 +591,12 @@ options.Class.include({
/**
* @private
*/
_makeSCSSCusto: async function (url, values) {
_makeSCSSCusto: async function (url, values, defaultValue = 'null') {
return this._rpc({
route: '/website/make_scss_custo',
params: {
'url': url,
'values': _.mapObject(values, v => 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};` : '';
},
//--------------------------------------------------------------------------
@@ -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;
}
};
@@ -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
+20 -7
View File
@@ -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 {
@@ -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,
}
]);
@@ -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',
+10
View File
@@ -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')
@@ -31,6 +31,7 @@
<t t-set="target" t-value="'.s_blockquote_content'"/>
<t t-set="with_colors" t-value="True"/>
<t t-set="with_images" t-value="True"/>
<t t-set="with_gradients" t-value="True"/>
<t t-set="images_dependencies" t-value="'blockquote_cover_opt'"/>
</t>
@@ -62,6 +62,7 @@
<t t-set="with_colors" t-value="True"/>
<t t-set="with_images" t-value="False"/>
<t t-set="with_color_combinations" t-value="True"/>
<t t-set="with_gradients" t-value="True"/>
</t>
<div data-js="Box" data-selector=".s_media_list_item" data-target="&gt; .row">
<t t-call="website.snippet_options_border_widgets"/>
+32 -9
View File
@@ -190,8 +190,8 @@
<i class="fa fa-fw fa-film"/>
</we-button>
</xpath>
<xpath expr="//we-button[@data-toggle-bg-filter]" position="attributes">
<attribute name="data-dependencies" add="bg_video_toggler_opt" separator=","/>
<xpath expr="//t[@t-set='color_filter_dependencies']" position="after">
<t t-set="color_filter_dependencies" t-valuef="#{color_filter_dependencies}, bg_video_toggler_opt"/>
</xpath>
<xpath expr="//div[@data-js='BackgroundOptimize']" position="after">
<!-- Parallax -->
@@ -384,6 +384,7 @@
<t t-set="with_colors" t-value="True"/>
<t t-set="with_images" t-value="True"/>
<t t-set="css_compatible" t-value="True"/>
<t t-set="with_gradients" t-value="True"/>
</t>
<div data-js="Carousel"
data-selector="section"
@@ -481,6 +482,7 @@
<t t-set="with_colors" t-value="True"/>
<t t-set="with_images" t-value="True"/>
<t t-set="with_color_combinations" t-value="True"/>
<t t-set="with_gradients" t-value="True"/>
</t>
<t t-call="web_editor.snippet_options_background_options">
@@ -489,6 +491,7 @@
<t t-set="with_colors" t-value="True"/>
<t t-set="with_images" t-value="False"/>
<t t-set="with_color_combinations" t-value="True"/>
<t t-set="with_gradients" t-value="True"/>
</t>
<t t-call="web_editor.snippet_options_background_options">
@@ -552,7 +555,8 @@
data-selector="[data-oe-expression='res_company.partner_id']"
data-no-check="true"/>
<div data-selector="#wrapwrap > header"
<div data-js="WebsiteLevelColor"
data-selector="#wrapwrap > header"
data-no-check="true"
groups="website.group_website_designer">
<we-select string="Template" data-variable="header-template" data-reload="/">
@@ -628,10 +632,18 @@
<we-row string="Colors">
<we-colorpicker data-customize-website-color="" data-color="menu"
data-with-combinations="true"/>
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"/>
<we-colorpicker data-dependencies="header_boxed_opt"
data-customize-website-color="" data-color="header-boxed"
data-with-combinations="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"/>
</we-row>
</div>
@@ -834,7 +846,8 @@
</div>
<!-- Footer - Colors & Layouts -->
<div data-selector="#wrapwrap > footer"
<div data-js="WebsiteLevelColor"
data-selector="#wrapwrap > footer"
data-no-check="true"
groups="website.group_website_designer">
@@ -878,7 +891,11 @@
<!-- Colors -->
<we-colorpicker string="Colors"
data-customize-website-color="" data-color="footer"
data-with-combinations="true"/>
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"/>
<we-select string="Slideout Effect" data-variable="footer-effect" data-reload="/">
<we-button string="Regular"
data-customize-website-views=""
@@ -941,12 +958,17 @@
</div>
<!-- Copyright -->
<div data-selector=".o_footer_copyright"
<div data-js="WebsiteLevelColor"
data-selector=".o_footer_copyright"
data-no-check="true"
groups="website.group_website_designer">
<we-colorpicker string="Colors"
data-customize-website-color="" data-color="copyright"
data-with-combinations="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"/>
<we-select string="Language Selector" data-reload="/">
<we-button data-name="language_selector_none_opt"
data-customize-website-views="">None</we-button>
@@ -985,6 +1007,7 @@
<we-row string="Background" class="o_we_full_row">
<t t-call="web_editor.snippet_options_background_color_widget">
<t t-set="with_color_combinations" t-value="True"/>
<t t-set="with_gradients" t-value="True"/>
</t>
<we-button-group class="ml-auto">
<we-imagepicker title="Image" data-background="" data-button-style="true"/>