[FW][MERGE] web, web_editor, website: make gradients editable

See sub-commits for details.

This merge allows to create and edit gradients instead of just having
the possibility to select pre-defined ones.

task-2599771

closes odoo/odoo#76608

Forward-port-of: odoo/odoo#74045
Signed-off-by: Quentin Smetz (qsm) <qsm@odoo.com>
This commit is contained in:
Odoo's Mergebot
2021-09-16 01:51:16 +02:00
committed by GitHub
9 changed files with 873 additions and 72 deletions
@@ -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) {
@@ -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);
}
},
//--------------------------------------------------------------------------
@@ -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 = $('<input class="w-100" type="range" min="0" max="100"/>');
$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')));
},
});
//------------------------------------------------------------------------------
@@ -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();
}
});
},
@@ -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) {
@@ -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;
}
}
}
}
@@ -58,7 +58,7 @@
</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-5 py-0 o_we_color_btn o_colorpicker_reset" t-att-title="trash_title">
<button type="button" class="my-1 ml-5 py-0 o_we_color_btn o_colorpicker_reset o_we_hover_danger" t-att-title="trash_title">
<i class="fa fa-trash"/>
</button>
</t>
+88
View File
@@ -75,6 +75,94 @@
<button class="w-50" t-attf-style="background-image: #{gradient};" t-att-data-color="gradient"/>
</t>
</div>
<div class="o_colorpicker_section o_custom_gradient_editor" data-name="custom_gradient">
<button class="w-50 o_custom_gradient_btn o_we_color_btn py-1 btn" title="Define a custom gradient">Custom</button>
<div class="o_color_picker_inputs mx-1">
<t t-set="input_classes" t-value="'p-0 border-0 text-center text-monospace bg-transparent text-white'" />
<div class="d-flex justify-content-between my-2 o_type_row">
<we-title>Type</we-title>
<span class="d-flex justify-content-between">
<we-button data-gradient-type="linear-gradient" class="d-flex align-items-baseline active">Linear</we-button>
<we-button data-gradient-type="radial-gradient" class="d-flex align-items-baseline">Radial</we-button>
</span>
</div>
<div class="d-flex justify-content-between my-2 o_angle_row">
<we-title>Angle</we-title>
<span class="o_custom_gradient_input bg-black-50 px-1 d-flex align-items-baseline">
<input data-name="angle" type="text" t-attf-class="{{input_classes}}" style="width: 5ch;" value="90"/>
<span class="flex-grow-0 ml-1 text-white-50">deg</span>
</span>
</div>
<div class="d-flex justify-content-between my-2 o_position_row">
<we-title>Position</we-title>
<span class="d-flex">
<span class="mr-2">X</span>
<span class="o_custom_gradient_input bg-black-50 px-1 d-flex">
<input data-name="positionX" type="text" t-attf-class="{{input_classes}}" style="width: 3ch;" value="25"/>
<span class="flex-grow-0 ml-1 text-white-50">%</span>
</span>
<span class="mr-2 ml-3">Y</span>
<span class="o_custom_gradient_input bg-black-50 px-1 d-flex">
<input data-name="positionY" type="text" t-attf-class="{{input_classes}}" style="width: 3ch;" value="25"/>
<span class="flex-grow-0 ml-1 text-white-50">%</span>
</span>
</span>
</div>
<div class="d-flex justify-content-between my-2 o_size_row">
<we-title>Size</we-title>
<span class="d-flex justify-content-between">
<we-button data-gradient-size="closest-side" class="d-flex align-items-baseline"
title="Extend to the closest side">
<svg width="16" height="20" viewBox="0 0 16 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1.5" y="3.5" width="13" height="13" stroke="#AAAAAD"/>
<path d="M3 4H9V8C9 8.55228 8.55228 9 8 9H4C3.44772 9 3 8.55228 3 8V4Z" fill="#8C8C92"/>
<path d="M6 11C7.65685 11 9 9.65685 9 8C9 6.34315 7.65685 5 6 5C4.34315 5 3 6.34315 3 8C3 9.65685 4.34315 11 6 11Z" fill="#74747B"/>
<path d="M6 10C7.10457 10 8 9.10457 8 8C8 6.89543 7.10457 6 6 6C4.89543 6 4 6.89543 4 8C4 9.10457 4.89543 10 6 10Z" fill="white"/>
<rect x="2" y="3" width="12" height="1" fill="white"/>
</svg>
</we-button>
<we-button data-gradient-size="closest-corner" class="d-flex align-items-baseline"
title="Extend to the closest corner">
<svg width="16" height="20" viewBox="0 0 16 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1.5" y="3.5" width="13" height="13" stroke="#AAAAAD"/>
<path d="M2 4H9V7C9 9.20914 7.20914 11 5 11H2V4Z" fill="#8C8C92"/>
<path d="M6 11C7.65685 11 9 9.65685 9 8C9 6.34315 7.65685 5 6 5C4.34315 5 3 6.34315 3 8C3 9.65685 4.34315 11 6 11Z" fill="#74747B"/>
<path d="M6 10C7.10457 10 8 9.10457 8 8C8 6.89543 7.10457 6 6 6C4.89543 6 4 6.89543 4 8C4 9.10457 4.89543 10 6 10Z" fill="white"/>
<rect x="1" y="3" width="8" height="1" fill="white"/>
<rect x="1" y="11" width="8" height="1" transform="rotate(-90 1 11)" fill="white"/>
</svg>
</we-button>
<we-button data-gradient-size="farthest-side" class="d-flex align-items-baseline active"
title="Extend to the farthest side">
<svg width="16" height="20" viewBox="0 0 16 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1.5" y="3.5" width="13" height="13" stroke="#AAAAAD"/>
<path d="M7 12C10.3137 12 14 10.2091 14 8C14 5.79086 10.3137 4 7 4C3.68629 4 2 5.79086 2 8C2 10.2091 3.68629 12 7 12Z" fill="#8C8C92"/>
<path d="M6 11C7.65685 11 9 9.65685 9 8C9 6.34315 7.65685 5 6 5C4.34315 5 3 6.34315 3 8C3 9.65685 4.34315 11 6 11Z" fill="#74747B"/>
<path d="M6 10C7.10457 10 8 9.10457 8 8C8 6.89543 7.10457 6 6 6C4.89543 6 4 6.89543 4 8C4 9.10457 4.89543 10 6 10Z" fill="white"/>
<rect x="14" y="4" width="1" height="12" fill="white"/>
</svg>
</we-button>
<we-button data-gradient-size="farthest-corner" class="d-flex align-items-baseline"
title="Extend to the farthest corner">
<svg width="16" height="20" viewBox="0 0 16 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1.5" y="3.5" width="13" height="13" stroke="#AAAAAD"/>
<path d="M2 4H14V10C14 13.3137 11.3137 16 8 16H2V4Z" fill="#8C8C92"/>
<path d="M6 11C7.65685 11 9 9.65685 9 8C9 6.34315 7.65685 5 6 5C4.34315 5 3 6.34315 3 8C3 9.65685 4.34315 11 6 11Z" fill="#74747B"/>
<path d="M6 10C7.10457 10 8 9.10457 8 8C8 6.89543 7.10457 6 6 6C4.89543 6 4 6.89543 4 8C4 9.10457 4.89543 10 6 10Z" fill="white"/>
<rect x="15" y="17" width="7" height="0.999999" transform="rotate(-180 15 17)" fill="white"/>
<rect x="15" y="10" width="7" height="1" transform="rotate(90 15 10)" fill="white"/>
</svg>
</we-button>
</span>
</div>
<div class="mx-1 o_custom_gradient_scale">
<div class="w-100"/>
</div>
<div class="w-100 o_slider_multi" role="group"/>
<we-button class="o_remove_color fa fa-fw fa-trash o_we_link o_we_hover_danger"
title="Remove Selected Color" aria-label="Remove Selected Color"/>
</div>
</div>
</colorpicker>
</template>
</odoo>
@@ -15,6 +15,43 @@ const gradients = [
'linear-gradient(135deg, rgb(255, 222, 202) 0%, rgb(202, 115, 69) 100%)',
];
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);
return {
trigger: `.o_we_colorpicker_switch_pane_btn[data-target="${target}"]`,
content: `Switch to ${name}`,
};
}
function addCheck(steps, 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) {
steps.push(switchTo(xType, name));
}
steps.push(step);
}
function checkAndUpdateBackgroundColor({
checkCC, checkNoCC,
checkBg, checkNoBg,
@@ -26,49 +63,12 @@ function checkAndUpdateBackgroundColor({
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');
addCheck(steps, checkCC, checkNoCC, 'cc', true);
addCheck(steps, checkBg, checkNoBg, 'bg');
addCheck(steps, checkGradient, checkNoGradient, 'gradient');
if (changeType) {
switchTo(changeType);
steps.push(switchTo(changeType));
steps.push(wTourUtils.changeOption('ColoredLevelBackground', `.o_we_color_btn[data-color="${change}"]`));
steps.push({
trigger: finalSelector,
@@ -81,6 +81,16 @@ function checkAndUpdateBackgroundColor({
return steps;
}
function updateAndCheckCustomGradient({updateStep, checkGradient}) {
const steps = [updateStep, {
trigger: `#wrapwrap section.${snippets[0].id}.o_cc1`,
content: 'Color combination 1 still selected',
run: () => null,
}];
addCheck(steps, checkGradient, checkGradient !== gradients[0] && gradients[0], 'gradient', true);
return steps;
}
tour.register('snippet_background_edition', {
test: true,
url: '/?enable_editor=1',
@@ -208,6 +218,96 @@ wTourUtils.changeOption('ColoredLevelBackground', '[data-name="bg_image_toggle_o
},
}),
// Customize gradient
wTourUtils.changeBackgroundColor(),
switchTo('gradient'),
// Avoid navigating across tabs to maintain current editor state
// Step colors
...updateAndCheckCustomGradient({
updateStep: {
trigger: '.colorpicker .o_custom_gradient_scale',
content: 'Add step',
run: 'click',
},
checkGradient: 'linear-gradient(135deg, rgb(203, 94, 238) 0%, rgb(203, 94, 238) 0%, rgb(75, 225, 236) 100%)',
}),
...updateAndCheckCustomGradient({
updateStep: {
trigger: '.colorpicker .o_slider_multi input.active',
content: 'Move step',
run: () => {
const slider = $('.colorpicker .o_slider_multi input.active');
slider.val(45);
slider.trigger('click');
},
},
checkGradient: 'linear-gradient(135deg, rgb(203, 94, 238) 0%, rgb(203, 94, 238) 45%, rgb(75, 225, 236) 100%)',
}),
...updateAndCheckCustomGradient({
updateStep: {
trigger: '.colorpicker .o_color_picker_inputs .o_hex_div input',
content: 'Pick step color',
run: 'text #FF0000',
},
checkGradient: 'linear-gradient(135deg, rgb(203, 94, 238) 0%, rgb(255, 0, 0) 45%, rgb(75, 225, 236) 100%)',
}),
...updateAndCheckCustomGradient({
updateStep: {
trigger: '.colorpicker .o_remove_color',
content: 'Delete step',
run: 'click',
},
checkGradient: 'linear-gradient(135deg, rgb(203, 94, 238) 0%, rgb(75, 225, 236) 100%)',
}),
// Linear
...updateAndCheckCustomGradient({
updateStep: {
trigger: '.colorpicker input[data-name="angle"]',
content: 'Change angle',
run: 'text_blur 50',
},
checkGradient: 'linear-gradient(50deg, rgb(203, 94, 238) 0%, rgb(75, 225, 236) 100%)',
}),
// Radial
...updateAndCheckCustomGradient({
updateStep: {
trigger: '.colorpicker we-button[data-gradient-type="radial-gradient"]',
content: 'Switch to Radial',
run: 'click',
},
checkGradient: 'radial-gradient(circle farthest-side at 25% 25%, rgb(203, 94, 238) 0%, rgb(75, 225, 236) 100%)',
}),
...updateAndCheckCustomGradient({
updateStep: {
trigger: '.colorpicker input[data-name="positionX"]',
content: 'Change X position',
run: 'text_blur 33',
},
checkGradient: 'radial-gradient(circle farthest-side at 33% 25%, rgb(203, 94, 238) 0%, rgb(75, 225, 236) 100%)',
}),
...updateAndCheckCustomGradient({
updateStep: {
trigger: '.colorpicker input[data-name="positionY"]',
content: 'Change Y position',
run: 'text_blur 75',
},
checkGradient: 'radial-gradient(circle farthest-side at 33% 75%, rgb(203, 94, 238) 0%, rgb(75, 225, 236) 100%)',
}),
...updateAndCheckCustomGradient({
updateStep: {
trigger: '.colorpicker we-button[data-gradient-size="closest-side"]',
content: 'Change color spread size',
run: 'click',
},
checkGradient: 'radial-gradient(circle closest-side at 33% 75%, rgb(203, 94, 238) 0%, rgb(75, 225, 236) 100%)',
}),
// Revert to predefined gradient
{
trigger: `.o_we_color_btn[data-color="${gradients[0]}"]`,
content: `Revert to predefiend gradient ${gradients[0]}`,
run: 'click',
},
// Replace the gradient by a bg color
...checkAndUpdateBackgroundColor({
checkCC: 1,