[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:
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user