diff --git a/addons/web/static/src/legacy/js/widgets/colorpicker.js b/addons/web/static/src/legacy/js/widgets/colorpicker.js
index a2854d85131..5e80566bdbc 100644
--- a/addons/web/static/src/legacy/js/widgets/colorpicker.js
+++ b/addons/web/static/src/legacy/js/widgets/colorpicker.js
@@ -26,6 +26,7 @@ var ColorpickerWidget = Widget.extend({
* @param {Object} [options]
* @param {string} [options.defaultColor='#FF0000']
* @param {string} [options.noTransparency=false]
+ * @param {boolean} [options.stopClickPropagation=false]
*/
init: function (parent, options) {
this._super(...arguments);
@@ -111,7 +112,11 @@ var ColorpickerWidget = Widget.extend({
setSelectedColor: function (color) {
var rgba = ColorpickerWidget.convertCSSColorToRgba(color);
if (rgba) {
+ const oldPreviewActive = this.previewActive;
+ this.previewActive = false;
this._updateRgba(rgba.red, rgba.green, rgba.blue, rgba.opacity);
+ this.previewActive = oldPreviewActive;
+ this._updateUI();
}
},
@@ -299,6 +304,9 @@ var ColorpickerWidget = Widget.extend({
* @param {Event} ev
*/
_onClick: function (ev) {
+ if (this.options.stopClickPropagation) {
+ ev.stopPropagation();
+ }
ev.originalEvent.__isColorpickerClick = true;
$(ev.target).find('> .o_opacity_pointer, > .o_slider_pointer, > .o_picker_pointer').addBack('.o_opacity_pointer, .o_slider_pointer, .o_picker_pointer').focus();
if (ev.target.dataset.colorMethod === 'hex' && !this.selectedHexValue) {
diff --git a/addons/web_editor/static/src/js/editor/snippets.options.js b/addons/web_editor/static/src/js/editor/snippets.options.js
index 8e02efdf2f4..32db8dccec6 100644
--- a/addons/web_editor/static/src/js/editor/snippets.options.js
+++ b/addons/web_editor/static/src/js/editor/snippets.options.js
@@ -1453,6 +1453,11 @@ const ColorpickerUserValueWidget = SelectUserValueWidget.extend({
this.colorPreviewEl.style.backgroundImage = this._value;
}
}
+ // If the palette was already opened (e.g. modifying a gradient), the new DOM state must be
+ // reflected in the palette, but the tab selection must not be impacted.
+ if (this.colorPalette.setSelectedColor) {
+ this.colorPalette.setSelectedColor(this._ccValue, this._value, false);
+ }
},
//--------------------------------------------------------------------------
diff --git a/addons/web_editor/static/src/js/wysiwyg/widgets/color_palette.js b/addons/web_editor/static/src/js/wysiwyg/widgets/color_palette.js
index b6989d42559..dbe54e9132f 100644
--- a/addons/web_editor/static/src/js/wysiwyg/widgets/color_palette.js
+++ b/addons/web_editor/static/src/js/wysiwyg/widgets/color_palette.js
@@ -21,6 +21,16 @@ const ColorPaletteWidget = Widget.extend({
'mouseenter .o_we_color_btn': '_onColorButtonEnter',
'mouseleave .o_we_color_btn': '_onColorButtonLeave',
'click .o_we_colorpicker_switch_pane_btn': '_onSwitchPaneButtonClick',
+ 'click .o_custom_gradient_editor .o_custom_gradient_btn': '_onGradientCustomButtonClick',
+ 'click .o_custom_gradient_editor': '_onPanelClick',
+ 'change .o_custom_gradient_editor input[type="text"]': '_onGradientInputChange',
+ 'keypress .o_custom_gradient_editor input[type="text"]': '_onGradientInputKeyPress',
+ 'click .o_custom_gradient_editor we-button:not(.o_remove_color)': '_onGradientButtonClick',
+ 'mouseenter .o_custom_gradient_editor we-button:not(.o_remove_color)': '_onGradientButtonEnter',
+ 'mouseleave .o_custom_gradient_editor we-button:not(.o_remove_color)': '_onGradientButtonLeave',
+ 'click .o_custom_gradient_scale': '_onGradientPreviewClick',
+ // Note: _onGradientSliderClick on slider is attached at slider creation.
+ 'click .o_custom_gradient_editor .o_remove_color': '_onGradientDeleteClick',
},
custom_events: {
'colorpicker_select': '_onColorPickerSelect',
@@ -77,7 +87,10 @@ const ColorPaletteWidget = Widget.extend({
},
{
id: 'gradients',
- pickers: this.options.withGradients ? ['predefined_gradients'] : [],
+ pickers: this.options.withGradients ? [
+ 'predefined_gradients',
+ 'custom_gradient',
+ ] : [],
}];
this.sections = {};
@@ -163,6 +176,36 @@ const ColorPaletteWidget = Widget.extend({
});
}
+ // Palette for gradient
+ if (this.pickers['custom_gradient']) {
+ this.gradientColorPicker = new ColorpickerWidget(this, {
+ stopClickPropagation: true,
+ });
+ await this.gradientColorPicker.appendTo(this.sections['gradients']);
+ const editor = this.pickers['custom_gradient'];
+ this.gradientEditorParts = {
+ 'customButton': editor.querySelector('.o_custom_gradient_btn'),
+ 'customContent': editor.querySelector('.o_color_picker_inputs'),
+ 'linearButton': editor.querySelector('we-button[data-gradient-type="linear-gradient"]'),
+ 'angleRow': editor.querySelector('.o_angle_row'),
+ 'angle': editor.querySelector('input[data-name="angle"]'),
+ 'radialButton': editor.querySelector('we-button[data-gradient-type="radial-gradient"]'),
+ 'positionRow': editor.querySelector('.o_position_row'),
+ 'positionX': editor.querySelector('input[data-name="positionX"]'),
+ 'positionY': editor.querySelector('input[data-name="positionY"]'),
+ 'sizeRow': editor.querySelector('.o_size_row'),
+ 'scale': editor.querySelector('.o_custom_gradient_scale div'),
+ 'sliders': editor.querySelector('.o_slider_multi'),
+ 'deleteButton': editor.querySelector('.o_remove_color'),
+ };
+ const gradient = weUtils.isColorGradient(this.options.selectedColor) && this.options.selectedColor;
+ this._selectGradient(gradient);
+ const resizeObserver = new window.ResizeObserver(() => {
+ this._adjustActiveSliderDelete();
+ });
+ resizeObserver.observe(this.gradientEditorParts.sliders);
+ }
+
// Switch to the correct tab
const selectedButtonIndex = this.tabs.map(tab => tab.id).indexOf(this.options.selectedTab);
this._selectTabFromButton(this.el.querySelectorAll('button')[selectedButtonIndex]);
@@ -274,12 +317,16 @@ const ColorPaletteWidget = Widget.extend({
};
},
/**
- * Sets the currently selected colors
+ * Sets the currently selected colors.
+ *
+ * Note: the tab selection is done here because of an optimization to avoid creating the whole
+ * palette hundreds of times when opening the THEME tab.
*
* @param {string|number} ccValue
* @param {string} color rgb[a]
+ * @param {boolean} [selectTab=true]
*/
- setSelectedColor: function (ccValue, color) {
+ setSelectedColor: function (ccValue, color, selectTab = true) {
if (color === 'rgba(0, 0, 0, 0)' && this.options.opacity !== 1) {
color = 'rgba(0, 0, 0, ' + this.options.opacity + ')';
}
@@ -287,9 +334,11 @@ const ColorPaletteWidget = Widget.extend({
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]);
+ if (selectTab) {
+ // 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]);
+ }
},
//--------------------------------------------------------------------------
@@ -410,10 +459,254 @@ const ColorPaletteWidget = Widget.extend({
this.trigger_up(eventName, colorInfo);
}
this._buildCustomColors();
- this._markSelectedColor();
if (this.colorPicker) {
this.colorPicker.setSelectedColor(colorInfo.color);
}
+ if (this.gradientColorPicker) {
+ const customGradient = weUtils.isColorGradient(colorInfo.color) ? colorInfo.color : false;
+ this._selectGradient(customGradient);
+ }
+ this._markSelectedColor();
+ },
+ /**
+ * Populates the gradient editor.
+ *
+ * @private
+ * @param {string} gradient CSS string
+ */
+ _selectGradient: function (gradient) {
+ const editor = this.gradientEditorParts;
+ if (this.gradientColorPicker.$el) {
+ this.gradientColorPicker.do_hide();
+ }
+ const colorSplits = [];
+ if (gradient) {
+ gradient = gradient.toLowerCase();
+ // Extract colors and their positions: colors can either be in the #rrggbb format or in the
+ // rgb/rgba(...) format, positions are expected to be expressed as percentages
+ // (lengths are not supported).
+ for (const entry of gradient.matchAll(/(#[0-9a-f]{6}|rgba?\(\s*[0-9]+\s*,\s*[0-9]+\s*,\s*[0-9]+\s*[,\s*[0-9.]*]?\s*\))\s*([[0-9]+%]?)/g)) {
+ colorSplits.push([entry[1], entry[2].replace('%', '')]);
+ }
+ }
+ // Consider unsupported gradients as not gradients.
+ if (!gradient || colorSplits.length < 2) {
+ $(editor.customContent).addClass('d-none');
+ editor.customButton.style['background-image'] = '';
+ editor.customButton.dataset.color = false;
+ return;
+ }
+ $(editor.customContent).removeClass('d-none');
+ editor.customButton.style['background-image'] = gradient;
+ editor.customButton.dataset.color = gradient;
+ // The scale display shows the gradient colors horizontally by canceling the type and angle
+ // which are before the first comma.
+ const scaleGradient = gradient.replace(/[^,]+,/, 'linear-gradient(90deg,');
+ editor.scale.style['background-image'] = scaleGradient;
+
+ const isLinear = gradient.startsWith('linear-gradient(');
+ // Keep track of last selected slider's position.
+ let lastSliderPosition;
+ const activeSlider = editor.sliders.querySelector('input.active');
+ if (activeSlider) {
+ lastSliderPosition = activeSlider.value;
+ }
+ let $lastSlider;
+ // Rebuild sliders for each color milestone of the gradient.
+ editor.sliders.replaceChildren();
+ for (const index in colorSplits) {
+ const colorSplit = colorSplits[index];
+ let color = colorSplit[0];
+ const position = colorSplit[1] || 100 * index / colorSplits.length;
+ const $slider = this._createGradientSlider(position, color);
+ if (position === lastSliderPosition) {
+ $lastSlider = $slider;
+ }
+ }
+
+ editor.deleteButton.classList.add('d-none');
+ // Update form elements related to type.
+ if (isLinear) {
+ editor.linearButton.classList.add('active');
+ editor.radialButton.classList.remove('active');
+
+ let angle = gradient.match(/([0-9]+)deg/);
+ angle = angle ? angle[1] : 0;
+ editor.angle.value = angle;
+ } else {
+ editor.linearButton.classList.remove('active');
+ editor.radialButton.classList.add('active');
+
+ const sizeMatch = gradient.match(/(closest|farthest)-(side|corner)/);
+ const size = sizeMatch ? sizeMatch[0] : 'farthest-corner';
+ const $buttons = $(editor.sizeRow).find('we-button');
+ $buttons.removeClass('active');
+ $(editor.sizeRow).find("we-button[data-gradient-size='" + size + "']").addClass('active');
+
+ const position = gradient.match(/ at ([0-9]+)% ([0-9]+)%/) || ['', '50', '50'];
+ editor.positionX.value = position[1];
+ editor.positionY.value = position[2];
+ }
+ this._updateGradientVisibility(isLinear);
+ this._activateGradientSlider($lastSlider || $(this.pickers['custom_gradient'].querySelector('.o_slider_multi input')));
+ },
+ /**
+ * Adjusts the visibility of the gradient editor elements.
+ *
+ * @private
+ * @param {boolean} isLinear
+ */
+ _updateGradientVisibility: function (isLinear) {
+ const editor = this.gradientEditorParts;
+ if (isLinear) {
+ editor.angleRow.classList.remove('d-none');
+ editor.angleRow.classList.add('d-flex');
+ editor.positionRow.classList.add('d-none');
+ editor.positionRow.classList.remove('d-flex');
+ editor.sizeRow.classList.add('d-none');
+ editor.sizeRow.classList.remove('d-flex');
+ } else {
+ editor.angleRow.classList.add('d-none');
+ editor.angleRow.classList.remove('d-flex');
+ editor.positionRow.classList.remove('d-none');
+ editor.positionRow.classList.add('d-flex');
+ editor.sizeRow.classList.remove('d-none');
+ editor.sizeRow.classList.add('d-flex');
+ }
+ },
+ /**
+ * Removes the transparency from an rgba color.
+ *
+ * @private
+ * @param {string} color rgba CSS color string
+ * @returns {string} rgb CSS color string
+ */
+ _opacifyColor: function (color) {
+ if (color.startsWith('rgba')) {
+ return color.replace('rgba', 'rgb').replace(/,\s*[0-9.]+\s*\)/, ')');
+ }
+ return color;
+ },
+ /**
+ * Creates and adds a slider for the gradient color definition.
+ *
+ * @private
+ * @param {int} position between 0 and 100
+ * @param {string} color
+ * @returns {jQuery} created slider
+ */
+ _createGradientSlider: function (position, color) {
+ const $slider = $('');
+ $slider.attr('value', position);
+ $slider.attr('data-color', color);
+ $slider.css('color', this._opacifyColor(color));
+ $slider.on('click', this._onGradientSliderClick.bind(this));
+ $slider.appendTo(this.gradientEditorParts.sliders);
+ this._sortGradientSliders();
+ return $slider;
+ },
+ /**
+ * Activates a slider of the gradient color definition.
+ *
+ * @private
+ * @param {jQuery} $slider
+ */
+ _activateGradientSlider: function ($slider) {
+ const $sliders = $(this.gradientEditorParts.sliders).find('input');
+ $sliders.removeClass('active');
+ $slider.addClass('active');
+
+ const color = $slider.data('color');
+ // Note: show before marking the selected color as, unfortunately,
+ // setting the color and updating the UI accordinly relies on the widget
+ // already being rendered in the DOM.
+ this.gradientColorPicker.do_show();
+ this.gradientColorPicker.setSelectedColor(color);
+ this._sortGradientSliders();
+ this._adjustActiveSliderDelete();
+ },
+ /**
+ * Adjusts the position of the slider delete button.
+ *
+ * @private
+ */
+ _adjustActiveSliderDelete: function () {
+ const $sliders = $(this.gradientEditorParts.sliders).find('input');
+ const $activeSlider = $(this.gradientEditorParts.sliders).find('input.active');
+ if ($sliders.length > 2 && $activeSlider.length) {
+ this.gradientEditorParts.deleteButton.classList.remove('d-none');
+ const sliderWidth = $activeSlider.width();
+ const thumbWidth = 12; // TODO find a way to access it in CSS
+ const deleteWidth = $(this.gradientEditorParts.deleteButton).width();
+ const pixelOffset = (sliderWidth - thumbWidth) * $activeSlider[0].value / 100 + (thumbWidth - deleteWidth) / 2;
+ this.gradientEditorParts.deleteButton.style['margin-left'] = `${pixelOffset}px`;
+ this.gradientEditorParts.deleteButton.style['margin-right'] = `-${deleteWidth / 2}px`;
+ } else {
+ this.gradientEditorParts.deleteButton.classList.add('d-none');
+ }
+ },
+ /**
+ * Reorders the sliders of the gradient color definition by their position.
+ *
+ * @private
+ */
+ _sortGradientSliders: function () {
+ const $sliderInputs = $(this.gradientEditorParts.sliders).find('input');
+ for (const slider of $sliderInputs.sort((a, b) => parseInt(a.value, 10) - parseInt(b.value, 10))) {
+ this.gradientEditorParts.sliders.appendChild(slider);
+ }
+ },
+ /**
+ * Computes the customized gradient from the custom gradient editor.
+ *
+ * @private
+ * @returns {string} gradient string corresponding to the currently selected options.
+ */
+ _computeGradient: function () {
+ const editor = this.gradientEditorParts;
+
+ const $picker = $(this.pickers['custom_gradient']);
+
+ const colors = [];
+ for (const slider of $(editor.sliders).find('input')) {
+ const color = ColorpickerWidget.convertCSSColorToRgba($(slider).data('color'));
+ const colorText = color.opacity !== 100 ? `rgba(${color.red}, ${color.green}, ${color.blue}, ${color.opacity / 100})`
+ : `rgb(${color.red}, ${color.green}, ${color.blue})`;
+ const position = slider.value;
+ colors.push(`${colorText} ${position}%`);
+ }
+
+ const type = $picker.find('.o_type_row we-button.active').data('gradientType');
+ const isLinear = type === 'linear-gradient';
+ let typeParam;
+ if (isLinear) {
+ const angle = editor.angle.value || 0;
+ typeParam = `${angle}deg`;
+ } else {
+ const positionX = editor.positionX.value || 50;
+ const positionY = editor.positionY.value || 50;
+ const size = $picker.find('.o_size_row we-button.active').data('gradientSize');
+ typeParam = `circle ${size} at ${positionX}% ${positionY}%`;
+ }
+
+ return `${type}(${typeParam}, ${colors.join(', ')})`;
+ },
+ /**
+ * Computes the customized gradient from the custom gradient editor and displays it.
+ *
+ * @private
+ * @param {boolean} isPreview
+ */
+ _updateGradient: function (isPreview) {
+ const gradient = this._computeGradient();
+ // Avoid updating an unchanged gradient.
+ if (weUtils.areCssValuesEqual(gradient, this.selectedColor) && !isPreview) {
+ return;
+ }
+ this.trigger_up(isPreview ? 'color_hover' : 'color_picked', Object.assign(this.getSelectedColors(), {
+ color: gradient,
+ target: this.colorPicker.el,
+ }));
},
/**
* Marks the selected colors.
@@ -445,6 +738,26 @@ const ColorPaletteWidget = Widget.extend({
el.classList.toggle('d-none', el.dataset.colorTab !== buttonEl.dataset.target);
});
},
+ /**
+ * Updates a gradient color from a selection in the color picker.
+ *
+ * @private
+ * @param {Event} ev from gradient's colorpicker
+ * @param {boolean} isPreview
+ */
+ _updateGradientColor(ev, isPreview) {
+ ev.stopPropagation();
+ const $slider = $(this.gradientEditorParts.sliders).find('input.active');
+ const color = ev.data.cssColor;
+ if (!weUtils.areCssValuesEqual(color, $slider.data('color'))) {
+ const previousColor = $slider.data('color');
+ $slider.data('color', color);
+ this._updateGradient(isPreview);
+ if (isPreview) {
+ $slider.data('color', previousColor);
+ }
+ }
+ },
//--------------------------------------------------------------------------
// Handlers
@@ -490,10 +803,14 @@ const ColorPaletteWidget = Widget.extend({
* @param {Event} ev
*/
_onColorPickerPreview: function (ev) {
- this.trigger_up('color_hover', Object.assign(this.getSelectedColors(), {
- color: ev.data.cssColor,
- target: this.colorPicker.el,
- }));
+ if (ev.target === this.gradientColorPicker) {
+ this._updateGradientColor(ev, true);
+ } else {
+ 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).
@@ -502,10 +819,14 @@ const ColorPaletteWidget = Widget.extend({
* @param {Event} ev
*/
_onColorPickerSelect: function (ev) {
- this._selectColor(Object.assign(this.getSelectedColors(), {
- color: ev.data.cssColor,
- target: this.colorPicker.el,
- }), 'custom_color_picked');
+ if (ev.target === this.gradientColorPicker) {
+ this._updateGradientColor(ev);
+ } else {
+ this._selectColor(Object.assign(this.getSelectedColors(), {
+ color: ev.data.cssColor,
+ target: this.colorPicker.el,
+ }), 'custom_color_picked');
+ }
},
/**
* @private
@@ -515,6 +836,149 @@ const ColorPaletteWidget = Widget.extend({
ev.stopPropagation();
this._selectTabFromButton(ev.currentTarget);
},
+ /**
+ * @private
+ * @param {Event} ev
+ */
+ _onGradientSliderClick(ev) {
+ ev.stopPropagation();
+ this._activateGradientSlider($(ev.target));
+ this._updateGradient();
+ },
+ /**
+ * Adds a color inside the gradient based on the position clicked within the preview.
+ *
+ * @private
+ * @param {Event} ev
+ */
+ _onGradientPreviewClick(ev) {
+ ev.stopPropagation();
+ const offset = ev.offsetX;
+ const width = parseInt(window.getComputedStyle(ev.target).width, 10);
+ const position = 100 * offset / width;
+
+ let previousColor;
+ let nextColor;
+ let previousPosition;
+ let nextPosition;
+ for (const slider of $(this.gradientEditorParts.sliders).find('input')) {
+ if (slider.value < position) {
+ previousColor = slider.dataset.color;
+ previousPosition = slider.value;
+ } else {
+ nextColor = slider.dataset.color;
+ nextPosition = slider.value;
+ break;
+ }
+ }
+ let color;
+ if (previousColor && nextColor) {
+ previousColor = ColorpickerWidget.convertCSSColorToRgba(previousColor);
+ nextColor = ColorpickerWidget.convertCSSColorToRgba(nextColor);
+ const previousRatio = (nextPosition - position) / (nextPosition - previousPosition);
+ const nextRatio = 1 - previousRatio;
+ const red = Math.round(previousRatio * previousColor.red + nextRatio * nextColor.red);
+ const green = Math.round(previousRatio * previousColor.green + nextRatio * nextColor.green);
+ const blue = Math.round(previousRatio * previousColor.blue + nextRatio * nextColor.blue);
+ const opacity = Math.round(previousRatio * previousColor.opacity + nextRatio * nextColor.opacity);
+ color = `rgba(${red}, ${green}, ${blue}, ${opacity / 100})`;
+ } else {
+ color = nextColor || previousColor || 'rgba(128, 128, 128, 0.5)';
+ }
+
+ const $slider = this._createGradientSlider(position, color);
+ this._activateGradientSlider($slider);
+ this._updateGradient();
+ },
+ /**
+ * @private
+ * @param {Event} ev
+ */
+ _onPanelClick(ev) {
+ // Ignore to avoid closing popup.
+ ev.stopPropagation();
+ },
+ /**
+ * @private
+ * @param {Event} ev
+ */
+ _onGradientInputChange(ev) {
+ this._updateGradient();
+ },
+ /**
+ * @private
+ * @param {Event} ev
+ */
+ _onGradientInputKeyPress: function (ev) {
+ if (ev.charCode === $.ui.keyCode.ENTER) {
+ ev.preventDefault();
+ this._onGradientInputChange();
+ }
+ },
+ /**
+ * @private
+ * @param {Event} ev
+ */
+ _onGradientButtonClick(ev) {
+ const $buttons = $(ev.target).closest('span').find('we-button');
+ $buttons.removeClass('active');
+ $(ev.target).closest('we-button').addClass('active');
+ this._updateGradient();
+ },
+ /**
+ * @private
+ * @param {Event} ev
+ */
+ _onGradientButtonEnter(ev) {
+ ev.stopPropagation();
+ const $activeButton = $(ev.target).closest('span').find('we-button.active');
+ const $buttons = $(ev.target).closest('span').find('we-button');
+ $buttons.removeClass('active');
+ $(ev.target).closest('we-button').addClass('active');
+ this._updateGradient(true);
+ $buttons.removeClass('active');
+ $activeButton.addClass('active');
+ },
+ /**
+ * @private
+ * @param {Event} ev
+ */
+ _onGradientButtonLeave(ev) {
+ ev.stopPropagation();
+ this.trigger_up('color_leave', Object.assign(this.getSelectedColors(), {
+ target: ev.target,
+ }));
+ },
+ /**
+ * @private
+ * @param {Event} ev
+ */
+ _onGradientCustomButtonClick(ev) {
+ let gradient = this.gradientEditorParts.customButton.style['backgroundImage'];
+ if (!gradient) {
+ // default to first predefined
+ gradient = this.pickers['predefined_gradients'].querySelector('button').dataset.color;
+ }
+ this._selectColor(Object.assign(this.getSelectedColors(), {
+ color: gradient,
+ target: this.gradientEditorParts.customButton,
+ }), 'custom_color_picked');
+ this._updateGradient();
+ },
+ /**
+ * @private
+ * @param {Event} ev
+ */
+ _onGradientDeleteClick(ev) {
+ ev.stopPropagation();
+ const $activeSlider = $(this.pickers['custom_gradient'].querySelector('.o_slider_multi input.active'));
+ $activeSlider.off();
+ $activeSlider.remove();
+ this.gradientEditorParts.deleteButton.classList.add('d-none');
+ this.gradientEditorParts.deleteButton.classList.remove('active');
+ this._updateGradient();
+ this._activateGradientSlider($(this.pickers['custom_gradient'].querySelector('.o_slider_multi input')));
+ },
});
//------------------------------------------------------------------------------
diff --git a/addons/web_editor/static/src/js/wysiwyg/widgets/link_tools.js b/addons/web_editor/static/src/js/wysiwyg/widgets/link_tools.js
index be14f37e3d8..539899d56e0 100644
--- a/addons/web_editor/static/src/js/wysiwyg/widgets/link_tools.js
+++ b/addons/web_editor/static/src/js/wysiwyg/widgets/link_tools.js
@@ -33,6 +33,8 @@ const LinkTools = Link.extend({
}
const link = node || this.getOrCreateLink(editable);
this._super(parent, options, editable, data, $button, link);
+ // Keep track of each colorpicker.
+ this.colorpickers = {};
},
/**
* @override
@@ -142,7 +144,8 @@ const LinkTools = Link.extend({
* @override
*/
_getLinkCustomFill: function () {
- return this.$('we-selection-items[name="link-custom-color-fill"] .o_we_color_preview').css('background-color') || '';
+ const $colorPreview = this.$('we-selection-items[name="link-custom-color-fill"] .o_we_color_preview');
+ return $colorPreview.css('background-image') || $colorPreview.css('background-color') || '';
},
/**
* @override
@@ -175,10 +178,19 @@ const LinkTools = Link.extend({
if (el.dataset.value === 'custom') {
const textColor = this.$link[0].style['color'];
this.$('.link-custom-color-text .o_we_color_preview').css('background-color', textColor);
+ if (this.colorpickers['color'] && textColor && textColor !== 'false') {
+ // Keep currently selected tab when setting color.
+ this.colorpickers['color'].setSelectedColor(null, textColor, false);
+ }
const backgroundColor = this.$link[0].style['background-color'];
this.$('.link-custom-color-fill .o_we_color_preview').css('background-color', backgroundColor);
const backgroundImage = this.$link[0].style['background-image'];
this.$('.link-custom-color-fill .o_we_color_preview').css('background-image', backgroundImage);
+ const backgroundFill = (backgroundImage !== 'false' && backgroundImage) || backgroundColor;
+ if (this.colorpickers['background-color'] && backgroundFill && backgroundFill !== 'false') {
+ // Keep currently selected tab when setting color.
+ this.colorpickers['background-color'].setSelectedColor(null, backgroundFill, false);
+ }
const borderWidth = this.$link[0].style['border-width'];
const numberAndUnit = getNumericAndUnit(borderWidth);
this.$('.link-custom-color-border input').val(numberAndUnit ? numberAndUnit[0] : "1");
@@ -194,6 +206,10 @@ const LinkTools = Link.extend({
$activeBorderStyleButton.find('div').clone().appendTo($activeBorderStyleToggler);
const borderColor = this.$link[0].style['border-color'];
this.$('.link-custom-color-border .o_we_color_preview').css('background-color', borderColor);
+ if (this.colorpickers['border-color'] && borderColor && borderColor !== 'false') {
+ // Keep currently selected tab when setting color.
+ this.colorpickers['border-color'].setSelectedColor(null, borderColor, false);
+ }
}
}
},
@@ -239,6 +255,7 @@ const LinkTools = Link.extend({
selectedColor: color,
withGradients: cssProperty === 'background-color',
});
+ this.colorpickers[cssProperty] = colorpicker;
colorpicker.appendTo($(ev.target).closest('.dropdown').find('.dropdown-menu'));
colorpicker.on('custom_color_picked color_picked color_hover color_leave', this, (ev) => {
let color = ev.data.color;
@@ -256,6 +273,7 @@ const LinkTools = Link.extend({
$colorPreview.css('background-color', color);
$colorPreview.css('background-image', gradientColor);
this.options.wysiwyg.odooEditor.historyStep();
+ this._updateOptionsUI();
}
});
},
diff --git a/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js b/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js
index 67313267d28..7bd52a505cc 100644
--- a/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js
+++ b/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js
@@ -49,6 +49,8 @@ const Wysiwyg = Widget.extend({
? this.options.autohideToolbar
: !options.snippets;
this.saving_mutex = new concurrency.Mutex();
+ // Keeps track of color palettes per event name.
+ this.colorpickers = {};
this._onDocumentMousedown = this._onDocumentMousedown.bind(this);
this._onBlur = this._onBlur.bind(this);
},
@@ -1057,6 +1059,29 @@ const Wysiwyg = Widget.extend({
}
}, 0);
},
+ /**
+ * @private
+ * @param {jQuery} $
+ * @param {String} eventName 'foreColor' or 'backColor'
+ * @returns {String} color
+ */
+ _getSelectedColor($, eventName) {
+ const selection = this.odooEditor.document.getSelection();
+ const range = selection.rangeCount && selection.getRangeAt(0);
+ const targetNode = range && range.startContainer;
+ 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;
+ }
+ }
+ return backgroundGradient || $(targetElement).css(eventName === "foreColor" ? 'color' : 'backgroundColor');
+ },
_createPalette() {
const $dropdownContent = this.toolbar.$el.find('#colorInputButtonGroup .colorPalette');
// The editor's root widget can be website or web's root widget and cannot be properly retrieved...
@@ -1081,33 +1106,24 @@ const Wysiwyg = Widget.extend({
mutex.exec(() => {
const oldColorpicker = colorpicker;
const hookEl = oldColorpicker ? oldColorpicker.el : elem;
-
+ const selectedColor = this._getSelectedColor($, eventName);
const selection = this.odooEditor.document.getSelection();
const range = selection.rangeCount && selection.getRangeAt(0);
- const targetNode = range && range.startContainer;
- 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;
- }
- }
const hadNonCollapsedSelection = range && !selection.isCollapsed;
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: backgroundGradient || $(targetElement).css(eventName === "foreColor" ? 'color' : 'backgroundColor'),
+ selectedColor: selectedColor,
+ selectedTab: weUtils.isColorGradient(selectedColor) ? 'gradients' : 'theme-colors',
withGradients: true,
});
+ this.colorpickers[eventName] = colorpicker;
colorpicker.on('custom_color_picked color_picked', null, ev => {
if (hadNonCollapsedSelection) {
this.odooEditor.historyResetLatestComputedSelection(true);
}
this._processAndApplyColor(eventName, ev.data.color);
+ this._updateEditorUI();
});
colorpicker.on('color_hover color_leave', null, ev => {
if (hadNonCollapsedSelection) {
@@ -1309,6 +1325,15 @@ const Wysiwyg = Widget.extend({
setTimeout(() => $target.tooltip('dispose'), 800);
}, 400);
}
+ // Update color of already opened colorpickers.
+ for (let eventName in this.colorpickers) {
+ const selectedColor = this._getSelectedColor($, eventName);
+ if (selectedColor) {
+ // If the palette was already opened (e.g. modifying a gradient), the new DOM state
+ // must be reflected in the palette, but the tab selection must not be impacted.
+ this.colorpickers[eventName].setSelectedColor(null, selectedColor, false);
+ }
+ }
},
_updateMediaJustifyButton: function (commandState) {
if (!this.lastMediaClicked) {
diff --git a/addons/web_editor/static/src/scss/wysiwyg_snippets.scss b/addons/web_editor/static/src/scss/wysiwyg_snippets.scss
index f7e90ad3c31..459fad65679 100644
--- a/addons/web_editor/static/src/scss/wysiwyg_snippets.scss
+++ b/addons/web_editor/static/src/scss/wysiwyg_snippets.scss
@@ -1709,6 +1709,8 @@ body.editor_enable.editor_has_snippets {
}
.o_colorpicker_reset {
@extend %we-generic-button;
+ border: 0;
+ background-color: transparent;
}
.o_colorpicker_sections {
@@ -1750,6 +1752,97 @@ body.editor_enable.editor_has_snippets {
bottom: -1px;
}
}
+ .o_custom_gradient_editor {
+ font-size: 12px;
+
+ .o_custom_gradient_btn {
+ color: #ffffff;
+ background-color: $o-we-sidebar-content-field-colorpicker-dropdown-bg;
+ // Needed when nested inside toolbar.
+ float: none;
+ box-sizing: border-box;
+ }
+ span.o_custom_gradient_input {
+ border: 1px solid black;
+ input {
+ outline: none;
+ }
+ &:focus-within {
+ border-color: $o-we-sidebar-content-field-input-border-color;
+ }
+ }
+ .o_custom_gradient_scale {
+ @extend %o-preview-alpha-background;
+ cursor: copy;
+ div {
+ height: 20px;
+ }
+ }
+ .o_slider_multi {
+ display: grid;
+
+ input[type=range] {
+ pointer-events: none;
+ focusable: false;
+ grid-column: 1/span 2;
+ grid-row: 3;
+ background: none;
+ -webkit-appearance: none;
+ cursor: ew-resize;
+ @supports (-moz-appearance:none) {
+ margin-top: 2px;
+ }
+
+ &::-webkit-slider-thumb {
+ pointer-events: auto;
+ border: 1.5px solid rgba(255, 255, 255, 0.8);
+ background: currentColor;
+ -webkit-appearance: none;
+ box-shadow: 0px 0px 0px #000000;
+ height: 20px;
+ width: 12px;
+ border-radius: 5px;
+ }
+
+ &.active::-webkit-slider-thumb {
+ border-color: $o-we-accent;
+ }
+
+ &::-moz-range-thumb {
+ pointer-events: auto;
+ border: 1.5px solid rgba(255, 255, 255, 0.8);
+ background: currentColor;
+ box-shadow: 0px 0px 0px #000000;
+ height: 18px;
+ width: 10px;
+ border-radius: 5px;
+ margin-top: 3px;
+ }
+
+ &.active::-moz-range-thumb {
+ border-color: $o-we-accent;
+ }
+
+ &::-ms-thumb {
+ pointer-events: auto;
+ border: 1.5px solid rgba(255, 255, 255, 0.8);
+ background: currentColor;
+ box-shadow: 0px 0px 0px #000000;
+ height: 20px;
+ width: 12px;
+ border-radius: 5px;
+ }
+
+ &.active::-ms-thumb {
+ border-color: $o-we-accent;
+ }
+ }
+ }
+ .o_remove_color {
+ font-size: 14px !important;
+ text-align: center !important;
+ }
+ }
}
}
diff --git a/addons/web_editor/static/src/xml/snippets.xml b/addons/web_editor/static/src/xml/snippets.xml
index 8049765e898..e4aa11f69d1 100644
--- a/addons/web_editor/static/src/xml/snippets.xml
+++ b/addons/web_editor/static/src/xml/snippets.xml
@@ -58,7 +58,7 @@