[IMP] web_editor, *: add new user value colorpicker widget

* web, mass_mailing, website

Introduced a new sub-widget for snippet options: we-colorpicker. This
allows to retrieve a color value (either a normalized css color or a
theme color name) to be used for a snippet option method.

This commit replaces the old 'colorpicker' snippet option whose behavior
is now simply achieved by defining an xml option using a we-colorpicker
widget in combination with the 'selectStyle' standard method.

This commit also adds the possibility to control the border-color of
elements alongside their border-width. Again, just a we-colorpicker
widget in combination with the 'selectStyle' standard method.

Note: this commit reviewed what the colorpalette palette widget
notifies. This is not a cssColor and a 'isClass' flag to say if it is
a css class or not. The colorpalette always returns a color string which
can be the name of a color like 'alpha'. This is up to the caller to
check if the color string is a css color or not with isCSSColor. In the
case it is not, the related code can read the actual color by reading
the related css variable.

Part of https://github.com/odoo/odoo/pull/40282
In preparation of task-2122935 and other tasks
This commit is contained in:
qsm-odoo
2019-12-01 15:49:57 +00:00
parent 216cf93b4d
commit fdac73e99d
12 changed files with 376 additions and 242 deletions
@@ -807,8 +807,12 @@
data-selector=".note-editable > div:not(.o_layout), .note-editable .oe_structure > div, td, th"
data-exclude=".o_mail_no_resize, .o_mail_no_options"/>
<div data-js="colorpicker" string="Background Color"
data-selector=".note-editable > div:not(.o_layout), .note-editable .oe_structure > div, td, th"
data-exclude=".o_mail_no_colorpicker, .o_mail_no_options"/>
<div data-selector=".note-editable > div:not(.o_layout), .note-editable .oe_structure > div, td, th"
data-exclude=".o_mail_no_colorpicker, .o_mail_no_options">
<we-colorpicker string="Background Color"
data-select-style="true"
data-css-property="background-color"
data-color-prefix="bg-"/>
</div>
</template>
</odoo>
@@ -15,7 +15,7 @@ var ColorpickerDialog = Dialog.extend({
'mousedown .o_color_pick_area': '_onMouseDownPicker',
'mousedown .o_color_slider': '_onMouseDownSlider',
'mousedown .o_opacity_slider': '_onMouseDownOpacitySlider',
'change .o_color_picker_inputs' : '_onChangeInputs',
'change .o_color_picker_inputs': '_onChangeInputs',
}),
/**
@@ -560,6 +560,9 @@ ColorpickerDialog.convertCSSColorToRgba = function (cssColor) {
};
}
// TODO maybe implement a support for receiving css color like 'red' or
// 'transparent' (which are now considered non-css color by isCSSColor...)
return false;
};
/**
@@ -580,6 +583,16 @@ ColorpickerDialog.normalizeCSSColor = function (cssColor) {
}
return ColorpickerDialog.convertRgbaToCSSColor(rgba.red, rgba.green, rgba.blue, rgba.opacity);
};
/**
* Checks if a given string is a css color.
*
* @static
* @param {string} cssColor
* @returns {boolean}
*/
ColorpickerDialog.isCSSColor = function (cssColor) {
return ColorpickerDialog.convertCSSColorToRgba(cssColor) !== false;
};
return ColorpickerDialog;
});
@@ -14,6 +14,7 @@ const ColorpickerDialog = require('web.ColorpickerDialog');
const CSS_SHORTHANDS = {
'border-width': ['border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width'],
'border-radius': ['border-top-left-radius', 'border-top-right-radius', 'border-bottom-right-radius', 'border-bottom-left-radius'],
'border-color': ['border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color'],
};
/**
* Key-value mapping to list converters from an unit A to an unit B.
@@ -4,6 +4,7 @@ odoo.define('web_editor.rte.summernote', function (require) {
var Class = require('web.Class');
const concurrency = require('web.concurrency');
var core = require('web.core');
const ColorpickerDialog = require('web.ColorpickerDialog');
var ColorPaletteWidget = require('web_editor.ColorPalette').ColorPaletteWidget;
var mixins = require('web.mixins');
var fonts = require('wysiwyg.fonts');
@@ -50,13 +51,17 @@ renderer.createPalette = function ($container, options) {
const targetNode = r.sc;
const targetElement = targetNode.nodeType === Node.ELEMENT_NODE ? targetNode : targetNode.parentNode;
colorpicker = new ColorPaletteWidget(parent, {
colorPrefix: eventName === "foreColor" ? 'text-' : 'bg-',
excluded: ['transparent_grayscale'],
$editable: rte.Class.prototype.editable(), // Our parent is the root widget, we can't retrieve the editable section from it...
selectedColor: $(targetElement).css(eventName === "foreColor" ? 'color' : 'backgroundColor'),
targetClasses: [...targetElement.classList],
});
colorpicker.on('color_picked', null, ev => applyColor(ev.data.target, eventName, ev.data.cssColor));
colorpicker.on('color_picked', null, ev => {
let color = ev.data.color;
if (!ColorpickerDialog.isCSSColor(color)) {
color = (eventName === "foreColor" ? 'text-' : 'bg-') + color;
}
applyColor(ev.data.target, eventName, color);
});
colorpicker.on('color_reset', null, ev => applyColor(ev.data.target, eventName, 'inherit'));
return colorpicker.replace(hookEl).then(() => {
if (oldColorpicker) {
@@ -567,6 +567,7 @@ const SelectUserValueWidget = UserValueWidget.extend({
return;
}
}
this._super(...arguments);
},
//--------------------------------------------------------------------------
@@ -827,16 +828,169 @@ const MultiUserValueWidget = UserValueWidget.extend({
},
});
const ColorpickerUserValueWidget = SelectUserValueWidget.extend({ // FIXME should be reloaded on focus
custom_events: {
'color_picked': '_onColorPicked',
'color_hover': '_onColorHovered',
'color_leave': '_onColorLeft',
'color_reset': '_onColorReset',
},
/**
* @override
*/
init: function () {
this._super(...arguments);
if (!this.title) {
this.title = ' ';
}
},
/**
* @override
*/
start: async function () {
const _super = this._super.bind(this);
const args = arguments;
this.el.classList.add('o_we_so_color_palette');
// Pre-instanciate the color palette widget
await this._renderColorPalette();
// Build the select element with a custom span to hold the color preview
this.colorPreviewEl = document.createElement('span');
this.options.childNodes = [this.colorPalette.el];
this.options.valueEl = this.colorPreviewEl;
return _super(...args);
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @override
*/
getMethodsParams: function () {
return _.extend(this._super(...arguments), {
colorNames: this.colorPalette.getColorNames(),
});
},
/**
* @override
*/
getValue: function (methodName) {
return this._previewColor || this._super(...arguments);
},
/**
* @override
*/
isContainer: function () {
return false;
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @private
* @returns {Promise}
*/
_renderColorPalette: function () {
const options = {};
options.selectedColor = this._value;
if (this.options.dataAttributes.excluded) {
options.excluded = this.options.dataAttributes.excluded.replace(/ /g, '').split(',');
}
const oldColorPalette = this.colorPalette;
this.colorPalette = new ColorPaletteWidget(this, options);
if (oldColorPalette) {
return this.colorPalette.insertAfter(oldColorPalette.el).then(() => {
oldColorPalette.destroy();
});
}
return this.colorPalette.appendTo(document.createDocumentFragment());
},
/**
* Updates the color preview + re-render the whole color palette widget.
*
* @override
*/
_updateUI: async function (color) {
await this._super(...arguments);
this.colorPreviewEl.classList.remove(...this.colorPalette.getColorNames().map(c => 'bg-' + c));
this.colorPreviewEl.style.removeProperty('background-color');
if (this._value) {
if (ColorpickerDialog.isCSSColor(this._value)) {
this.colorPreviewEl.style.backgroundColor = this._value;
} else {
this.colorPreviewEl.classList.add('bg-' + this._value);
}
}
await this._renderColorPalette();
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Called when a color button is clicked -> confirms the preview.
*
* @private
* @param {Event} ev
*/
_onColorPicked: function (ev) {
this._previewColor = false;
this._value = ev.data.color;
this._notifyValueChange(false);
},
/**
* Called when a color button is entered -> previews the background color.
*
* @private
* @param {Event} ev
*/
_onColorHovered: function (ev) {
this._previewColor = ev.data.color;
this._notifyValueChange(true);
},
/**
* Called when a color button is left -> cancels the preview.
*
* @private
* @param {Event} ev
*/
_onColorLeft: function (ev) {
this._previewColor = false;
this._notifyValueChange('reset');
},
/**
* Called when the color reset button is clicked -> removes all color
* classes and color styles.
*
* @private
*/
_onColorReset: function () {
this._value = '';
this._notifyValueChange(false);
},
});
const userValueWidgetsRegistry = {
'we-button': ButtonUserValueWidget,
'we-checkbox': CheckboxUserValueWidget,
'we-select': SelectUserValueWidget,
'we-input': InputUserValueWidget,
'we-multi': MultiUserValueWidget,
'we-colorpicker': ColorpickerUserValueWidget,
};
//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
/**
* Handles a set of options for one snippet. The registry returned by this
* module contains the names of the specialized SnippetOptionWidget which can be
@@ -1024,7 +1178,54 @@ const SnippetOptionWidget = Widget.extend({
* @param {Object} params
*/
selectStyle: function (previewMode, widgetValue, params) {
if (params.cssProperty === 'background-color') {
this.$target.trigger('background-color-event', previewMode);
}
const 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);
}
// 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).
if (params.colorNames && params.colorPrefix) {
const classes = params.colorNames.map(c => params.colorPrefix + c);
this.$target[0].classList.remove(...classes);
if (params.colorNames.includes(widgetValue)) {
const originalCSSValue = window.getComputedStyle(this.$target[0])[cssProps[0]];
const className = params.colorPrefix + widgetValue;
this.$target[0].classList.add(className);
if (originalCSSValue !== window.getComputedStyle(this.$target[0])[cssProps[0]]) {
// If applying the class did indeed changed the css
// property we are editing, nothing more has to be done.
// (except adding the extra class)
this.$target.addClass(params.extraClass);
return;
}
// Otherwise, it means that class probably does not exist,
// we remove it and continue. Especially useful for some
// prefixes which only work with some color names but not all.
this.$target[0].classList.remove(className);
}
}
// 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.
const htmlStyle = window.getComputedStyle(document.documentElement);
const htmlPropValue = htmlStyle.getPropertyValue('--' + widgetValue);
if (htmlPropValue) {
widgetValue = `var(--${widgetValue})`;
}
const values = widgetValue.split(/\s+/g);
while (values.length < cssProps.length) {
switch (values.length) {
@@ -1043,15 +1244,6 @@ const SnippetOptionWidget = Widget.extend({
}
}
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);
}
const styles = window.getComputedStyle(this.$target[0]);
let hasUserValue = false;
for (let i = cssProps.length - 1; i > 0; i--) {
@@ -1160,6 +1352,14 @@ const SnippetOptionWidget = Widget.extend({
return dataValue || params.attributeDefaultValue || '';
}
case 'selectStyle': {
if (params.colorPrefix && params.colorNames) {
for (const c of params.colorNames) {
if (this.$target[0].classList.contains(params.colorPrefix + c)) {
return c;
}
}
}
const styles = window.getComputedStyle(this.$target[0]);
const cssProps = weUtils.CSS_SHORTHANDS[params.cssProperty] || [params.cssProperty];
const cssValues = cssProps.map(cssProp => {
@@ -1198,7 +1398,7 @@ const SnippetOptionWidget = Widget.extend({
* @returns {string}
*/
_normalizeWidgetValue: function (value) {
value = `${value}`; // Force to string
value = `${value}`.trim(); // Force to a trimmed string
value = ColorpickerDialog.normalizeCSSColor(value); // If is a css color, normalize it
return value;
},
@@ -1682,137 +1882,6 @@ registry['sizing_y'] = registry.sizing.extend({
},
});
/**
* Handles the edition of snippet's background color classes.
*/
registry.colorpicker = SnippetOptionWidget.extend({
xmlDependencies: ['/web_editor/static/src/xml/snippets.xml'],
custom_events: {
'color_picked': '_onColorPicked',
'color_hover': '_onColorHovered',
'color_leave': '_onColorLeft',
'color_reset': '_onColorReset',
},
/**
* @override
*/
start: async function () {
const _super = this._super.bind(this);
const args = arguments;
// Pre-instanciate the color palette widget
const options = {
selectedColor: this.$target.css('background-color'),
targetClasses: [...this.$target[0].classList],
};
if (this.data.paletteExclude) {
options.excluded = this.data.paletteExclude.replace(/ /g, '').split(',');
}
if (this.data.colorPrefix) {
options.colorPrefix = this.data.colorPrefix;
}
this.colorPalette = new ColorPaletteWidget(this, options);
await this.colorPalette.appendTo(document.createDocumentFragment());
// Build the select element with a custom span to hold the color preview
this.colorPreviewEl = document.createElement('span');
const selectWidget = new SelectUserValueWidget(this, this.data.string, {
classes: ['o_we_so_color_palette'],
childNodes: [this.colorPalette.el],
valueEl: this.colorPreviewEl,
});
await selectWidget.appendTo(document.createDocumentFragment());
// Replace the colorpicker UI with the select element
this.$el.empty().append(selectWidget.el);
return _super(...args);
},
/**
* @override
*/
onFocus: function () {
this.colorPalette.reloadColorPalette();
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Change the color of the targeted background
*
* @private
* @param {string} cssColor
* @param {boolean} isClass
*/
_changeTargetColor: function (cssColor, isClass) {
this.$target[0].classList.remove(...this.colorPalette.getClasses());
if (isClass) {
this.$target.addClass(cssColor);
} else {
this.$target.css('background-color', cssColor);
}
},
/**
* @override
*/
_updateUI: async function () {
await this._super.apply(this, arguments);
this.colorPreviewEl.style.backgroundColor = this.$target.css('background-color');
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Called when a color button is clicked -> confirm the preview.
*
* @private
* @param {Event} ev
*/
_onColorPicked: function (ev) {
this._changeTargetColor(ev.data.cssColor);
this._updateUI().then(() => {
this.$target.closest('.o_editable').trigger('content_changed');
this.$target.trigger('background-color-event', false);
});
},
/**
* Called when a color button is entered -> preview the background color.
*
* @private
* @param {Event} ev
*/
_onColorHovered: function (ev) {
this._changeTargetColor(ev.data.cssColor, ev.data.isClass);
this.$target.trigger('background-color-event', true);
},
/**
* Called when a color button is left -> cancel the preview.
*
* @private
* @param {Event} ev
*/
_onColorLeft: function (ev) {
this._changeTargetColor(ev.data.cssColor, ev.data.isClass);
this.$target.trigger('background-color-event', 'reset');
},
/**
* Called when the color reset button is clicked -> remove all background
* color classes.
*
* @private
*/
_onColorReset: function () {
this._changeTargetColor('');
this._updateUI().then(() => {
this.$target.trigger('content_changed');
});
},
});
/**
* Handles the edition of snippet's background image.
*/
@@ -27,8 +27,6 @@ const ColorPaletteWidget = Widget.extend({
*
* @param {Object} [options]
* @param {string} [options.selectedColor] The class or css attribute color selected by default.
* @param {string[]} [options.targetClasses]
* @param {string} [options.colorPrefix='bg-'] Used for the class based colors (theme).
* @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.
@@ -39,8 +37,6 @@ const ColorPaletteWidget = Widget.extend({
this.summernoteCustomColorsArray = [].concat(...summernoteCustomColors);
this.options = _.extend({
selectedColor: false,
targetClasses: [],
colorPrefix: 'bg-',
resetButton: true,
excluded: [],
excludeSectionOf: null,
@@ -124,31 +120,26 @@ const ColorPaletteWidget = Widget.extend({
this.$el.append($('<button/>', {'class': 'd-none', 'data-color': 'secondary'}));
// Compute class colors
this.classes = [];
this.colorNames = [];
const style = window.getComputedStyle(document.documentElement);
this.el.querySelectorAll('button[data-color]').forEach(elem => {
const color = elem.dataset.color;
const colorName = elem.dataset.color;
const color = ColorpickerDialog.normalizeCSSColor(style.getPropertyValue('--' + colorName).trim());
const $color = $(elem);
$color.addClass('bg-' + color);
const className = this.options.colorPrefix + color;
if (this.options.targetClasses.includes(className)) {
$color.addClass('bg-' + colorName);
if (this.options.selectedColor && (this.options.selectedColor === colorName || this.options.selectedColor === color)) {
$color.addClass('selected');
}
this.classes.push(className);
this.colorNames.push(colorName);
});
return res;
},
/**
* Return a list of the color classes used in the color palette
* Return a list of the color names used in the color palette
*/
getClasses: function () {
return this.classes;
},
/**
* Reloads the color palette to get other custom colors on the page
*/
reloadColorPalette: function () {
this._buildCustomColors();
getColorNames: function () {
return this.colorNames;
},
//--------------------------------------------------------------------------
@@ -215,10 +206,9 @@ const ColorPaletteWidget = Widget.extend({
*/
_getButtonInfo: function (buttonEl) {
const bgColor = buttonEl.style.backgroundColor;
const cssColor = bgColor ? ColorpickerDialog.normalizeCSSColor(bgColor) : (this.options.colorPrefix + buttonEl.dataset.color);
const color = bgColor ? ColorpickerDialog.normalizeCSSColor(bgColor) : buttonEl.dataset.color;
return {
cssColor: cssColor,
isClass: !!buttonEl.dataset.color,
color: color,
target: buttonEl,
};
},
@@ -263,8 +253,7 @@ const ColorPaletteWidget = Widget.extend({
params = this._getButtonInfo(selected);
} else {
params = {
cssColor: '',
isClass: false,
color: '',
};
}
params.target = ev.target;
@@ -297,8 +286,7 @@ const ColorPaletteWidget = Widget.extend({
colorpicker.on('colorpicker:saved', this, ev => {
this._addCustomColorButton(ev.data.cssColor, ['selected']);
this.trigger_up('color_picked', {
cssColor: ev.data.cssColor,
isClass: false,
color: ev.data.cssColor,
target: target,
});
});
@@ -2,6 +2,12 @@
/// This file regroups basic style rules for web_editor enable page edition and backend utils.
///
:root {
@each $color, $value in $grays {
--#{$color}: #{$value};
}
}
html, body {
position: relative;
width: 100%;
@@ -297,26 +297,6 @@ body.editor_enable.editor_has_snippets {
@include we-icon-button('\f107');
}
we-row {
display: block;
> div {
display: flex;
align-items: center;
> * {
flex: 1 1 auto;
+ * {
margin-left: 0.5em;
}
}
> .fa, > .fas, > .fab, > .far {
flex: 0 0 auto;
}
}
}
we-select {
position: relative;
display: flex;
@@ -374,13 +354,8 @@ body.editor_enable.editor_has_snippets {
}
we-input {
display: flex;
align-items: center;
@extend we-row.o_we_inline;
> we-title {
min-width: 60%;
margin-right: 5%;
}
> input {
flex: 1 1 auto;
width: 0;
@@ -398,11 +373,47 @@ body.editor_enable.editor_has_snippets {
}
}
we-row we-input {
flex-wrap: wrap;
we-row {
position: relative;
display: block;
> we-title {
min-width: 95%;
> div {
flex: 1 1 auto;
display: flex;
align-items: center;
> * {
flex: 1 1 auto;
+ * {
margin-left: 0.5em;
}
}
> .fa, > .fas, > .fab, > .far {
flex: 0 0 auto;
}
}
&.o_we_inline {
display: flex;
align-items: center;
> we-title {
min-width: 60%;
margin-right: 5%;
}
}
we-select {
position: static;
}
we-input {
flex-wrap: wrap;
> we-title {
min-width: 95%;
}
}
}
+6 -2
View File
@@ -78,7 +78,9 @@ MockServer.include({
' <div data-js="content" data-selector=".s_hr, .test_option_all" ' +
'data-drop-in=".note-editable" data-drop-near="p, h1, h2, h3, blockquote, .s_hr"></div>' +
' <div data-js="sizing_y" data-selector=".s_hr, .test_option_all"></div>' +
' <div data-js="colorpicker" string="Background Color" data-selector=".test_option_all"></div>' +
' <div data-selector=".test_option_all">' +
' <we-colorpicker string="Background Color" data-select-style="true" data-css-property="background-color" data-color-prefix="bg-"/>' +
' </div>' +
' <div data-js="background" data-selector=".test_option_all">' +
' <we-button data-choose-image="true" data-no-preview="true"><i class="far fa-image"/> Background Image</we-button>' +
' </div>' +
@@ -245,7 +247,9 @@ function wysiwygData(data) {
' <div data-js="content" data-selector=".s_hr, .test_option_all" ' +
'data-drop-in=".note-editable" data-drop-near="p, h1, h2, h3, blockquote, .s_hr"></div>' +
' <div data-js="sizing_y" data-selector=".s_hr, .test_option_all"></div>' +
' <div data-js="colorpicker" string="Background Color" data-selector=".test_option_all"></div>' +
' <div data-selector=".test_option_all">' +
' <we-colorpicker string="Background Color" data-select-style="true" data-css-property="background-color" data-color-prefix="bg-"/>' +
' </div>' +
' <div data-js="background" data-selector=".test_option_all">' +
' <we-button data-choose-image="true" data-no-preview="true"><i class="far fa-image"/> Background Image</we-button>' +
' </div>' +
@@ -1541,7 +1541,7 @@ options.registry.topMenuTransparency = options.Class.extend({
},
});
options.registry.topMenuColor = options.registry.colorpicker.extend({
options.registry.topMenuColor = options.Class.extend({
/**
* @override
*/
@@ -8,6 +8,7 @@ var Widget = require('web.Widget');
var weWidgets = require('wysiwyg.widgets');
var websiteNavbarData = require('website.navbar');
var ColorPaletteDialog = require('web_editor.ColorPalette').ColorPaletteDialog;
const ColorpickerDialog = require('web.ColorpickerDialog');
var _t = core._t;
@@ -518,13 +519,14 @@ var ThemeCustomizeDialog = Dialog.extend({
var colorpicker = new ColorPaletteDialog(self, {
resetButton: false,
defaultColor: $color.css('background-color'),
excluded: ['transparent_grayscale'],
excludeSectionOf: colorName,
});
var chosenColor = undefined;
colorpicker.on('color_picked', self, function (ev) {
ev.stopPropagation();
chosenColor = ev.data.cssColor;
chosenColor = ev.data.color;
if (!ColorpickerDialog.isCSSColor(chosenColor)) {
chosenColor = ColorpickerDialog.normalizeCSSColor(self.style.getPropertyValue('--' + chosenColor).trim());
}
colorpicker.close();
});
colorpicker.on('closed', self, function (ev) {
+61 -30
View File
@@ -982,25 +982,39 @@
<t t-call="web_editor.snippet_options"/>
<!-- COLOR | .s_three_columns | .s_comparisons -->
<div data-js="colorpicker" string="Color"
data-selector=".s_three_columns .row > div, .s_comparisons .row > div, .s_tabs .row > div"
data-target=".card"/>
<div data-selector=".s_three_columns .row > div, .s_comparisons .row > div, .s_tabs .row > div"
data-target=".card">
<we-colorpicker string="Color"
data-select-style="true"
data-css-property="background-color"
data-color-prefix="bg-"/>
</div>
<!-- COLOR | .s_hr -->
<div data-js="colorpicker" string="Color"
data-selector=".s_hr"
data-target="hr"
data-color-prefix="border-"
data-palette-exclude="transparent_grayscale, common, custom"/>
<div data-selector=".s_hr"
data-target="hr">
<we-colorpicker string="Color"
data-select-style="true"
data-css-property="border-color"
data-color-prefix="border-"
data-excluded="transparent_grayscale"/>
</div>
<!-- COLOR | .s_cards -->
<div data-js="colorpicker" string="Color"
data-selector=".s_card, .accordion .card"/>
<div data-selector=".s_card, .accordion .card">
<we-colorpicker string="Color"
data-select-style="true"
data-css-property="background-color"
data-color-prefix="bg-"/>
</div>
<!-- COLOR | .s_alert -->
<div data-js="colorpicker" string="Color"
data-selector=".s_alert"
data-color-prefix="alert-"/>
<div data-selector=".s_alert">
<we-colorpicker string="Color"
data-select-style="true"
data-css-property="background-color"
data-color-prefix="alert-"/>
</div>
<!-- STYLES | .s_title -->
<div data-selector=".s_title" data-target="[class*='s_title_']">
@@ -1259,25 +1273,39 @@
</div>
<!-- Color | Section -->
<div id="so_main_colorpicker" string="Background Color"
data-js="colorpicker"
data-selector="section, .carousel-item"
data-exclude=".parallax, .o_gallery .carousel-item"/>
<div id="so_main_colorpicker"
data-selector="section, .carousel-item"
data-exclude=".parallax, .o_gallery .carousel-item">
<we-colorpicker string="Background Color"
data-select-style="true"
data-css-property="background-color"
data-color-prefix="bg-"/>
</div>
<!-- FILTER | .s_parallax -->
<div data-js="colorpicker" string="Filter"
data-selector=".parallax"
data-palette-exclude="common, theme, custom"
data-palette-default="transparent_grayscale"/>
<div data-selector=".parallax">
<we-colorpicker string="Filter"
data-select-style="true"
data-css-property="background-color"
data-color-prefix="bg-"
data-excluded="common, theme, custom"/>
</div>
<!-- Color | Columns -->
<div data-js="colorpicker" string="Background Color"
data-selector="section .row > div"
data-exclude=".s_col_no_bgcolor, .s_col_no_bgcolor.row > div, .o_gallery .row > div"/>
<div data-selector="section .row > div"
data-exclude=".s_col_no_bgcolor, .s_col_no_bgcolor.row > div, .o_gallery .row > div">
<we-colorpicker string="Background Color"
data-select-style="true"
data-css-property="background-color"
data-color-prefix="bg-"/>
</div>
<div data-selector="section .row > div"
data-exclude=".s_col_no_bgcolor, .s_col_no_bgcolor.row > div, .o_gallery .row > div">
<we-input string="Border" data-select-style="0" data-css-property="border-width" data-unit="px" data-extra-class="border"/>
<we-row string="Border" class="o_we_inline">
<we-colorpicker data-select-style="true" data-css-property="border-color" data-color-prefix="border-"/>
<we-input data-select-style="0" data-css-property="border-width" data-unit="px" data-extra-class="border"/>
</we-row>
<we-input string="Rounded" data-select-style="0" data-css-property="border-radius" data-unit="px" data-extra-class="rounded"/>
<we-checkbox string="Shadow" data-select-class="shadow"/>
</div>
@@ -1321,11 +1349,14 @@
<we-checkbox string="Transparent" data-transparent="true" data-no-preview="true"/>
</div>
<div data-js="topMenuColor" string="Background Color"
data-selector="[data-main-object^='website.page('] #wrapwrap > header"
data-palette-exclude="theme,common, custom"
data-palette-default="transparent_grayscale"
data-no-check="true">
<div data-js="topMenuColor"
data-selector="[data-main-object^='website.page('] #wrapwrap > header"
data-no-check="true">
<we-colorpicker string="Background Color"
data-select-style="true"
data-css-property="background-color"
data-color-prefix="bg-"
data-excluded="theme, common, custom"/>
</div>
<!-- Anchor Name -->