From 03fc3a268d6be0b4a6d7829eec4164c21b6ae300 Mon Sep 17 00:00:00 2001 From: Nicolas Bayet Date: Wed, 7 Jun 2023 07:33:12 +0000 Subject: [PATCH 1/7] [REF] mass_mailing: rename wysiwyg.js to mass_mailing_wysiwyg.js In order to match the filename with the only classname of that file, this commit rename `wysiwyg.js` to `MassMailingWysiwyg.js`. task-3175256 Part-of: odoo/odoo#118966 --- .../static/src/js/{wysiwyg.js => mass_mailing_wysiwyg.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename addons/mass_mailing/static/src/js/{wysiwyg.js => mass_mailing_wysiwyg.js} (100%) diff --git a/addons/mass_mailing/static/src/js/wysiwyg.js b/addons/mass_mailing/static/src/js/mass_mailing_wysiwyg.js similarity index 100% rename from addons/mass_mailing/static/src/js/wysiwyg.js rename to addons/mass_mailing/static/src/js/mass_mailing_wysiwyg.js From 82ae32665ca4a2b587ff4528cb1dc156d2c5f11b Mon Sep 17 00:00:00 2001 From: Nicolas Bayet Date: Mon, 10 Jul 2023 09:59:20 +0000 Subject: [PATCH 2/7] [REF] web: move history of colorpicker.js to split the file Part-of: odoo/odoo#118966 --- .../colorpicker}/colorpicker.js | 0 addons/web/static/src/core/utils/colors.js | 756 ++++++++++++++++++ .../legacy/js/widgets/colorpicker_dialog.js | 756 ++++++++++++++++++ 3 files changed, 1512 insertions(+) rename addons/web/static/src/{legacy/js/widgets => core/colorpicker}/colorpicker.js (100%) create mode 100644 addons/web/static/src/core/utils/colors.js create mode 100644 addons/web/static/src/legacy/js/widgets/colorpicker_dialog.js diff --git a/addons/web/static/src/legacy/js/widgets/colorpicker.js b/addons/web/static/src/core/colorpicker/colorpicker.js similarity index 100% rename from addons/web/static/src/legacy/js/widgets/colorpicker.js rename to addons/web/static/src/core/colorpicker/colorpicker.js diff --git a/addons/web/static/src/core/utils/colors.js b/addons/web/static/src/core/utils/colors.js new file mode 100644 index 00000000000..c87eb8a2544 --- /dev/null +++ b/addons/web/static/src/core/utils/colors.js @@ -0,0 +1,756 @@ +/** @odoo-module alias=web.Colorpicker **/ + +import core from "web.core"; +import utils from "web.utils"; +import Dialog from "web.Dialog"; +import Widget from "web.Widget"; +import { uniqueId } from "@web/core/utils/functions"; +import { debounce, throttleForAnimation } from "@web/core/utils/timing"; + +var _t = core._t; + +var ColorpickerWidget = Widget.extend({ + template: 'Colorpicker', + events: { + 'click': '_onClick', + 'keypress': '_onKeypress', + 'mousedown .o_color_pick_area': '_onMouseDownPicker', + 'mousedown .o_color_slider': '_onMouseDownSlider', + 'mousedown .o_opacity_slider': '_onMouseDownOpacitySlider', + 'change .o_color_picker_inputs': '_onChangeInputs', + }, + + /** + * @constructor + * @param {Widget} parent + * @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); + options = options || {}; + + this.pickerFlag = false; + this.sliderFlag = false; + this.opacitySliderFlag = false; + this.colorComponents = {}; + this.uniqueId = uniqueId("colorpicker"); + this.selectedHexValue = ''; + + this.options = Object.assign({}, options); + }, + /** + * @override + */ + start: function () { + this.$colorpickerArea = this.$('.o_color_pick_area'); + this.$colorpickerPointer = this.$('.o_picker_pointer'); + this.$colorSlider = this.$('.o_color_slider'); + this.$colorSliderPointer = this.$('.o_slider_pointer'); + this.$opacitySlider = this.$('.o_opacity_slider'); + this.$opacitySliderPointer = this.$('.o_opacity_pointer'); + + var defaultColor = this.options.defaultColor || '#FF0000'; + var rgba = ColorpickerWidget.convertCSSColorToRgba(defaultColor); + if (rgba) { + this._updateRgba(rgba.red, rgba.green, rgba.blue, rgba.opacity); + } + + // Pre-fill the inputs. This is because on safari, the baseline for empty + // input is not the baseline of where the text would be, but the bottom + // of the input itself. (see https://bugs.webkit.org/show_bug.cgi?id=142968) + // This will cause the first _updateUI to alter the layout of the colorpicker + // which will change its height. Changing the height of an element inside of + // the callback to a ResizeObserver observing it will cause an error + // (ResizeObserver loop completed with undelivered notifications) that cannot + // be caught, which will open the crash manager. Prefilling the inputs sets + // the baseline correctly from the start so the layout doesn't change. + Object.entries(this.colorComponents).forEach(([component, value]) => { + const input = this.el.querySelector(`.o_${component}_input`); + if (input) { + input.value = value; + } + }); + const resizeObserver = new window.ResizeObserver(() => { + this._updateUI(); + }); + resizeObserver.observe(this.el); + + // Need to be bound on all documents to work in all possible cases (we + // have to be able to start dragging/moving from the colorpicker to + // anywhere on the screen, crossing iframes). + this.$documents = $([window.top, ...Array.from(window.top.frames).filter(frame => { + try { + const document = frame.document; + return !!document; + } catch { + // We cannot access the document (cross origin). + return false; + } + })].map(w => w.document)); + this.throttleOnMouseMove = throttleForAnimation((ev) => { + this._onMouseMovePicker(ev); + this._onMouseMoveSlider(ev); + this._onMouseMoveOpacitySlider(ev); + }); + this.$documents.on(`mousemove.${this.uniqueId}`, this.throttleOnMouseMove); + this.$documents.on(`mouseup.${this.uniqueId}`, debounce((ev) => { + if (this.pickerFlag || this.sliderFlag || this.opacitySliderFlag) { + this._colorSelected(); + } + this.pickerFlag = false; + this.sliderFlag = false; + this.opacitySliderFlag = false; + }, 10)); + + this.previewActive = true; + return this._super.apply(this, arguments); + }, + /** + * @override + */ + destroy: function () { + this._super.apply(this, arguments); + if (this.throttleOnMouseMove) { + this.$documents.off(`.${this.uniqueId}`); + this.throttleOnMouseMove.cancel(); + } + }, + /** + * Sets the currently selected color + * + * @param {string} color rgb[a] + */ + 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(); + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Updates input values, color preview, picker and slider pointer positions. + * + * @private + */ + _updateUI: function () { + var self = this; + + // Update inputs + for (const [color, value] of Object.entries(this.colorComponents)) { + self.$(`.o_${color}_input`).val(value); + } + + // Update preview + this.$('.o_color_preview').css('background-color', this.colorComponents.cssColor); + + // Update picker area and picker pointer position + this.$colorpickerArea.css( + "background-color", + `hsl(${this.colorComponents.hue}, 100%, 50%)` + ); + var top = (100 - this.colorComponents.lightness) * this.$colorpickerArea.height() / 100; + var left = this.colorComponents.saturation * this.$colorpickerArea.width() / 100; + this.$colorpickerPointer.css({ + top: (top - 5) + 'px', + left: (left - 5) + 'px', + }); + + // Update color slider position + var height = this.$colorSlider.height(); + var y = this.colorComponents.hue * height / 360; + this.$colorSliderPointer.css('top', Math.round(y - 2)); + + if (! this.options.noTransparency) { + // Update opacity slider position + var heightOpacity = this.$opacitySlider.height(); + var z = heightOpacity * (1 - this.colorComponents.opacity / 100.0); + this.$opacitySliderPointer.css('top', Math.round(z - 2)); + + // Add gradient color on opacity slider + this.$opacitySlider.css('background', 'linear-gradient(' + this.colorComponents.hex + ' 0%, transparent 100%)'); + } + }, + /** + * Updates colors according to given hex value. Opacity is left unchanged. + * + * @private + * @param {string} hex - hexadecimal code + */ + _updateHex: function (hex) { + var rgb = ColorpickerWidget.convertCSSColorToRgba(hex); + if (!rgb) { + return; + } + Object.assign(this.colorComponents, + {hex: hex}, + rgb, + ColorpickerWidget.convertRgbToHsl(rgb.red, rgb.green, rgb.blue) + ); + this._updateCssColor(); + }, + /** + * Updates colors according to given RGB values. + * + * @private + * @param {integer} r + * @param {integer} g + * @param {integer} b + * @param {integer} [a] + */ + _updateRgba: function (r, g, b, a) { + // Remove full transparency in case some lightness is added + const opacity = a || this.colorComponents.opacity; + if (opacity < 0.1 && (r > 0.1 || g > 0.1 || b > 0.1)) { + a = 100; + } + + // We update the hexadecimal code by transforming into a css color and + // ignoring the opacity (we don't display opacity component in hexa as + // not supported on all browsers) + var hex = ColorpickerWidget.convertRgbaToCSSColor(r, g, b); + if (!hex) { + return; + } + Object.assign(this.colorComponents, + {red: r, green: g, blue: b}, + a === undefined ? {} : {opacity: a}, + {hex: hex}, + ColorpickerWidget.convertRgbToHsl(r, g, b) + ); + this._updateCssColor(); + }, + /** + * Updates colors according to given HSL values. + * + * @private + * @param {integer} h + * @param {integer} s + * @param {integer} l + */ + _updateHsl: function (h, s, l) { + // Remove full transparency in case some lightness is added + let a = this.colorComponents.opacity; + if (a < 0.1 && l > 0.1) { + a = 100; + } + + var rgb = ColorpickerWidget.convertHslToRgb(h, s, l); + if (!rgb) { + return; + } + // We receive an hexa as we ignore the opacity + const hex = ColorpickerWidget.convertRgbaToCSSColor(rgb.red, rgb.green, rgb.blue); + Object.assign(this.colorComponents, + {hue: h, saturation: s, lightness: l}, + rgb, + {hex: hex}, + {opacity: a}, + ); + this._updateCssColor(); + }, + /** + * Updates color opacity. + * + * @private + * @param {integer} a + */ + _updateOpacity: function (a) { + if (a < 0 || a > 100) { + return; + } + Object.assign(this.colorComponents, + {opacity: a} + ); + this._updateCssColor(); + }, + /** + * Trigger an event to annonce that the widget value has changed + * + * @private + */ + _colorSelected: function () { + this.trigger_up('colorpicker_select', this.colorComponents); + }, + /** + * Updates css color representation. + * + * @private + */ + _updateCssColor: function () { + const r = this.colorComponents.red; + const g = this.colorComponents.green; + const b = this.colorComponents.blue; + const a = this.colorComponents.opacity; + Object.assign(this.colorComponents, + {cssColor: ColorpickerWidget.convertRgbaToCSSColor(r, g, b, a)} + ); + if (this.previewActive) { + this.trigger_up('colorpicker_preview', this.colorComponents); + } + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {Event} ev + */ + _onKeypress: function (ev) { + if (ev.charCode === $.ui.keyCode.ENTER) { + if (ev.target.tagName === 'INPUT') { + this._onChangeInputs(ev); + } + ev.preventDefault(); + this.trigger_up('enter_key_color_colorpicker'); + } + }, + /** + * @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) { + ev.target.select(); + this.selectedHexValue = ev.target.value; + return; + } + this.selectedHexValue = ''; + }, + /** + * Updates color when the user starts clicking on the picker. + * + * @private + * @param {Event} ev + */ + _onMouseDownPicker: function (ev) { + this.pickerFlag = true; + ev.preventDefault(); + this._onMouseMovePicker(ev); + }, + /** + * Updates saturation and lightness values on mouse drag over picker. + * + * @private + * @param {Event} ev + */ + _onMouseMovePicker: function (ev) { + if (!this.pickerFlag) { + return; + } + + var offset = this.$colorpickerArea.offset(); + var top = ev.pageY - offset.top; + var left = ev.pageX - offset.left; + var saturation = Math.round(100 * left / this.$colorpickerArea.width()); + var lightness = Math.round(100 * (this.$colorpickerArea.height() - top) / this.$colorpickerArea.height()); + saturation = utils.confine(saturation, 0, 100); + lightness = utils.confine(lightness, 0, 100); + + this._updateHsl(this.colorComponents.hue, saturation, lightness); + this._updateUI(); + }, + /** + * Updates color when user starts clicking on slider. + * + * @private + * @param {Event} ev + */ + _onMouseDownSlider: function (ev) { + this.sliderFlag = true; + ev.preventDefault(); + this._onMouseMoveSlider(ev); + }, + /** + * Updates hue value on mouse drag over slider. + * + * @private + * @param {Event} ev + */ + _onMouseMoveSlider: function (ev) { + if (!this.sliderFlag) { + return; + } + + var y = ev.pageY - this.$colorSlider.offset().top; + var hue = Math.round(360 * y / this.$colorSlider.height()); + hue = utils.confine(hue, 0, 360); + + this._updateHsl(hue, this.colorComponents.saturation, this.colorComponents.lightness); + this._updateUI(); + }, + /** + * Updates opacity when user starts clicking on opacity slider. + * + * @private + * @param {Event} ev + */ + _onMouseDownOpacitySlider: function (ev) { + this.opacitySliderFlag = true; + ev.preventDefault(); + this._onMouseMoveOpacitySlider(ev); + }, + /** + * Updates opacity value on mouse drag over opacity slider. + * + * @private + * @param {Event} ev + */ + _onMouseMoveOpacitySlider: function (ev) { + if (!this.opacitySliderFlag || this.options.noTransparency) { + return; + } + + var y = ev.pageY - this.$opacitySlider.offset().top; + var opacity = Math.round(100 * (1 - y / this.$opacitySlider.height())); + opacity = utils.confine(opacity, 0, 100); + + this._updateOpacity(opacity); + this._updateUI(); + }, + /** + * Called when input value is changed -> Updates UI: Set picker and slider + * position and set colors. + * + * @private + * @param {Event} ev + */ + _onChangeInputs: function (ev) { + switch ($(ev.target).data('colorMethod')) { + case 'hex': + this._updateHex(this.$('.o_hex_input').val()); + break; + case 'rgb': + this._updateRgba( + parseInt(this.$('.o_red_input').val()), + parseInt(this.$('.o_green_input').val()), + parseInt(this.$('.o_blue_input').val()) + ); + break; + case 'hsl': + this._updateHsl( + parseInt(this.$('.o_hue_input').val()), + parseInt(this.$('.o_saturation_input').val()), + parseInt(this.$('.o_lightness_input').val()) + ); + break; + case 'opacity': + this._updateOpacity(parseInt(this.$('.o_opacity_input').val())); + break; + } + this._updateUI(); + this._colorSelected(); + }, +}); + +//-------------------------------------------------------------------------- +// Static +//-------------------------------------------------------------------------- + +/** + * Converts RGB color components to HSL components. + * + * @static + * @param {integer} r - [0, 255] + * @param {integer} g - [0, 255] + * @param {integer} b - [0, 255] + * @returns {Object|false} + * - hue [0, 360[ (float) + * - saturation [0, 100] (float) + * - lightness [0, 100] (float) + */ +ColorpickerWidget.convertRgbToHsl = function (r, g, b) { + if (typeof (r) !== 'number' || isNaN(r) || r < 0 || r > 255 + || typeof (g) !== 'number' || isNaN(g) || g < 0 || g > 255 + || typeof (b) !== 'number' || isNaN(b) || b < 0 || b > 255) { + return false; + } + + var red = r / 255; + var green = g / 255; + var blue = b / 255; + var maxColor = Math.max(red, green, blue); + var minColor = Math.min(red, green, blue); + var delta = maxColor - minColor; + var hue = 0; + var saturation = 0; + var lightness = (maxColor + minColor) / 2; + if (delta) { + if (maxColor === red) { + hue = (green - blue) / delta; + } + if (maxColor === green) { + hue = 2 + (blue - red) / delta; + } + if (maxColor === blue) { + hue = 4 + (red - green) / delta; + } + if (maxColor) { + saturation = delta / (1 - Math.abs(2 * lightness - 1)); + } + } + hue = 60 * hue; + return { + hue: hue < 0 ? hue + 360 : hue, + saturation: saturation * 100, + lightness: lightness * 100, + }; +}; +/** + * Converts HSL color components to RGB components. + * + * @static + * @param {number} h - [0, 360[ (float) + * @param {number} s - [0, 100] (float) + * @param {number} l - [0, 100] (float) + * @returns {Object|false} + * - red [0, 255] (integer) + * - green [0, 255] (integer) + * - blue [0, 255] (integer) + */ +ColorpickerWidget.convertHslToRgb = function (h, s, l) { + if (typeof (h) !== 'number' || isNaN(h) || h < 0 || h > 360 + || typeof (s) !== 'number' || isNaN(s) || s < 0 || s > 100 + || typeof (l) !== 'number' || isNaN(l) || l < 0 || l > 100) { + return false; + } + + var huePrime = h / 60; + var saturation = s / 100; + var lightness = l / 100; + var chroma = saturation * (1 - Math.abs(2 * lightness - 1)); + var secondComponent = chroma * (1 - Math.abs(huePrime % 2 - 1)); + var lightnessAdjustment = lightness - chroma / 2; + var precision = 255; + chroma = Math.round((chroma + lightnessAdjustment) * precision); + secondComponent = Math.round((secondComponent + lightnessAdjustment) * precision); + lightnessAdjustment = Math.round((lightnessAdjustment) * precision); + if (huePrime >= 0 && huePrime < 1) { + return { + red: chroma, + green: secondComponent, + blue: lightnessAdjustment, + }; + } + if (huePrime >= 1 && huePrime < 2) { + return { + red: secondComponent, + green: chroma, + blue: lightnessAdjustment, + }; + } + if (huePrime >= 2 && huePrime < 3) { + return { + red: lightnessAdjustment, + green: chroma, + blue: secondComponent, + }; + } + if (huePrime >= 3 && huePrime < 4) { + return { + red: lightnessAdjustment, + green: secondComponent, + blue: chroma, + }; + } + if (huePrime >= 4 && huePrime < 5) { + return { + red: secondComponent, + green: lightnessAdjustment, + blue: chroma, + }; + } + if (huePrime >= 5 && huePrime <= 6) { + return { + red: chroma, + green: lightnessAdjustment, + blue: secondComponent, + }; + } + return false; +}; +/** + * Converts RGBA color components to a normalized CSS color: if the opacity + * is invalid or equal to 100, a hex is returned; otherwise a rgba() css color + * is returned. + * + * Those choice have multiple reason: + * - A hex color is more common to c/c from other utilities on the web and is + * also shorter than rgb() css colors + * - Opacity in hexadecimal notations is not supported on all browsers and is + * also less common to use. + * + * @static + * @param {integer} r - [0, 255] + * @param {integer} g - [0, 255] + * @param {integer} b - [0, 255] + * @param {float} a - [0, 100] + * @returns {string} + */ +ColorpickerWidget.convertRgbaToCSSColor = function (r, g, b, a) { + if (typeof (r) !== 'number' || isNaN(r) || r < 0 || r > 255 + || typeof (g) !== 'number' || isNaN(g) || g < 0 || g > 255 + || typeof (b) !== 'number' || isNaN(b) || b < 0 || b > 255) { + return false; + } + if (typeof (a) !== 'number' || isNaN(a) || a < 0 || Math.abs(a - 100) < Number.EPSILON) { + const rr = r < 16 ? '0' + r.toString(16) : r.toString(16); + const gg = g < 16 ? '0' + g.toString(16) : g.toString(16); + const bb = b < 16 ? '0' + b.toString(16) : b.toString(16); + return (`#${rr}${gg}${bb}`).toUpperCase(); + } + return `rgba(${r}, ${g}, ${b}, ${parseFloat((a / 100.0).toFixed(3))})`; +}; +/** + * Converts a CSS color (rgb(), rgba(), hexadecimal) to RGBA color components. + * + * Note: we don't support using and displaying hexadecimal color with opacity + * but this method allows to receive one and returns the correct opacity value. + * + * @static + * @param {string} cssColor - hexadecimal code or rgb() or rgba() + * @returns {Object|false} + * - red [0, 255] (integer) + * - green [0, 255] (integer) + * - blue [0, 255] (integer) + * - opacity [0, 100.0] (float) + */ +ColorpickerWidget.convertCSSColorToRgba = function (cssColor) { + // Check if cssColor is a rgba() or rgb() color + const rgba = cssColor.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/); + if (rgba) { + if (rgba[4] === undefined) { + rgba[4] = 1; + } + return { + red: parseInt(rgba[1]), + green: parseInt(rgba[2]), + blue: parseInt(rgba[3]), + opacity: Math.round(parseFloat(rgba[4]) * 100), + }; + } + + // Otherwise, check if cssColor is an hexadecimal code color + if (/^#([0-9A-F]{6}|[0-9A-F]{8})$/i.test(cssColor)) { + return { + red: parseInt(cssColor.substr(1, 2), 16), + green: parseInt(cssColor.substr(3, 2), 16), + blue: parseInt(cssColor.substr(5, 2), 16), + opacity: (cssColor.length === 9 ? (parseInt(cssColor.substr(7, 2), 16) / 255) : 1) * 100, + }; + } + + // TODO maybe implement a support for receiving css color like 'red' or + // 'transparent' (which are now considered non-css color by isCSSColor...) + // Note: however, if ever implemented be careful of 'white'/'black' which + // actually are color names for our color system... + + return false; +}; +/** + * Converts a CSS color (rgb(), rgba(), hexadecimal) to a normalized version + * of the same color (@see convertRgbaToCSSColor). + * + * Normalized color can be safely compared using string comparison. + * + * @static + * @param {string} cssColor - hexadecimal code or rgb() or rgba() + * @returns {string} - the normalized css color or the given css color if it + * failed to be normalized + */ +ColorpickerWidget.normalizeCSSColor = function (cssColor) { + const rgba = ColorpickerWidget.convertCSSColorToRgba(cssColor); + if (!rgba) { + return cssColor; + } + return ColorpickerWidget.convertRgbaToCSSColor(rgba.red, rgba.green, rgba.blue, rgba.opacity); +}; +/** + * Checks if a given string is a css color. + * + * @static + * @param {string} cssColor + * @returns {boolean} + */ +ColorpickerWidget.isCSSColor = function (cssColor) { + return ColorpickerWidget.convertCSSColorToRgba(cssColor) !== false; +}; + +/** + * Mixes two colors by applying a weighted average of their red, green and blue + * components. + * + * @static + * @param {string} cssColor1 - hexadecimal code or rgb() or rgba() + * @param {string} cssColor2 - hexadecimal code or rgb() or rgba() + * @param {number} weight - a number between 0 and 1 + * @returns {string} - mixed color in hexadecimal format + */ +ColorpickerWidget.mixCssColors = function (cssColor1, cssColor2, weight) { + const rgba1 = ColorpickerWidget.convertCSSColorToRgba(cssColor1); + const rgba2 = ColorpickerWidget.convertCSSColorToRgba(cssColor2); + const rgb1 = [rgba1.red, rgba1.green, rgba1.blue]; + const rgb2 = [rgba2.red, rgba2.green, rgba2.blue]; + const [r, g, b] = rgb1.map((_, idx) => Math.round(rgb2[idx] + (rgb1[idx] - rgb2[idx]) * weight)); + return ColorpickerWidget.convertRgbaToCSSColor(r, g, b); +}; + +const ColorpickerDialog = Dialog.extend({ + /** + * @override + */ + init: function (parent, options) { + this.options = options || {}; + this._super(parent, Object.assign({ + size: 'small', + title: _t('Pick a color'), + buttons: [ + {text: _t('Choose'), classes: 'btn-primary', close: true, click: this._onFinalPick.bind(this)}, + {text: _t('Discard'), close: true}, + ], + }, this.options)); + }, + /** + * @override + */ + start: function () { + const proms = [this._super(...arguments)]; + this.colorPicker = new ColorpickerWidget(this, Object.assign({ + colorPreview: true, + }, this.options)); + proms.push(this.colorPicker.appendTo(this.$el)); + return Promise.all(proms); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onFinalPick: function () { + this.trigger_up('colorpicker:saved', this.colorPicker.colorComponents); + }, +}); + +export default { + ColorpickerDialog: ColorpickerDialog, + ColorpickerWidget: ColorpickerWidget, +}; diff --git a/addons/web/static/src/legacy/js/widgets/colorpicker_dialog.js b/addons/web/static/src/legacy/js/widgets/colorpicker_dialog.js new file mode 100644 index 00000000000..c87eb8a2544 --- /dev/null +++ b/addons/web/static/src/legacy/js/widgets/colorpicker_dialog.js @@ -0,0 +1,756 @@ +/** @odoo-module alias=web.Colorpicker **/ + +import core from "web.core"; +import utils from "web.utils"; +import Dialog from "web.Dialog"; +import Widget from "web.Widget"; +import { uniqueId } from "@web/core/utils/functions"; +import { debounce, throttleForAnimation } from "@web/core/utils/timing"; + +var _t = core._t; + +var ColorpickerWidget = Widget.extend({ + template: 'Colorpicker', + events: { + 'click': '_onClick', + 'keypress': '_onKeypress', + 'mousedown .o_color_pick_area': '_onMouseDownPicker', + 'mousedown .o_color_slider': '_onMouseDownSlider', + 'mousedown .o_opacity_slider': '_onMouseDownOpacitySlider', + 'change .o_color_picker_inputs': '_onChangeInputs', + }, + + /** + * @constructor + * @param {Widget} parent + * @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); + options = options || {}; + + this.pickerFlag = false; + this.sliderFlag = false; + this.opacitySliderFlag = false; + this.colorComponents = {}; + this.uniqueId = uniqueId("colorpicker"); + this.selectedHexValue = ''; + + this.options = Object.assign({}, options); + }, + /** + * @override + */ + start: function () { + this.$colorpickerArea = this.$('.o_color_pick_area'); + this.$colorpickerPointer = this.$('.o_picker_pointer'); + this.$colorSlider = this.$('.o_color_slider'); + this.$colorSliderPointer = this.$('.o_slider_pointer'); + this.$opacitySlider = this.$('.o_opacity_slider'); + this.$opacitySliderPointer = this.$('.o_opacity_pointer'); + + var defaultColor = this.options.defaultColor || '#FF0000'; + var rgba = ColorpickerWidget.convertCSSColorToRgba(defaultColor); + if (rgba) { + this._updateRgba(rgba.red, rgba.green, rgba.blue, rgba.opacity); + } + + // Pre-fill the inputs. This is because on safari, the baseline for empty + // input is not the baseline of where the text would be, but the bottom + // of the input itself. (see https://bugs.webkit.org/show_bug.cgi?id=142968) + // This will cause the first _updateUI to alter the layout of the colorpicker + // which will change its height. Changing the height of an element inside of + // the callback to a ResizeObserver observing it will cause an error + // (ResizeObserver loop completed with undelivered notifications) that cannot + // be caught, which will open the crash manager. Prefilling the inputs sets + // the baseline correctly from the start so the layout doesn't change. + Object.entries(this.colorComponents).forEach(([component, value]) => { + const input = this.el.querySelector(`.o_${component}_input`); + if (input) { + input.value = value; + } + }); + const resizeObserver = new window.ResizeObserver(() => { + this._updateUI(); + }); + resizeObserver.observe(this.el); + + // Need to be bound on all documents to work in all possible cases (we + // have to be able to start dragging/moving from the colorpicker to + // anywhere on the screen, crossing iframes). + this.$documents = $([window.top, ...Array.from(window.top.frames).filter(frame => { + try { + const document = frame.document; + return !!document; + } catch { + // We cannot access the document (cross origin). + return false; + } + })].map(w => w.document)); + this.throttleOnMouseMove = throttleForAnimation((ev) => { + this._onMouseMovePicker(ev); + this._onMouseMoveSlider(ev); + this._onMouseMoveOpacitySlider(ev); + }); + this.$documents.on(`mousemove.${this.uniqueId}`, this.throttleOnMouseMove); + this.$documents.on(`mouseup.${this.uniqueId}`, debounce((ev) => { + if (this.pickerFlag || this.sliderFlag || this.opacitySliderFlag) { + this._colorSelected(); + } + this.pickerFlag = false; + this.sliderFlag = false; + this.opacitySliderFlag = false; + }, 10)); + + this.previewActive = true; + return this._super.apply(this, arguments); + }, + /** + * @override + */ + destroy: function () { + this._super.apply(this, arguments); + if (this.throttleOnMouseMove) { + this.$documents.off(`.${this.uniqueId}`); + this.throttleOnMouseMove.cancel(); + } + }, + /** + * Sets the currently selected color + * + * @param {string} color rgb[a] + */ + 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(); + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Updates input values, color preview, picker and slider pointer positions. + * + * @private + */ + _updateUI: function () { + var self = this; + + // Update inputs + for (const [color, value] of Object.entries(this.colorComponents)) { + self.$(`.o_${color}_input`).val(value); + } + + // Update preview + this.$('.o_color_preview').css('background-color', this.colorComponents.cssColor); + + // Update picker area and picker pointer position + this.$colorpickerArea.css( + "background-color", + `hsl(${this.colorComponents.hue}, 100%, 50%)` + ); + var top = (100 - this.colorComponents.lightness) * this.$colorpickerArea.height() / 100; + var left = this.colorComponents.saturation * this.$colorpickerArea.width() / 100; + this.$colorpickerPointer.css({ + top: (top - 5) + 'px', + left: (left - 5) + 'px', + }); + + // Update color slider position + var height = this.$colorSlider.height(); + var y = this.colorComponents.hue * height / 360; + this.$colorSliderPointer.css('top', Math.round(y - 2)); + + if (! this.options.noTransparency) { + // Update opacity slider position + var heightOpacity = this.$opacitySlider.height(); + var z = heightOpacity * (1 - this.colorComponents.opacity / 100.0); + this.$opacitySliderPointer.css('top', Math.round(z - 2)); + + // Add gradient color on opacity slider + this.$opacitySlider.css('background', 'linear-gradient(' + this.colorComponents.hex + ' 0%, transparent 100%)'); + } + }, + /** + * Updates colors according to given hex value. Opacity is left unchanged. + * + * @private + * @param {string} hex - hexadecimal code + */ + _updateHex: function (hex) { + var rgb = ColorpickerWidget.convertCSSColorToRgba(hex); + if (!rgb) { + return; + } + Object.assign(this.colorComponents, + {hex: hex}, + rgb, + ColorpickerWidget.convertRgbToHsl(rgb.red, rgb.green, rgb.blue) + ); + this._updateCssColor(); + }, + /** + * Updates colors according to given RGB values. + * + * @private + * @param {integer} r + * @param {integer} g + * @param {integer} b + * @param {integer} [a] + */ + _updateRgba: function (r, g, b, a) { + // Remove full transparency in case some lightness is added + const opacity = a || this.colorComponents.opacity; + if (opacity < 0.1 && (r > 0.1 || g > 0.1 || b > 0.1)) { + a = 100; + } + + // We update the hexadecimal code by transforming into a css color and + // ignoring the opacity (we don't display opacity component in hexa as + // not supported on all browsers) + var hex = ColorpickerWidget.convertRgbaToCSSColor(r, g, b); + if (!hex) { + return; + } + Object.assign(this.colorComponents, + {red: r, green: g, blue: b}, + a === undefined ? {} : {opacity: a}, + {hex: hex}, + ColorpickerWidget.convertRgbToHsl(r, g, b) + ); + this._updateCssColor(); + }, + /** + * Updates colors according to given HSL values. + * + * @private + * @param {integer} h + * @param {integer} s + * @param {integer} l + */ + _updateHsl: function (h, s, l) { + // Remove full transparency in case some lightness is added + let a = this.colorComponents.opacity; + if (a < 0.1 && l > 0.1) { + a = 100; + } + + var rgb = ColorpickerWidget.convertHslToRgb(h, s, l); + if (!rgb) { + return; + } + // We receive an hexa as we ignore the opacity + const hex = ColorpickerWidget.convertRgbaToCSSColor(rgb.red, rgb.green, rgb.blue); + Object.assign(this.colorComponents, + {hue: h, saturation: s, lightness: l}, + rgb, + {hex: hex}, + {opacity: a}, + ); + this._updateCssColor(); + }, + /** + * Updates color opacity. + * + * @private + * @param {integer} a + */ + _updateOpacity: function (a) { + if (a < 0 || a > 100) { + return; + } + Object.assign(this.colorComponents, + {opacity: a} + ); + this._updateCssColor(); + }, + /** + * Trigger an event to annonce that the widget value has changed + * + * @private + */ + _colorSelected: function () { + this.trigger_up('colorpicker_select', this.colorComponents); + }, + /** + * Updates css color representation. + * + * @private + */ + _updateCssColor: function () { + const r = this.colorComponents.red; + const g = this.colorComponents.green; + const b = this.colorComponents.blue; + const a = this.colorComponents.opacity; + Object.assign(this.colorComponents, + {cssColor: ColorpickerWidget.convertRgbaToCSSColor(r, g, b, a)} + ); + if (this.previewActive) { + this.trigger_up('colorpicker_preview', this.colorComponents); + } + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {Event} ev + */ + _onKeypress: function (ev) { + if (ev.charCode === $.ui.keyCode.ENTER) { + if (ev.target.tagName === 'INPUT') { + this._onChangeInputs(ev); + } + ev.preventDefault(); + this.trigger_up('enter_key_color_colorpicker'); + } + }, + /** + * @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) { + ev.target.select(); + this.selectedHexValue = ev.target.value; + return; + } + this.selectedHexValue = ''; + }, + /** + * Updates color when the user starts clicking on the picker. + * + * @private + * @param {Event} ev + */ + _onMouseDownPicker: function (ev) { + this.pickerFlag = true; + ev.preventDefault(); + this._onMouseMovePicker(ev); + }, + /** + * Updates saturation and lightness values on mouse drag over picker. + * + * @private + * @param {Event} ev + */ + _onMouseMovePicker: function (ev) { + if (!this.pickerFlag) { + return; + } + + var offset = this.$colorpickerArea.offset(); + var top = ev.pageY - offset.top; + var left = ev.pageX - offset.left; + var saturation = Math.round(100 * left / this.$colorpickerArea.width()); + var lightness = Math.round(100 * (this.$colorpickerArea.height() - top) / this.$colorpickerArea.height()); + saturation = utils.confine(saturation, 0, 100); + lightness = utils.confine(lightness, 0, 100); + + this._updateHsl(this.colorComponents.hue, saturation, lightness); + this._updateUI(); + }, + /** + * Updates color when user starts clicking on slider. + * + * @private + * @param {Event} ev + */ + _onMouseDownSlider: function (ev) { + this.sliderFlag = true; + ev.preventDefault(); + this._onMouseMoveSlider(ev); + }, + /** + * Updates hue value on mouse drag over slider. + * + * @private + * @param {Event} ev + */ + _onMouseMoveSlider: function (ev) { + if (!this.sliderFlag) { + return; + } + + var y = ev.pageY - this.$colorSlider.offset().top; + var hue = Math.round(360 * y / this.$colorSlider.height()); + hue = utils.confine(hue, 0, 360); + + this._updateHsl(hue, this.colorComponents.saturation, this.colorComponents.lightness); + this._updateUI(); + }, + /** + * Updates opacity when user starts clicking on opacity slider. + * + * @private + * @param {Event} ev + */ + _onMouseDownOpacitySlider: function (ev) { + this.opacitySliderFlag = true; + ev.preventDefault(); + this._onMouseMoveOpacitySlider(ev); + }, + /** + * Updates opacity value on mouse drag over opacity slider. + * + * @private + * @param {Event} ev + */ + _onMouseMoveOpacitySlider: function (ev) { + if (!this.opacitySliderFlag || this.options.noTransparency) { + return; + } + + var y = ev.pageY - this.$opacitySlider.offset().top; + var opacity = Math.round(100 * (1 - y / this.$opacitySlider.height())); + opacity = utils.confine(opacity, 0, 100); + + this._updateOpacity(opacity); + this._updateUI(); + }, + /** + * Called when input value is changed -> Updates UI: Set picker and slider + * position and set colors. + * + * @private + * @param {Event} ev + */ + _onChangeInputs: function (ev) { + switch ($(ev.target).data('colorMethod')) { + case 'hex': + this._updateHex(this.$('.o_hex_input').val()); + break; + case 'rgb': + this._updateRgba( + parseInt(this.$('.o_red_input').val()), + parseInt(this.$('.o_green_input').val()), + parseInt(this.$('.o_blue_input').val()) + ); + break; + case 'hsl': + this._updateHsl( + parseInt(this.$('.o_hue_input').val()), + parseInt(this.$('.o_saturation_input').val()), + parseInt(this.$('.o_lightness_input').val()) + ); + break; + case 'opacity': + this._updateOpacity(parseInt(this.$('.o_opacity_input').val())); + break; + } + this._updateUI(); + this._colorSelected(); + }, +}); + +//-------------------------------------------------------------------------- +// Static +//-------------------------------------------------------------------------- + +/** + * Converts RGB color components to HSL components. + * + * @static + * @param {integer} r - [0, 255] + * @param {integer} g - [0, 255] + * @param {integer} b - [0, 255] + * @returns {Object|false} + * - hue [0, 360[ (float) + * - saturation [0, 100] (float) + * - lightness [0, 100] (float) + */ +ColorpickerWidget.convertRgbToHsl = function (r, g, b) { + if (typeof (r) !== 'number' || isNaN(r) || r < 0 || r > 255 + || typeof (g) !== 'number' || isNaN(g) || g < 0 || g > 255 + || typeof (b) !== 'number' || isNaN(b) || b < 0 || b > 255) { + return false; + } + + var red = r / 255; + var green = g / 255; + var blue = b / 255; + var maxColor = Math.max(red, green, blue); + var minColor = Math.min(red, green, blue); + var delta = maxColor - minColor; + var hue = 0; + var saturation = 0; + var lightness = (maxColor + minColor) / 2; + if (delta) { + if (maxColor === red) { + hue = (green - blue) / delta; + } + if (maxColor === green) { + hue = 2 + (blue - red) / delta; + } + if (maxColor === blue) { + hue = 4 + (red - green) / delta; + } + if (maxColor) { + saturation = delta / (1 - Math.abs(2 * lightness - 1)); + } + } + hue = 60 * hue; + return { + hue: hue < 0 ? hue + 360 : hue, + saturation: saturation * 100, + lightness: lightness * 100, + }; +}; +/** + * Converts HSL color components to RGB components. + * + * @static + * @param {number} h - [0, 360[ (float) + * @param {number} s - [0, 100] (float) + * @param {number} l - [0, 100] (float) + * @returns {Object|false} + * - red [0, 255] (integer) + * - green [0, 255] (integer) + * - blue [0, 255] (integer) + */ +ColorpickerWidget.convertHslToRgb = function (h, s, l) { + if (typeof (h) !== 'number' || isNaN(h) || h < 0 || h > 360 + || typeof (s) !== 'number' || isNaN(s) || s < 0 || s > 100 + || typeof (l) !== 'number' || isNaN(l) || l < 0 || l > 100) { + return false; + } + + var huePrime = h / 60; + var saturation = s / 100; + var lightness = l / 100; + var chroma = saturation * (1 - Math.abs(2 * lightness - 1)); + var secondComponent = chroma * (1 - Math.abs(huePrime % 2 - 1)); + var lightnessAdjustment = lightness - chroma / 2; + var precision = 255; + chroma = Math.round((chroma + lightnessAdjustment) * precision); + secondComponent = Math.round((secondComponent + lightnessAdjustment) * precision); + lightnessAdjustment = Math.round((lightnessAdjustment) * precision); + if (huePrime >= 0 && huePrime < 1) { + return { + red: chroma, + green: secondComponent, + blue: lightnessAdjustment, + }; + } + if (huePrime >= 1 && huePrime < 2) { + return { + red: secondComponent, + green: chroma, + blue: lightnessAdjustment, + }; + } + if (huePrime >= 2 && huePrime < 3) { + return { + red: lightnessAdjustment, + green: chroma, + blue: secondComponent, + }; + } + if (huePrime >= 3 && huePrime < 4) { + return { + red: lightnessAdjustment, + green: secondComponent, + blue: chroma, + }; + } + if (huePrime >= 4 && huePrime < 5) { + return { + red: secondComponent, + green: lightnessAdjustment, + blue: chroma, + }; + } + if (huePrime >= 5 && huePrime <= 6) { + return { + red: chroma, + green: lightnessAdjustment, + blue: secondComponent, + }; + } + return false; +}; +/** + * Converts RGBA color components to a normalized CSS color: if the opacity + * is invalid or equal to 100, a hex is returned; otherwise a rgba() css color + * is returned. + * + * Those choice have multiple reason: + * - A hex color is more common to c/c from other utilities on the web and is + * also shorter than rgb() css colors + * - Opacity in hexadecimal notations is not supported on all browsers and is + * also less common to use. + * + * @static + * @param {integer} r - [0, 255] + * @param {integer} g - [0, 255] + * @param {integer} b - [0, 255] + * @param {float} a - [0, 100] + * @returns {string} + */ +ColorpickerWidget.convertRgbaToCSSColor = function (r, g, b, a) { + if (typeof (r) !== 'number' || isNaN(r) || r < 0 || r > 255 + || typeof (g) !== 'number' || isNaN(g) || g < 0 || g > 255 + || typeof (b) !== 'number' || isNaN(b) || b < 0 || b > 255) { + return false; + } + if (typeof (a) !== 'number' || isNaN(a) || a < 0 || Math.abs(a - 100) < Number.EPSILON) { + const rr = r < 16 ? '0' + r.toString(16) : r.toString(16); + const gg = g < 16 ? '0' + g.toString(16) : g.toString(16); + const bb = b < 16 ? '0' + b.toString(16) : b.toString(16); + return (`#${rr}${gg}${bb}`).toUpperCase(); + } + return `rgba(${r}, ${g}, ${b}, ${parseFloat((a / 100.0).toFixed(3))})`; +}; +/** + * Converts a CSS color (rgb(), rgba(), hexadecimal) to RGBA color components. + * + * Note: we don't support using and displaying hexadecimal color with opacity + * but this method allows to receive one and returns the correct opacity value. + * + * @static + * @param {string} cssColor - hexadecimal code or rgb() or rgba() + * @returns {Object|false} + * - red [0, 255] (integer) + * - green [0, 255] (integer) + * - blue [0, 255] (integer) + * - opacity [0, 100.0] (float) + */ +ColorpickerWidget.convertCSSColorToRgba = function (cssColor) { + // Check if cssColor is a rgba() or rgb() color + const rgba = cssColor.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/); + if (rgba) { + if (rgba[4] === undefined) { + rgba[4] = 1; + } + return { + red: parseInt(rgba[1]), + green: parseInt(rgba[2]), + blue: parseInt(rgba[3]), + opacity: Math.round(parseFloat(rgba[4]) * 100), + }; + } + + // Otherwise, check if cssColor is an hexadecimal code color + if (/^#([0-9A-F]{6}|[0-9A-F]{8})$/i.test(cssColor)) { + return { + red: parseInt(cssColor.substr(1, 2), 16), + green: parseInt(cssColor.substr(3, 2), 16), + blue: parseInt(cssColor.substr(5, 2), 16), + opacity: (cssColor.length === 9 ? (parseInt(cssColor.substr(7, 2), 16) / 255) : 1) * 100, + }; + } + + // TODO maybe implement a support for receiving css color like 'red' or + // 'transparent' (which are now considered non-css color by isCSSColor...) + // Note: however, if ever implemented be careful of 'white'/'black' which + // actually are color names for our color system... + + return false; +}; +/** + * Converts a CSS color (rgb(), rgba(), hexadecimal) to a normalized version + * of the same color (@see convertRgbaToCSSColor). + * + * Normalized color can be safely compared using string comparison. + * + * @static + * @param {string} cssColor - hexadecimal code or rgb() or rgba() + * @returns {string} - the normalized css color or the given css color if it + * failed to be normalized + */ +ColorpickerWidget.normalizeCSSColor = function (cssColor) { + const rgba = ColorpickerWidget.convertCSSColorToRgba(cssColor); + if (!rgba) { + return cssColor; + } + return ColorpickerWidget.convertRgbaToCSSColor(rgba.red, rgba.green, rgba.blue, rgba.opacity); +}; +/** + * Checks if a given string is a css color. + * + * @static + * @param {string} cssColor + * @returns {boolean} + */ +ColorpickerWidget.isCSSColor = function (cssColor) { + return ColorpickerWidget.convertCSSColorToRgba(cssColor) !== false; +}; + +/** + * Mixes two colors by applying a weighted average of their red, green and blue + * components. + * + * @static + * @param {string} cssColor1 - hexadecimal code or rgb() or rgba() + * @param {string} cssColor2 - hexadecimal code or rgb() or rgba() + * @param {number} weight - a number between 0 and 1 + * @returns {string} - mixed color in hexadecimal format + */ +ColorpickerWidget.mixCssColors = function (cssColor1, cssColor2, weight) { + const rgba1 = ColorpickerWidget.convertCSSColorToRgba(cssColor1); + const rgba2 = ColorpickerWidget.convertCSSColorToRgba(cssColor2); + const rgb1 = [rgba1.red, rgba1.green, rgba1.blue]; + const rgb2 = [rgba2.red, rgba2.green, rgba2.blue]; + const [r, g, b] = rgb1.map((_, idx) => Math.round(rgb2[idx] + (rgb1[idx] - rgb2[idx]) * weight)); + return ColorpickerWidget.convertRgbaToCSSColor(r, g, b); +}; + +const ColorpickerDialog = Dialog.extend({ + /** + * @override + */ + init: function (parent, options) { + this.options = options || {}; + this._super(parent, Object.assign({ + size: 'small', + title: _t('Pick a color'), + buttons: [ + {text: _t('Choose'), classes: 'btn-primary', close: true, click: this._onFinalPick.bind(this)}, + {text: _t('Discard'), close: true}, + ], + }, this.options)); + }, + /** + * @override + */ + start: function () { + const proms = [this._super(...arguments)]; + this.colorPicker = new ColorpickerWidget(this, Object.assign({ + colorPreview: true, + }, this.options)); + proms.push(this.colorPicker.appendTo(this.$el)); + return Promise.all(proms); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onFinalPick: function () { + this.trigger_up('colorpicker:saved', this.colorPicker.colorComponents); + }, +}); + +export default { + ColorpickerDialog: ColorpickerDialog, + ColorpickerWidget: ColorpickerWidget, +}; From 230a9429bbc86ffc916e8990a59242ae24c63775 Mon Sep 17 00:00:00 2001 From: Nicolas Bayet Date: Mon, 10 Jul 2023 10:17:02 +0000 Subject: [PATCH 3/7] [REF] web: finish split of colorpicker.js Part-of: odoo/odoo#118966 --- .../src/core/colorpicker/colorpicker.js | 299 +------- addons/web/static/src/core/utils/colors.js | 536 +------------ .../legacy/js/widgets/colorpicker_dialog.js | 709 +----------------- 3 files changed, 16 insertions(+), 1528 deletions(-) diff --git a/addons/web/static/src/core/colorpicker/colorpicker.js b/addons/web/static/src/core/colorpicker/colorpicker.js index c87eb8a2544..501d6acae97 100644 --- a/addons/web/static/src/core/colorpicker/colorpicker.js +++ b/addons/web/static/src/core/colorpicker/colorpicker.js @@ -1,8 +1,7 @@ -/** @odoo-module alias=web.Colorpicker **/ +/** @odoo-module **/ import core from "web.core"; import utils from "web.utils"; -import Dialog from "web.Dialog"; import Widget from "web.Widget"; import { uniqueId } from "@web/core/utils/functions"; import { debounce, throttleForAnimation } from "@web/core/utils/timing"; @@ -458,299 +457,3 @@ var ColorpickerWidget = Widget.extend({ this._colorSelected(); }, }); - -//-------------------------------------------------------------------------- -// Static -//-------------------------------------------------------------------------- - -/** - * Converts RGB color components to HSL components. - * - * @static - * @param {integer} r - [0, 255] - * @param {integer} g - [0, 255] - * @param {integer} b - [0, 255] - * @returns {Object|false} - * - hue [0, 360[ (float) - * - saturation [0, 100] (float) - * - lightness [0, 100] (float) - */ -ColorpickerWidget.convertRgbToHsl = function (r, g, b) { - if (typeof (r) !== 'number' || isNaN(r) || r < 0 || r > 255 - || typeof (g) !== 'number' || isNaN(g) || g < 0 || g > 255 - || typeof (b) !== 'number' || isNaN(b) || b < 0 || b > 255) { - return false; - } - - var red = r / 255; - var green = g / 255; - var blue = b / 255; - var maxColor = Math.max(red, green, blue); - var minColor = Math.min(red, green, blue); - var delta = maxColor - minColor; - var hue = 0; - var saturation = 0; - var lightness = (maxColor + minColor) / 2; - if (delta) { - if (maxColor === red) { - hue = (green - blue) / delta; - } - if (maxColor === green) { - hue = 2 + (blue - red) / delta; - } - if (maxColor === blue) { - hue = 4 + (red - green) / delta; - } - if (maxColor) { - saturation = delta / (1 - Math.abs(2 * lightness - 1)); - } - } - hue = 60 * hue; - return { - hue: hue < 0 ? hue + 360 : hue, - saturation: saturation * 100, - lightness: lightness * 100, - }; -}; -/** - * Converts HSL color components to RGB components. - * - * @static - * @param {number} h - [0, 360[ (float) - * @param {number} s - [0, 100] (float) - * @param {number} l - [0, 100] (float) - * @returns {Object|false} - * - red [0, 255] (integer) - * - green [0, 255] (integer) - * - blue [0, 255] (integer) - */ -ColorpickerWidget.convertHslToRgb = function (h, s, l) { - if (typeof (h) !== 'number' || isNaN(h) || h < 0 || h > 360 - || typeof (s) !== 'number' || isNaN(s) || s < 0 || s > 100 - || typeof (l) !== 'number' || isNaN(l) || l < 0 || l > 100) { - return false; - } - - var huePrime = h / 60; - var saturation = s / 100; - var lightness = l / 100; - var chroma = saturation * (1 - Math.abs(2 * lightness - 1)); - var secondComponent = chroma * (1 - Math.abs(huePrime % 2 - 1)); - var lightnessAdjustment = lightness - chroma / 2; - var precision = 255; - chroma = Math.round((chroma + lightnessAdjustment) * precision); - secondComponent = Math.round((secondComponent + lightnessAdjustment) * precision); - lightnessAdjustment = Math.round((lightnessAdjustment) * precision); - if (huePrime >= 0 && huePrime < 1) { - return { - red: chroma, - green: secondComponent, - blue: lightnessAdjustment, - }; - } - if (huePrime >= 1 && huePrime < 2) { - return { - red: secondComponent, - green: chroma, - blue: lightnessAdjustment, - }; - } - if (huePrime >= 2 && huePrime < 3) { - return { - red: lightnessAdjustment, - green: chroma, - blue: secondComponent, - }; - } - if (huePrime >= 3 && huePrime < 4) { - return { - red: lightnessAdjustment, - green: secondComponent, - blue: chroma, - }; - } - if (huePrime >= 4 && huePrime < 5) { - return { - red: secondComponent, - green: lightnessAdjustment, - blue: chroma, - }; - } - if (huePrime >= 5 && huePrime <= 6) { - return { - red: chroma, - green: lightnessAdjustment, - blue: secondComponent, - }; - } - return false; -}; -/** - * Converts RGBA color components to a normalized CSS color: if the opacity - * is invalid or equal to 100, a hex is returned; otherwise a rgba() css color - * is returned. - * - * Those choice have multiple reason: - * - A hex color is more common to c/c from other utilities on the web and is - * also shorter than rgb() css colors - * - Opacity in hexadecimal notations is not supported on all browsers and is - * also less common to use. - * - * @static - * @param {integer} r - [0, 255] - * @param {integer} g - [0, 255] - * @param {integer} b - [0, 255] - * @param {float} a - [0, 100] - * @returns {string} - */ -ColorpickerWidget.convertRgbaToCSSColor = function (r, g, b, a) { - if (typeof (r) !== 'number' || isNaN(r) || r < 0 || r > 255 - || typeof (g) !== 'number' || isNaN(g) || g < 0 || g > 255 - || typeof (b) !== 'number' || isNaN(b) || b < 0 || b > 255) { - return false; - } - if (typeof (a) !== 'number' || isNaN(a) || a < 0 || Math.abs(a - 100) < Number.EPSILON) { - const rr = r < 16 ? '0' + r.toString(16) : r.toString(16); - const gg = g < 16 ? '0' + g.toString(16) : g.toString(16); - const bb = b < 16 ? '0' + b.toString(16) : b.toString(16); - return (`#${rr}${gg}${bb}`).toUpperCase(); - } - return `rgba(${r}, ${g}, ${b}, ${parseFloat((a / 100.0).toFixed(3))})`; -}; -/** - * Converts a CSS color (rgb(), rgba(), hexadecimal) to RGBA color components. - * - * Note: we don't support using and displaying hexadecimal color with opacity - * but this method allows to receive one and returns the correct opacity value. - * - * @static - * @param {string} cssColor - hexadecimal code or rgb() or rgba() - * @returns {Object|false} - * - red [0, 255] (integer) - * - green [0, 255] (integer) - * - blue [0, 255] (integer) - * - opacity [0, 100.0] (float) - */ -ColorpickerWidget.convertCSSColorToRgba = function (cssColor) { - // Check if cssColor is a rgba() or rgb() color - const rgba = cssColor.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/); - if (rgba) { - if (rgba[4] === undefined) { - rgba[4] = 1; - } - return { - red: parseInt(rgba[1]), - green: parseInt(rgba[2]), - blue: parseInt(rgba[3]), - opacity: Math.round(parseFloat(rgba[4]) * 100), - }; - } - - // Otherwise, check if cssColor is an hexadecimal code color - if (/^#([0-9A-F]{6}|[0-9A-F]{8})$/i.test(cssColor)) { - return { - red: parseInt(cssColor.substr(1, 2), 16), - green: parseInt(cssColor.substr(3, 2), 16), - blue: parseInt(cssColor.substr(5, 2), 16), - opacity: (cssColor.length === 9 ? (parseInt(cssColor.substr(7, 2), 16) / 255) : 1) * 100, - }; - } - - // TODO maybe implement a support for receiving css color like 'red' or - // 'transparent' (which are now considered non-css color by isCSSColor...) - // Note: however, if ever implemented be careful of 'white'/'black' which - // actually are color names for our color system... - - return false; -}; -/** - * Converts a CSS color (rgb(), rgba(), hexadecimal) to a normalized version - * of the same color (@see convertRgbaToCSSColor). - * - * Normalized color can be safely compared using string comparison. - * - * @static - * @param {string} cssColor - hexadecimal code or rgb() or rgba() - * @returns {string} - the normalized css color or the given css color if it - * failed to be normalized - */ -ColorpickerWidget.normalizeCSSColor = function (cssColor) { - const rgba = ColorpickerWidget.convertCSSColorToRgba(cssColor); - if (!rgba) { - return cssColor; - } - return ColorpickerWidget.convertRgbaToCSSColor(rgba.red, rgba.green, rgba.blue, rgba.opacity); -}; -/** - * Checks if a given string is a css color. - * - * @static - * @param {string} cssColor - * @returns {boolean} - */ -ColorpickerWidget.isCSSColor = function (cssColor) { - return ColorpickerWidget.convertCSSColorToRgba(cssColor) !== false; -}; - -/** - * Mixes two colors by applying a weighted average of their red, green and blue - * components. - * - * @static - * @param {string} cssColor1 - hexadecimal code or rgb() or rgba() - * @param {string} cssColor2 - hexadecimal code or rgb() or rgba() - * @param {number} weight - a number between 0 and 1 - * @returns {string} - mixed color in hexadecimal format - */ -ColorpickerWidget.mixCssColors = function (cssColor1, cssColor2, weight) { - const rgba1 = ColorpickerWidget.convertCSSColorToRgba(cssColor1); - const rgba2 = ColorpickerWidget.convertCSSColorToRgba(cssColor2); - const rgb1 = [rgba1.red, rgba1.green, rgba1.blue]; - const rgb2 = [rgba2.red, rgba2.green, rgba2.blue]; - const [r, g, b] = rgb1.map((_, idx) => Math.round(rgb2[idx] + (rgb1[idx] - rgb2[idx]) * weight)); - return ColorpickerWidget.convertRgbaToCSSColor(r, g, b); -}; - -const ColorpickerDialog = Dialog.extend({ - /** - * @override - */ - init: function (parent, options) { - this.options = options || {}; - this._super(parent, Object.assign({ - size: 'small', - title: _t('Pick a color'), - buttons: [ - {text: _t('Choose'), classes: 'btn-primary', close: true, click: this._onFinalPick.bind(this)}, - {text: _t('Discard'), close: true}, - ], - }, this.options)); - }, - /** - * @override - */ - start: function () { - const proms = [this._super(...arguments)]; - this.colorPicker = new ColorpickerWidget(this, Object.assign({ - colorPreview: true, - }, this.options)); - proms.push(this.colorPicker.appendTo(this.$el)); - return Promise.all(proms); - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * @private - */ - _onFinalPick: function () { - this.trigger_up('colorpicker:saved', this.colorPicker.colorComponents); - }, -}); - -export default { - ColorpickerDialog: ColorpickerDialog, - ColorpickerWidget: ColorpickerWidget, -}; diff --git a/addons/web/static/src/core/utils/colors.js b/addons/web/static/src/core/utils/colors.js index c87eb8a2544..a06357dcfe7 100644 --- a/addons/web/static/src/core/utils/colors.js +++ b/addons/web/static/src/core/utils/colors.js @@ -1,467 +1,4 @@ -/** @odoo-module alias=web.Colorpicker **/ - -import core from "web.core"; -import utils from "web.utils"; -import Dialog from "web.Dialog"; -import Widget from "web.Widget"; -import { uniqueId } from "@web/core/utils/functions"; -import { debounce, throttleForAnimation } from "@web/core/utils/timing"; - -var _t = core._t; - -var ColorpickerWidget = Widget.extend({ - template: 'Colorpicker', - events: { - 'click': '_onClick', - 'keypress': '_onKeypress', - 'mousedown .o_color_pick_area': '_onMouseDownPicker', - 'mousedown .o_color_slider': '_onMouseDownSlider', - 'mousedown .o_opacity_slider': '_onMouseDownOpacitySlider', - 'change .o_color_picker_inputs': '_onChangeInputs', - }, - - /** - * @constructor - * @param {Widget} parent - * @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); - options = options || {}; - - this.pickerFlag = false; - this.sliderFlag = false; - this.opacitySliderFlag = false; - this.colorComponents = {}; - this.uniqueId = uniqueId("colorpicker"); - this.selectedHexValue = ''; - - this.options = Object.assign({}, options); - }, - /** - * @override - */ - start: function () { - this.$colorpickerArea = this.$('.o_color_pick_area'); - this.$colorpickerPointer = this.$('.o_picker_pointer'); - this.$colorSlider = this.$('.o_color_slider'); - this.$colorSliderPointer = this.$('.o_slider_pointer'); - this.$opacitySlider = this.$('.o_opacity_slider'); - this.$opacitySliderPointer = this.$('.o_opacity_pointer'); - - var defaultColor = this.options.defaultColor || '#FF0000'; - var rgba = ColorpickerWidget.convertCSSColorToRgba(defaultColor); - if (rgba) { - this._updateRgba(rgba.red, rgba.green, rgba.blue, rgba.opacity); - } - - // Pre-fill the inputs. This is because on safari, the baseline for empty - // input is not the baseline of where the text would be, but the bottom - // of the input itself. (see https://bugs.webkit.org/show_bug.cgi?id=142968) - // This will cause the first _updateUI to alter the layout of the colorpicker - // which will change its height. Changing the height of an element inside of - // the callback to a ResizeObserver observing it will cause an error - // (ResizeObserver loop completed with undelivered notifications) that cannot - // be caught, which will open the crash manager. Prefilling the inputs sets - // the baseline correctly from the start so the layout doesn't change. - Object.entries(this.colorComponents).forEach(([component, value]) => { - const input = this.el.querySelector(`.o_${component}_input`); - if (input) { - input.value = value; - } - }); - const resizeObserver = new window.ResizeObserver(() => { - this._updateUI(); - }); - resizeObserver.observe(this.el); - - // Need to be bound on all documents to work in all possible cases (we - // have to be able to start dragging/moving from the colorpicker to - // anywhere on the screen, crossing iframes). - this.$documents = $([window.top, ...Array.from(window.top.frames).filter(frame => { - try { - const document = frame.document; - return !!document; - } catch { - // We cannot access the document (cross origin). - return false; - } - })].map(w => w.document)); - this.throttleOnMouseMove = throttleForAnimation((ev) => { - this._onMouseMovePicker(ev); - this._onMouseMoveSlider(ev); - this._onMouseMoveOpacitySlider(ev); - }); - this.$documents.on(`mousemove.${this.uniqueId}`, this.throttleOnMouseMove); - this.$documents.on(`mouseup.${this.uniqueId}`, debounce((ev) => { - if (this.pickerFlag || this.sliderFlag || this.opacitySliderFlag) { - this._colorSelected(); - } - this.pickerFlag = false; - this.sliderFlag = false; - this.opacitySliderFlag = false; - }, 10)); - - this.previewActive = true; - return this._super.apply(this, arguments); - }, - /** - * @override - */ - destroy: function () { - this._super.apply(this, arguments); - if (this.throttleOnMouseMove) { - this.$documents.off(`.${this.uniqueId}`); - this.throttleOnMouseMove.cancel(); - } - }, - /** - * Sets the currently selected color - * - * @param {string} color rgb[a] - */ - 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(); - } - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Updates input values, color preview, picker and slider pointer positions. - * - * @private - */ - _updateUI: function () { - var self = this; - - // Update inputs - for (const [color, value] of Object.entries(this.colorComponents)) { - self.$(`.o_${color}_input`).val(value); - } - - // Update preview - this.$('.o_color_preview').css('background-color', this.colorComponents.cssColor); - - // Update picker area and picker pointer position - this.$colorpickerArea.css( - "background-color", - `hsl(${this.colorComponents.hue}, 100%, 50%)` - ); - var top = (100 - this.colorComponents.lightness) * this.$colorpickerArea.height() / 100; - var left = this.colorComponents.saturation * this.$colorpickerArea.width() / 100; - this.$colorpickerPointer.css({ - top: (top - 5) + 'px', - left: (left - 5) + 'px', - }); - - // Update color slider position - var height = this.$colorSlider.height(); - var y = this.colorComponents.hue * height / 360; - this.$colorSliderPointer.css('top', Math.round(y - 2)); - - if (! this.options.noTransparency) { - // Update opacity slider position - var heightOpacity = this.$opacitySlider.height(); - var z = heightOpacity * (1 - this.colorComponents.opacity / 100.0); - this.$opacitySliderPointer.css('top', Math.round(z - 2)); - - // Add gradient color on opacity slider - this.$opacitySlider.css('background', 'linear-gradient(' + this.colorComponents.hex + ' 0%, transparent 100%)'); - } - }, - /** - * Updates colors according to given hex value. Opacity is left unchanged. - * - * @private - * @param {string} hex - hexadecimal code - */ - _updateHex: function (hex) { - var rgb = ColorpickerWidget.convertCSSColorToRgba(hex); - if (!rgb) { - return; - } - Object.assign(this.colorComponents, - {hex: hex}, - rgb, - ColorpickerWidget.convertRgbToHsl(rgb.red, rgb.green, rgb.blue) - ); - this._updateCssColor(); - }, - /** - * Updates colors according to given RGB values. - * - * @private - * @param {integer} r - * @param {integer} g - * @param {integer} b - * @param {integer} [a] - */ - _updateRgba: function (r, g, b, a) { - // Remove full transparency in case some lightness is added - const opacity = a || this.colorComponents.opacity; - if (opacity < 0.1 && (r > 0.1 || g > 0.1 || b > 0.1)) { - a = 100; - } - - // We update the hexadecimal code by transforming into a css color and - // ignoring the opacity (we don't display opacity component in hexa as - // not supported on all browsers) - var hex = ColorpickerWidget.convertRgbaToCSSColor(r, g, b); - if (!hex) { - return; - } - Object.assign(this.colorComponents, - {red: r, green: g, blue: b}, - a === undefined ? {} : {opacity: a}, - {hex: hex}, - ColorpickerWidget.convertRgbToHsl(r, g, b) - ); - this._updateCssColor(); - }, - /** - * Updates colors according to given HSL values. - * - * @private - * @param {integer} h - * @param {integer} s - * @param {integer} l - */ - _updateHsl: function (h, s, l) { - // Remove full transparency in case some lightness is added - let a = this.colorComponents.opacity; - if (a < 0.1 && l > 0.1) { - a = 100; - } - - var rgb = ColorpickerWidget.convertHslToRgb(h, s, l); - if (!rgb) { - return; - } - // We receive an hexa as we ignore the opacity - const hex = ColorpickerWidget.convertRgbaToCSSColor(rgb.red, rgb.green, rgb.blue); - Object.assign(this.colorComponents, - {hue: h, saturation: s, lightness: l}, - rgb, - {hex: hex}, - {opacity: a}, - ); - this._updateCssColor(); - }, - /** - * Updates color opacity. - * - * @private - * @param {integer} a - */ - _updateOpacity: function (a) { - if (a < 0 || a > 100) { - return; - } - Object.assign(this.colorComponents, - {opacity: a} - ); - this._updateCssColor(); - }, - /** - * Trigger an event to annonce that the widget value has changed - * - * @private - */ - _colorSelected: function () { - this.trigger_up('colorpicker_select', this.colorComponents); - }, - /** - * Updates css color representation. - * - * @private - */ - _updateCssColor: function () { - const r = this.colorComponents.red; - const g = this.colorComponents.green; - const b = this.colorComponents.blue; - const a = this.colorComponents.opacity; - Object.assign(this.colorComponents, - {cssColor: ColorpickerWidget.convertRgbaToCSSColor(r, g, b, a)} - ); - if (this.previewActive) { - this.trigger_up('colorpicker_preview', this.colorComponents); - } - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * @private - * @param {Event} ev - */ - _onKeypress: function (ev) { - if (ev.charCode === $.ui.keyCode.ENTER) { - if (ev.target.tagName === 'INPUT') { - this._onChangeInputs(ev); - } - ev.preventDefault(); - this.trigger_up('enter_key_color_colorpicker'); - } - }, - /** - * @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) { - ev.target.select(); - this.selectedHexValue = ev.target.value; - return; - } - this.selectedHexValue = ''; - }, - /** - * Updates color when the user starts clicking on the picker. - * - * @private - * @param {Event} ev - */ - _onMouseDownPicker: function (ev) { - this.pickerFlag = true; - ev.preventDefault(); - this._onMouseMovePicker(ev); - }, - /** - * Updates saturation and lightness values on mouse drag over picker. - * - * @private - * @param {Event} ev - */ - _onMouseMovePicker: function (ev) { - if (!this.pickerFlag) { - return; - } - - var offset = this.$colorpickerArea.offset(); - var top = ev.pageY - offset.top; - var left = ev.pageX - offset.left; - var saturation = Math.round(100 * left / this.$colorpickerArea.width()); - var lightness = Math.round(100 * (this.$colorpickerArea.height() - top) / this.$colorpickerArea.height()); - saturation = utils.confine(saturation, 0, 100); - lightness = utils.confine(lightness, 0, 100); - - this._updateHsl(this.colorComponents.hue, saturation, lightness); - this._updateUI(); - }, - /** - * Updates color when user starts clicking on slider. - * - * @private - * @param {Event} ev - */ - _onMouseDownSlider: function (ev) { - this.sliderFlag = true; - ev.preventDefault(); - this._onMouseMoveSlider(ev); - }, - /** - * Updates hue value on mouse drag over slider. - * - * @private - * @param {Event} ev - */ - _onMouseMoveSlider: function (ev) { - if (!this.sliderFlag) { - return; - } - - var y = ev.pageY - this.$colorSlider.offset().top; - var hue = Math.round(360 * y / this.$colorSlider.height()); - hue = utils.confine(hue, 0, 360); - - this._updateHsl(hue, this.colorComponents.saturation, this.colorComponents.lightness); - this._updateUI(); - }, - /** - * Updates opacity when user starts clicking on opacity slider. - * - * @private - * @param {Event} ev - */ - _onMouseDownOpacitySlider: function (ev) { - this.opacitySliderFlag = true; - ev.preventDefault(); - this._onMouseMoveOpacitySlider(ev); - }, - /** - * Updates opacity value on mouse drag over opacity slider. - * - * @private - * @param {Event} ev - */ - _onMouseMoveOpacitySlider: function (ev) { - if (!this.opacitySliderFlag || this.options.noTransparency) { - return; - } - - var y = ev.pageY - this.$opacitySlider.offset().top; - var opacity = Math.round(100 * (1 - y / this.$opacitySlider.height())); - opacity = utils.confine(opacity, 0, 100); - - this._updateOpacity(opacity); - this._updateUI(); - }, - /** - * Called when input value is changed -> Updates UI: Set picker and slider - * position and set colors. - * - * @private - * @param {Event} ev - */ - _onChangeInputs: function (ev) { - switch ($(ev.target).data('colorMethod')) { - case 'hex': - this._updateHex(this.$('.o_hex_input').val()); - break; - case 'rgb': - this._updateRgba( - parseInt(this.$('.o_red_input').val()), - parseInt(this.$('.o_green_input').val()), - parseInt(this.$('.o_blue_input').val()) - ); - break; - case 'hsl': - this._updateHsl( - parseInt(this.$('.o_hue_input').val()), - parseInt(this.$('.o_saturation_input').val()), - parseInt(this.$('.o_lightness_input').val()) - ); - break; - case 'opacity': - this._updateOpacity(parseInt(this.$('.o_opacity_input').val())); - break; - } - this._updateUI(); - this._colorSelected(); - }, -}); - -//-------------------------------------------------------------------------- -// Static -//-------------------------------------------------------------------------- +/** @odoo-module **/ /** * Converts RGB color components to HSL components. @@ -475,7 +12,7 @@ var ColorpickerWidget = Widget.extend({ * - saturation [0, 100] (float) * - lightness [0, 100] (float) */ -ColorpickerWidget.convertRgbToHsl = function (r, g, b) { +export function convertRgbToHsl(r, g, b) { if (typeof (r) !== 'number' || isNaN(r) || r < 0 || r > 255 || typeof (g) !== 'number' || isNaN(g) || g < 0 || g > 255 || typeof (b) !== 'number' || isNaN(b) || b < 0 || b > 255) { @@ -524,7 +61,7 @@ ColorpickerWidget.convertRgbToHsl = function (r, g, b) { * - green [0, 255] (integer) * - blue [0, 255] (integer) */ -ColorpickerWidget.convertHslToRgb = function (h, s, l) { +export function convertHslToRgb(h, s, l) { if (typeof (h) !== 'number' || isNaN(h) || h < 0 || h > 360 || typeof (s) !== 'number' || isNaN(s) || s < 0 || s > 100 || typeof (l) !== 'number' || isNaN(l) || l < 0 || l > 100) { @@ -603,7 +140,7 @@ ColorpickerWidget.convertHslToRgb = function (h, s, l) { * @param {float} a - [0, 100] * @returns {string} */ -ColorpickerWidget.convertRgbaToCSSColor = function (r, g, b, a) { +export function convertRgbaToCSSColor(r, g, b, a) { if (typeof (r) !== 'number' || isNaN(r) || r < 0 || r > 255 || typeof (g) !== 'number' || isNaN(g) || g < 0 || g > 255 || typeof (b) !== 'number' || isNaN(b) || b < 0 || b > 255) { @@ -631,7 +168,7 @@ ColorpickerWidget.convertRgbaToCSSColor = function (r, g, b, a) { * - blue [0, 255] (integer) * - opacity [0, 100.0] (float) */ -ColorpickerWidget.convertCSSColorToRgba = function (cssColor) { +export function convertCSSColorToRgba(cssColor) { // Check if cssColor is a rgba() or rgb() color const rgba = cssColor.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/); if (rgba) { @@ -674,12 +211,12 @@ ColorpickerWidget.convertCSSColorToRgba = function (cssColor) { * @returns {string} - the normalized css color or the given css color if it * failed to be normalized */ -ColorpickerWidget.normalizeCSSColor = function (cssColor) { - const rgba = ColorpickerWidget.convertCSSColorToRgba(cssColor); +export function normalizeCSSColor(cssColor) { + const rgba = convertCSSColorToRgba(cssColor); if (!rgba) { return cssColor; } - return ColorpickerWidget.convertRgbaToCSSColor(rgba.red, rgba.green, rgba.blue, rgba.opacity); + return convertRgbaToCSSColor(rgba.red, rgba.green, rgba.blue, rgba.opacity); }; /** * Checks if a given string is a css color. @@ -688,10 +225,9 @@ ColorpickerWidget.normalizeCSSColor = function (cssColor) { * @param {string} cssColor * @returns {boolean} */ -ColorpickerWidget.isCSSColor = function (cssColor) { - return ColorpickerWidget.convertCSSColorToRgba(cssColor) !== false; +export function isCSSColor(cssColor) { + return convertCSSColorToRgba(cssColor) !== false; }; - /** * Mixes two colors by applying a weighted average of their red, green and blue * components. @@ -702,55 +238,11 @@ ColorpickerWidget.isCSSColor = function (cssColor) { * @param {number} weight - a number between 0 and 1 * @returns {string} - mixed color in hexadecimal format */ -ColorpickerWidget.mixCssColors = function (cssColor1, cssColor2, weight) { - const rgba1 = ColorpickerWidget.convertCSSColorToRgba(cssColor1); - const rgba2 = ColorpickerWidget.convertCSSColorToRgba(cssColor2); +export function mixCssColors(cssColor1, cssColor2, weight) { + const rgba1 = convertCSSColorToRgba(cssColor1); + const rgba2 = convertCSSColorToRgba(cssColor2); const rgb1 = [rgba1.red, rgba1.green, rgba1.blue]; const rgb2 = [rgba2.red, rgba2.green, rgba2.blue]; const [r, g, b] = rgb1.map((_, idx) => Math.round(rgb2[idx] + (rgb1[idx] - rgb2[idx]) * weight)); - return ColorpickerWidget.convertRgbaToCSSColor(r, g, b); -}; - -const ColorpickerDialog = Dialog.extend({ - /** - * @override - */ - init: function (parent, options) { - this.options = options || {}; - this._super(parent, Object.assign({ - size: 'small', - title: _t('Pick a color'), - buttons: [ - {text: _t('Choose'), classes: 'btn-primary', close: true, click: this._onFinalPick.bind(this)}, - {text: _t('Discard'), close: true}, - ], - }, this.options)); - }, - /** - * @override - */ - start: function () { - const proms = [this._super(...arguments)]; - this.colorPicker = new ColorpickerWidget(this, Object.assign({ - colorPreview: true, - }, this.options)); - proms.push(this.colorPicker.appendTo(this.$el)); - return Promise.all(proms); - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * @private - */ - _onFinalPick: function () { - this.trigger_up('colorpicker:saved', this.colorPicker.colorComponents); - }, -}); - -export default { - ColorpickerDialog: ColorpickerDialog, - ColorpickerWidget: ColorpickerWidget, + return convertRgbaToCSSColor(r, g, b); }; diff --git a/addons/web/static/src/legacy/js/widgets/colorpicker_dialog.js b/addons/web/static/src/legacy/js/widgets/colorpicker_dialog.js index c87eb8a2544..e6bc6232ca8 100644 --- a/addons/web/static/src/legacy/js/widgets/colorpicker_dialog.js +++ b/addons/web/static/src/legacy/js/widgets/colorpicker_dialog.js @@ -1,716 +1,10 @@ -/** @odoo-module alias=web.Colorpicker **/ +/** @odoo-module **/ import core from "web.core"; -import utils from "web.utils"; import Dialog from "web.Dialog"; -import Widget from "web.Widget"; -import { uniqueId } from "@web/core/utils/functions"; -import { debounce, throttleForAnimation } from "@web/core/utils/timing"; var _t = core._t; -var ColorpickerWidget = Widget.extend({ - template: 'Colorpicker', - events: { - 'click': '_onClick', - 'keypress': '_onKeypress', - 'mousedown .o_color_pick_area': '_onMouseDownPicker', - 'mousedown .o_color_slider': '_onMouseDownSlider', - 'mousedown .o_opacity_slider': '_onMouseDownOpacitySlider', - 'change .o_color_picker_inputs': '_onChangeInputs', - }, - - /** - * @constructor - * @param {Widget} parent - * @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); - options = options || {}; - - this.pickerFlag = false; - this.sliderFlag = false; - this.opacitySliderFlag = false; - this.colorComponents = {}; - this.uniqueId = uniqueId("colorpicker"); - this.selectedHexValue = ''; - - this.options = Object.assign({}, options); - }, - /** - * @override - */ - start: function () { - this.$colorpickerArea = this.$('.o_color_pick_area'); - this.$colorpickerPointer = this.$('.o_picker_pointer'); - this.$colorSlider = this.$('.o_color_slider'); - this.$colorSliderPointer = this.$('.o_slider_pointer'); - this.$opacitySlider = this.$('.o_opacity_slider'); - this.$opacitySliderPointer = this.$('.o_opacity_pointer'); - - var defaultColor = this.options.defaultColor || '#FF0000'; - var rgba = ColorpickerWidget.convertCSSColorToRgba(defaultColor); - if (rgba) { - this._updateRgba(rgba.red, rgba.green, rgba.blue, rgba.opacity); - } - - // Pre-fill the inputs. This is because on safari, the baseline for empty - // input is not the baseline of where the text would be, but the bottom - // of the input itself. (see https://bugs.webkit.org/show_bug.cgi?id=142968) - // This will cause the first _updateUI to alter the layout of the colorpicker - // which will change its height. Changing the height of an element inside of - // the callback to a ResizeObserver observing it will cause an error - // (ResizeObserver loop completed with undelivered notifications) that cannot - // be caught, which will open the crash manager. Prefilling the inputs sets - // the baseline correctly from the start so the layout doesn't change. - Object.entries(this.colorComponents).forEach(([component, value]) => { - const input = this.el.querySelector(`.o_${component}_input`); - if (input) { - input.value = value; - } - }); - const resizeObserver = new window.ResizeObserver(() => { - this._updateUI(); - }); - resizeObserver.observe(this.el); - - // Need to be bound on all documents to work in all possible cases (we - // have to be able to start dragging/moving from the colorpicker to - // anywhere on the screen, crossing iframes). - this.$documents = $([window.top, ...Array.from(window.top.frames).filter(frame => { - try { - const document = frame.document; - return !!document; - } catch { - // We cannot access the document (cross origin). - return false; - } - })].map(w => w.document)); - this.throttleOnMouseMove = throttleForAnimation((ev) => { - this._onMouseMovePicker(ev); - this._onMouseMoveSlider(ev); - this._onMouseMoveOpacitySlider(ev); - }); - this.$documents.on(`mousemove.${this.uniqueId}`, this.throttleOnMouseMove); - this.$documents.on(`mouseup.${this.uniqueId}`, debounce((ev) => { - if (this.pickerFlag || this.sliderFlag || this.opacitySliderFlag) { - this._colorSelected(); - } - this.pickerFlag = false; - this.sliderFlag = false; - this.opacitySliderFlag = false; - }, 10)); - - this.previewActive = true; - return this._super.apply(this, arguments); - }, - /** - * @override - */ - destroy: function () { - this._super.apply(this, arguments); - if (this.throttleOnMouseMove) { - this.$documents.off(`.${this.uniqueId}`); - this.throttleOnMouseMove.cancel(); - } - }, - /** - * Sets the currently selected color - * - * @param {string} color rgb[a] - */ - 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(); - } - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Updates input values, color preview, picker and slider pointer positions. - * - * @private - */ - _updateUI: function () { - var self = this; - - // Update inputs - for (const [color, value] of Object.entries(this.colorComponents)) { - self.$(`.o_${color}_input`).val(value); - } - - // Update preview - this.$('.o_color_preview').css('background-color', this.colorComponents.cssColor); - - // Update picker area and picker pointer position - this.$colorpickerArea.css( - "background-color", - `hsl(${this.colorComponents.hue}, 100%, 50%)` - ); - var top = (100 - this.colorComponents.lightness) * this.$colorpickerArea.height() / 100; - var left = this.colorComponents.saturation * this.$colorpickerArea.width() / 100; - this.$colorpickerPointer.css({ - top: (top - 5) + 'px', - left: (left - 5) + 'px', - }); - - // Update color slider position - var height = this.$colorSlider.height(); - var y = this.colorComponents.hue * height / 360; - this.$colorSliderPointer.css('top', Math.round(y - 2)); - - if (! this.options.noTransparency) { - // Update opacity slider position - var heightOpacity = this.$opacitySlider.height(); - var z = heightOpacity * (1 - this.colorComponents.opacity / 100.0); - this.$opacitySliderPointer.css('top', Math.round(z - 2)); - - // Add gradient color on opacity slider - this.$opacitySlider.css('background', 'linear-gradient(' + this.colorComponents.hex + ' 0%, transparent 100%)'); - } - }, - /** - * Updates colors according to given hex value. Opacity is left unchanged. - * - * @private - * @param {string} hex - hexadecimal code - */ - _updateHex: function (hex) { - var rgb = ColorpickerWidget.convertCSSColorToRgba(hex); - if (!rgb) { - return; - } - Object.assign(this.colorComponents, - {hex: hex}, - rgb, - ColorpickerWidget.convertRgbToHsl(rgb.red, rgb.green, rgb.blue) - ); - this._updateCssColor(); - }, - /** - * Updates colors according to given RGB values. - * - * @private - * @param {integer} r - * @param {integer} g - * @param {integer} b - * @param {integer} [a] - */ - _updateRgba: function (r, g, b, a) { - // Remove full transparency in case some lightness is added - const opacity = a || this.colorComponents.opacity; - if (opacity < 0.1 && (r > 0.1 || g > 0.1 || b > 0.1)) { - a = 100; - } - - // We update the hexadecimal code by transforming into a css color and - // ignoring the opacity (we don't display opacity component in hexa as - // not supported on all browsers) - var hex = ColorpickerWidget.convertRgbaToCSSColor(r, g, b); - if (!hex) { - return; - } - Object.assign(this.colorComponents, - {red: r, green: g, blue: b}, - a === undefined ? {} : {opacity: a}, - {hex: hex}, - ColorpickerWidget.convertRgbToHsl(r, g, b) - ); - this._updateCssColor(); - }, - /** - * Updates colors according to given HSL values. - * - * @private - * @param {integer} h - * @param {integer} s - * @param {integer} l - */ - _updateHsl: function (h, s, l) { - // Remove full transparency in case some lightness is added - let a = this.colorComponents.opacity; - if (a < 0.1 && l > 0.1) { - a = 100; - } - - var rgb = ColorpickerWidget.convertHslToRgb(h, s, l); - if (!rgb) { - return; - } - // We receive an hexa as we ignore the opacity - const hex = ColorpickerWidget.convertRgbaToCSSColor(rgb.red, rgb.green, rgb.blue); - Object.assign(this.colorComponents, - {hue: h, saturation: s, lightness: l}, - rgb, - {hex: hex}, - {opacity: a}, - ); - this._updateCssColor(); - }, - /** - * Updates color opacity. - * - * @private - * @param {integer} a - */ - _updateOpacity: function (a) { - if (a < 0 || a > 100) { - return; - } - Object.assign(this.colorComponents, - {opacity: a} - ); - this._updateCssColor(); - }, - /** - * Trigger an event to annonce that the widget value has changed - * - * @private - */ - _colorSelected: function () { - this.trigger_up('colorpicker_select', this.colorComponents); - }, - /** - * Updates css color representation. - * - * @private - */ - _updateCssColor: function () { - const r = this.colorComponents.red; - const g = this.colorComponents.green; - const b = this.colorComponents.blue; - const a = this.colorComponents.opacity; - Object.assign(this.colorComponents, - {cssColor: ColorpickerWidget.convertRgbaToCSSColor(r, g, b, a)} - ); - if (this.previewActive) { - this.trigger_up('colorpicker_preview', this.colorComponents); - } - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * @private - * @param {Event} ev - */ - _onKeypress: function (ev) { - if (ev.charCode === $.ui.keyCode.ENTER) { - if (ev.target.tagName === 'INPUT') { - this._onChangeInputs(ev); - } - ev.preventDefault(); - this.trigger_up('enter_key_color_colorpicker'); - } - }, - /** - * @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) { - ev.target.select(); - this.selectedHexValue = ev.target.value; - return; - } - this.selectedHexValue = ''; - }, - /** - * Updates color when the user starts clicking on the picker. - * - * @private - * @param {Event} ev - */ - _onMouseDownPicker: function (ev) { - this.pickerFlag = true; - ev.preventDefault(); - this._onMouseMovePicker(ev); - }, - /** - * Updates saturation and lightness values on mouse drag over picker. - * - * @private - * @param {Event} ev - */ - _onMouseMovePicker: function (ev) { - if (!this.pickerFlag) { - return; - } - - var offset = this.$colorpickerArea.offset(); - var top = ev.pageY - offset.top; - var left = ev.pageX - offset.left; - var saturation = Math.round(100 * left / this.$colorpickerArea.width()); - var lightness = Math.round(100 * (this.$colorpickerArea.height() - top) / this.$colorpickerArea.height()); - saturation = utils.confine(saturation, 0, 100); - lightness = utils.confine(lightness, 0, 100); - - this._updateHsl(this.colorComponents.hue, saturation, lightness); - this._updateUI(); - }, - /** - * Updates color when user starts clicking on slider. - * - * @private - * @param {Event} ev - */ - _onMouseDownSlider: function (ev) { - this.sliderFlag = true; - ev.preventDefault(); - this._onMouseMoveSlider(ev); - }, - /** - * Updates hue value on mouse drag over slider. - * - * @private - * @param {Event} ev - */ - _onMouseMoveSlider: function (ev) { - if (!this.sliderFlag) { - return; - } - - var y = ev.pageY - this.$colorSlider.offset().top; - var hue = Math.round(360 * y / this.$colorSlider.height()); - hue = utils.confine(hue, 0, 360); - - this._updateHsl(hue, this.colorComponents.saturation, this.colorComponents.lightness); - this._updateUI(); - }, - /** - * Updates opacity when user starts clicking on opacity slider. - * - * @private - * @param {Event} ev - */ - _onMouseDownOpacitySlider: function (ev) { - this.opacitySliderFlag = true; - ev.preventDefault(); - this._onMouseMoveOpacitySlider(ev); - }, - /** - * Updates opacity value on mouse drag over opacity slider. - * - * @private - * @param {Event} ev - */ - _onMouseMoveOpacitySlider: function (ev) { - if (!this.opacitySliderFlag || this.options.noTransparency) { - return; - } - - var y = ev.pageY - this.$opacitySlider.offset().top; - var opacity = Math.round(100 * (1 - y / this.$opacitySlider.height())); - opacity = utils.confine(opacity, 0, 100); - - this._updateOpacity(opacity); - this._updateUI(); - }, - /** - * Called when input value is changed -> Updates UI: Set picker and slider - * position and set colors. - * - * @private - * @param {Event} ev - */ - _onChangeInputs: function (ev) { - switch ($(ev.target).data('colorMethod')) { - case 'hex': - this._updateHex(this.$('.o_hex_input').val()); - break; - case 'rgb': - this._updateRgba( - parseInt(this.$('.o_red_input').val()), - parseInt(this.$('.o_green_input').val()), - parseInt(this.$('.o_blue_input').val()) - ); - break; - case 'hsl': - this._updateHsl( - parseInt(this.$('.o_hue_input').val()), - parseInt(this.$('.o_saturation_input').val()), - parseInt(this.$('.o_lightness_input').val()) - ); - break; - case 'opacity': - this._updateOpacity(parseInt(this.$('.o_opacity_input').val())); - break; - } - this._updateUI(); - this._colorSelected(); - }, -}); - -//-------------------------------------------------------------------------- -// Static -//-------------------------------------------------------------------------- - -/** - * Converts RGB color components to HSL components. - * - * @static - * @param {integer} r - [0, 255] - * @param {integer} g - [0, 255] - * @param {integer} b - [0, 255] - * @returns {Object|false} - * - hue [0, 360[ (float) - * - saturation [0, 100] (float) - * - lightness [0, 100] (float) - */ -ColorpickerWidget.convertRgbToHsl = function (r, g, b) { - if (typeof (r) !== 'number' || isNaN(r) || r < 0 || r > 255 - || typeof (g) !== 'number' || isNaN(g) || g < 0 || g > 255 - || typeof (b) !== 'number' || isNaN(b) || b < 0 || b > 255) { - return false; - } - - var red = r / 255; - var green = g / 255; - var blue = b / 255; - var maxColor = Math.max(red, green, blue); - var minColor = Math.min(red, green, blue); - var delta = maxColor - minColor; - var hue = 0; - var saturation = 0; - var lightness = (maxColor + minColor) / 2; - if (delta) { - if (maxColor === red) { - hue = (green - blue) / delta; - } - if (maxColor === green) { - hue = 2 + (blue - red) / delta; - } - if (maxColor === blue) { - hue = 4 + (red - green) / delta; - } - if (maxColor) { - saturation = delta / (1 - Math.abs(2 * lightness - 1)); - } - } - hue = 60 * hue; - return { - hue: hue < 0 ? hue + 360 : hue, - saturation: saturation * 100, - lightness: lightness * 100, - }; -}; -/** - * Converts HSL color components to RGB components. - * - * @static - * @param {number} h - [0, 360[ (float) - * @param {number} s - [0, 100] (float) - * @param {number} l - [0, 100] (float) - * @returns {Object|false} - * - red [0, 255] (integer) - * - green [0, 255] (integer) - * - blue [0, 255] (integer) - */ -ColorpickerWidget.convertHslToRgb = function (h, s, l) { - if (typeof (h) !== 'number' || isNaN(h) || h < 0 || h > 360 - || typeof (s) !== 'number' || isNaN(s) || s < 0 || s > 100 - || typeof (l) !== 'number' || isNaN(l) || l < 0 || l > 100) { - return false; - } - - var huePrime = h / 60; - var saturation = s / 100; - var lightness = l / 100; - var chroma = saturation * (1 - Math.abs(2 * lightness - 1)); - var secondComponent = chroma * (1 - Math.abs(huePrime % 2 - 1)); - var lightnessAdjustment = lightness - chroma / 2; - var precision = 255; - chroma = Math.round((chroma + lightnessAdjustment) * precision); - secondComponent = Math.round((secondComponent + lightnessAdjustment) * precision); - lightnessAdjustment = Math.round((lightnessAdjustment) * precision); - if (huePrime >= 0 && huePrime < 1) { - return { - red: chroma, - green: secondComponent, - blue: lightnessAdjustment, - }; - } - if (huePrime >= 1 && huePrime < 2) { - return { - red: secondComponent, - green: chroma, - blue: lightnessAdjustment, - }; - } - if (huePrime >= 2 && huePrime < 3) { - return { - red: lightnessAdjustment, - green: chroma, - blue: secondComponent, - }; - } - if (huePrime >= 3 && huePrime < 4) { - return { - red: lightnessAdjustment, - green: secondComponent, - blue: chroma, - }; - } - if (huePrime >= 4 && huePrime < 5) { - return { - red: secondComponent, - green: lightnessAdjustment, - blue: chroma, - }; - } - if (huePrime >= 5 && huePrime <= 6) { - return { - red: chroma, - green: lightnessAdjustment, - blue: secondComponent, - }; - } - return false; -}; -/** - * Converts RGBA color components to a normalized CSS color: if the opacity - * is invalid or equal to 100, a hex is returned; otherwise a rgba() css color - * is returned. - * - * Those choice have multiple reason: - * - A hex color is more common to c/c from other utilities on the web and is - * also shorter than rgb() css colors - * - Opacity in hexadecimal notations is not supported on all browsers and is - * also less common to use. - * - * @static - * @param {integer} r - [0, 255] - * @param {integer} g - [0, 255] - * @param {integer} b - [0, 255] - * @param {float} a - [0, 100] - * @returns {string} - */ -ColorpickerWidget.convertRgbaToCSSColor = function (r, g, b, a) { - if (typeof (r) !== 'number' || isNaN(r) || r < 0 || r > 255 - || typeof (g) !== 'number' || isNaN(g) || g < 0 || g > 255 - || typeof (b) !== 'number' || isNaN(b) || b < 0 || b > 255) { - return false; - } - if (typeof (a) !== 'number' || isNaN(a) || a < 0 || Math.abs(a - 100) < Number.EPSILON) { - const rr = r < 16 ? '0' + r.toString(16) : r.toString(16); - const gg = g < 16 ? '0' + g.toString(16) : g.toString(16); - const bb = b < 16 ? '0' + b.toString(16) : b.toString(16); - return (`#${rr}${gg}${bb}`).toUpperCase(); - } - return `rgba(${r}, ${g}, ${b}, ${parseFloat((a / 100.0).toFixed(3))})`; -}; -/** - * Converts a CSS color (rgb(), rgba(), hexadecimal) to RGBA color components. - * - * Note: we don't support using and displaying hexadecimal color with opacity - * but this method allows to receive one and returns the correct opacity value. - * - * @static - * @param {string} cssColor - hexadecimal code or rgb() or rgba() - * @returns {Object|false} - * - red [0, 255] (integer) - * - green [0, 255] (integer) - * - blue [0, 255] (integer) - * - opacity [0, 100.0] (float) - */ -ColorpickerWidget.convertCSSColorToRgba = function (cssColor) { - // Check if cssColor is a rgba() or rgb() color - const rgba = cssColor.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/); - if (rgba) { - if (rgba[4] === undefined) { - rgba[4] = 1; - } - return { - red: parseInt(rgba[1]), - green: parseInt(rgba[2]), - blue: parseInt(rgba[3]), - opacity: Math.round(parseFloat(rgba[4]) * 100), - }; - } - - // Otherwise, check if cssColor is an hexadecimal code color - if (/^#([0-9A-F]{6}|[0-9A-F]{8})$/i.test(cssColor)) { - return { - red: parseInt(cssColor.substr(1, 2), 16), - green: parseInt(cssColor.substr(3, 2), 16), - blue: parseInt(cssColor.substr(5, 2), 16), - opacity: (cssColor.length === 9 ? (parseInt(cssColor.substr(7, 2), 16) / 255) : 1) * 100, - }; - } - - // TODO maybe implement a support for receiving css color like 'red' or - // 'transparent' (which are now considered non-css color by isCSSColor...) - // Note: however, if ever implemented be careful of 'white'/'black' which - // actually are color names for our color system... - - return false; -}; -/** - * Converts a CSS color (rgb(), rgba(), hexadecimal) to a normalized version - * of the same color (@see convertRgbaToCSSColor). - * - * Normalized color can be safely compared using string comparison. - * - * @static - * @param {string} cssColor - hexadecimal code or rgb() or rgba() - * @returns {string} - the normalized css color or the given css color if it - * failed to be normalized - */ -ColorpickerWidget.normalizeCSSColor = function (cssColor) { - const rgba = ColorpickerWidget.convertCSSColorToRgba(cssColor); - if (!rgba) { - return cssColor; - } - return ColorpickerWidget.convertRgbaToCSSColor(rgba.red, rgba.green, rgba.blue, rgba.opacity); -}; -/** - * Checks if a given string is a css color. - * - * @static - * @param {string} cssColor - * @returns {boolean} - */ -ColorpickerWidget.isCSSColor = function (cssColor) { - return ColorpickerWidget.convertCSSColorToRgba(cssColor) !== false; -}; - -/** - * Mixes two colors by applying a weighted average of their red, green and blue - * components. - * - * @static - * @param {string} cssColor1 - hexadecimal code or rgb() or rgba() - * @param {string} cssColor2 - hexadecimal code or rgb() or rgba() - * @param {number} weight - a number between 0 and 1 - * @returns {string} - mixed color in hexadecimal format - */ -ColorpickerWidget.mixCssColors = function (cssColor1, cssColor2, weight) { - const rgba1 = ColorpickerWidget.convertCSSColorToRgba(cssColor1); - const rgba2 = ColorpickerWidget.convertCSSColorToRgba(cssColor2); - const rgb1 = [rgba1.red, rgba1.green, rgba1.blue]; - const rgb2 = [rgba2.red, rgba2.green, rgba2.blue]; - const [r, g, b] = rgb1.map((_, idx) => Math.round(rgb2[idx] + (rgb1[idx] - rgb2[idx]) * weight)); - return ColorpickerWidget.convertRgbaToCSSColor(r, g, b); -}; - const ColorpickerDialog = Dialog.extend({ /** * @override @@ -752,5 +46,4 @@ const ColorpickerDialog = Dialog.extend({ export default { ColorpickerDialog: ColorpickerDialog, - ColorpickerWidget: ColorpickerWidget, }; From cb4c2091b6552978520a5ed6795b77b7e7dd12bf Mon Sep 17 00:00:00 2001 From: Nicolas Bayet Date: Mon, 10 Jul 2023 10:17:36 +0000 Subject: [PATCH 4/7] [REF] web: move colorpicker.xml Part-of: odoo/odoo#118966 --- .../static/src/{legacy/xml => core/colorpicker}/colorpicker.xml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename addons/web/static/src/{legacy/xml => core/colorpicker}/colorpicker.xml (100%) diff --git a/addons/web/static/src/legacy/xml/colorpicker.xml b/addons/web/static/src/core/colorpicker/colorpicker.xml similarity index 100% rename from addons/web/static/src/legacy/xml/colorpicker.xml rename to addons/web/static/src/core/colorpicker/colorpicker.xml From dddba97927c1086f22beebe406f32c92290a6a6f Mon Sep 17 00:00:00 2001 From: Nicolas Bayet Date: Wed, 7 Jun 2023 08:11:27 +0000 Subject: [PATCH 5/7] [REF] web_editor: rename image_crop_widget.js to image_crop.js In a subsequent commit, the ImageCropWidget will be converted into an Owl component called ImageCrop. To match the new name, this commit rename the file. task-3175256 Part-of: odoo/odoo#118966 --- .../js/wysiwyg/widgets/{image_crop_widget.js => image_crop.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename addons/web_editor/static/src/js/wysiwyg/widgets/{image_crop_widget.js => image_crop.js} (100%) diff --git a/addons/web_editor/static/src/js/wysiwyg/widgets/image_crop_widget.js b/addons/web_editor/static/src/js/wysiwyg/widgets/image_crop.js similarity index 100% rename from addons/web_editor/static/src/js/wysiwyg/widgets/image_crop_widget.js rename to addons/web_editor/static/src/js/wysiwyg/widgets/image_crop.js From a2dab277007494cde0aa60df3ca866d1103f923f Mon Sep 17 00:00:00 2001 From: Nicolas Bayet Date: Tue, 20 Jun 2023 13:39:38 +0000 Subject: [PATCH 6/7] [REF] web_editor,*: move media_dialog_wrapper to its own file For organisation purpose, move the media_dialog_wrapper to its own file. task-3175256 Part-of: odoo/odoo#118966 --- .../static/src/snippets/s_rating/options.js | 2 +- .../components/media_dialog/media_dialog.js | 13 ------------- .../media_dialog/media_dialog_wrapper.js | 19 +++++++++++++++++++ .../static/src/js/editor/snippets.options.js | 2 +- .../static/src/snippets/s_rating/options.js | 2 +- .../static/src/js/website_sale.editor.js | 3 ++- 6 files changed, 24 insertions(+), 17 deletions(-) create mode 100644 addons/web_editor/static/src/components/media_dialog/media_dialog_wrapper.js diff --git a/addons/mass_mailing/static/src/snippets/s_rating/options.js b/addons/mass_mailing/static/src/snippets/s_rating/options.js index 0b69c328dd2..583ebc29eda 100644 --- a/addons/mass_mailing/static/src/snippets/s_rating/options.js +++ b/addons/mass_mailing/static/src/snippets/s_rating/options.js @@ -1,7 +1,7 @@ /** @odoo-module alias=mass_mailing.s_rating_options **/ import { ComponentWrapper } from "web.OwlCompatibility"; -import { MediaDialogWrapper } from "@web_editor/components/media_dialog/media_dialog"; +import { MediaDialogWrapper } from "@web_editor/components/media_dialog/media_dialog_wrapper"; import options from "web_editor.snippets.options"; options.registry.Rating = options.Class.extend({ diff --git a/addons/web_editor/static/src/components/media_dialog/media_dialog.js b/addons/web_editor/static/src/components/media_dialog/media_dialog.js index 06150f1e780..9eb8b50faa3 100644 --- a/addons/web_editor/static/src/components/media_dialog/media_dialog.js +++ b/addons/web_editor/static/src/components/media_dialog/media_dialog.js @@ -236,16 +236,3 @@ MediaDialog.components = { Dialog, Notebook, }; - -export class MediaDialogWrapper extends Component { - setup() { - this.dialogs = useWowlService('dialog'); - - onRendered(() => { - this.dialogs.add(MediaDialog, this.props, { - onClose: this.props.close, - }); - }); - } -} -MediaDialogWrapper.template = xml``; diff --git a/addons/web_editor/static/src/components/media_dialog/media_dialog_wrapper.js b/addons/web_editor/static/src/components/media_dialog/media_dialog_wrapper.js new file mode 100644 index 00000000000..319718babd5 --- /dev/null +++ b/addons/web_editor/static/src/components/media_dialog/media_dialog_wrapper.js @@ -0,0 +1,19 @@ + +/** @odoo-module **/ + +import { useWowlService } from '@web/legacy/utils'; +import { Component, onRendered, xml } from "@odoo/owl"; +import { MediaDialog } from "./media_dialog"; + +export class MediaDialogWrapper extends Component { + setup() { + this.dialogs = useWowlService('dialog'); + + onRendered(() => { + this.dialogs.add(MediaDialog, this.props, { + onClose: this.props.close, + }); + }); + } +} +MediaDialogWrapper.template = xml``; diff --git a/addons/web_editor/static/src/js/editor/snippets.options.js b/addons/web_editor/static/src/js/editor/snippets.options.js index 2829d810b2a..d6f4768bd2b 100644 --- a/addons/web_editor/static/src/js/editor/snippets.options.js +++ b/addons/web_editor/static/src/js/editor/snippets.options.js @@ -1,7 +1,7 @@ /** @odoo-module alias=web_editor.snippets.options **/ import { ComponentWrapper } from "web.OwlCompatibility"; -import { MediaDialogWrapper } from "@web_editor/components/media_dialog/media_dialog"; +import { MediaDialogWrapper } from "@web_editor/components/media_dialog/media_dialog_wrapper"; import core from "web.core"; import {ColorpickerWidget} from "web.Colorpicker"; import Dialog from "web.Dialog"; diff --git a/addons/website/static/src/snippets/s_rating/options.js b/addons/website/static/src/snippets/s_rating/options.js index faf40e92ccb..d6d334c1d5a 100644 --- a/addons/website/static/src/snippets/s_rating/options.js +++ b/addons/website/static/src/snippets/s_rating/options.js @@ -1,7 +1,7 @@ /** @odoo-module **/ import { ComponentWrapper } from "web.OwlCompatibility"; -import { MediaDialogWrapper } from "@web_editor/components/media_dialog/media_dialog"; +import { MediaDialogWrapper } from "@web_editor/components/media_dialog/media_dialog_wrapper"; import options from "web_editor.snippets.options"; diff --git a/addons/website_sale/static/src/js/website_sale.editor.js b/addons/website_sale/static/src/js/website_sale.editor.js index 05d0d454c03..3d49adec17e 100644 --- a/addons/website_sale/static/src/js/website_sale.editor.js +++ b/addons/website_sale/static/src/js/website_sale.editor.js @@ -3,7 +3,8 @@ import options from "web_editor.snippets.options"; import Wysiwyg from "website.wysiwyg"; import { ComponentWrapper } from "web.OwlCompatibility"; -import { MediaDialog, MediaDialogWrapper } from "@web_editor/components/media_dialog/media_dialog"; +import { MediaDialog } from "@web_editor/components/media_dialog/media_dialog"; +import { MediaDialogWrapper } from "@web_editor/components/media_dialog/media_dialog_wrapper"; import { useWowlService } from "@web/legacy/utils"; import {qweb, _t} from "web.core"; import {Markup} from "web.utils"; From d7245d2abf528d093226c80e40975e63d61e8997 Mon Sep 17 00:00:00 2001 From: Nicolas Bayet Date: Thu, 8 Jun 2023 07:11:50 +0000 Subject: [PATCH 7/7] [REF] web_editor,*: convert wysiwyg to owl The goal of this commit is to remove all dependency to legacy in the wysiwyg because the wysiwyg has to be loaded on a lot of form view (through the html_field). To remove the dependencies, all the legacy widget that the wysiwyg uses had to be converted to owl. In order to finish the PR faster, only a partial conversion of the widgets is done, changing only the part of the code that was necessary for it to work instead of rewriting the whole widget from scratch. Another pass should be done to convert all those widgets to fully embrace the owl paradigm. task-3175256 Part-of: odoo/odoo#118966 --- addons/mass_mailing/__manifest__.py | 24 +- .../data/mailing_data_templates.xml | 71 +- .../static/src/js/mass_mailing_html_field.js | 41 +- .../src/js/mass_mailing_link_dialog_fix.js | 72 - .../static/src/js/mass_mailing_snippets.js | 6 +- .../static/src/js/mass_mailing_wysiwyg.js | 93 +- .../static/src/js/snippets.editor.js | 5 +- .../static/src/scss/mass_mailing_mail.scss | 65 + .../static/src/xml/mass_mailing.editor.xml | 8 - .../static/tests/mass_mailing_html_tests.js | 36 +- addons/mass_mailing/views/assets.xml | 18 - .../wizard/mail_compose_message.py | 7 +- .../wizard/mailing_mailing_test.py | 8 +- addons/project/__manifest__.py | 1 - .../static/tests/field_html_file_upload.js | 8 +- addons/web/__manifest__.py | 7 +- .../src/core/colorpicker/colorpicker.js | 310 +++-- .../src/core/colorpicker/colorpicker.xml | 13 +- .../legacy/js/widgets/colorpicker_dialog.js | 22 +- addons/web_editor/__manifest__.py | 38 +- .../components/emoji_popover/emoji_popover.js | 19 - .../components/media_dialog/media_dialog.js | 3 +- .../components/media_dialog/video_selector.js | 13 +- .../media_dialog/video_selector.xml | 5 - .../static/src/js/backend/html_field.js | 155 +-- .../static/src/js/backend/html_field.xml | 6 +- .../web_editor/static/src/js/common/utils.js | 13 +- .../static/src/js/editor/custom_colors.js | 2 +- .../js/editor/odoo-editor/src/OdooEditor.js | 1 + .../static/src/js/editor/snippets.editor.js | 219 ++- .../static/src/js/editor/snippets.options.js | 175 ++- .../static/src/js/editor/toolbar.js | 123 +- .../js/frontend/loadWysiwygFromTextarea.js | 63 + .../static/src/js/frontend/loader.js | 72 +- .../static/src/js/frontend/loader_loading.js | 2 +- .../static/src/js/wysiwyg/conflict_dialog.js | 10 + .../static/src/js/wysiwyg/conflict_dialog.xml | 22 + .../static/src/js/wysiwyg/dialog.js | 90 -- .../src/js/wysiwyg/linkDialogCommand.js | 2 +- .../static/src/js/wysiwyg/widget.js | 6 - .../src/js/wysiwyg/widgets/alt_dialog.js | 90 +- .../src/js/wysiwyg/widgets/alt_dialog.xml | 33 + .../src/js/wysiwyg/widgets/color_palette.js | 727 +++++----- .../src/js/wysiwyg/widgets/image_crop.js | 258 ++-- .../static/src/js/wysiwyg/widgets/link.js | 487 +++---- .../src/js/wysiwyg/widgets/link_dialog.js | 178 +-- .../js/wysiwyg/widgets/link_popover_widget.js | 154 ++- .../src/js/wysiwyg/widgets/link_tools.js | 420 +++--- .../static/src/js/wysiwyg/widgets/widgets.js | 19 - .../static/src/js/wysiwyg/wysiwyg.js | 1221 ++++++++++------- .../static/src/js/wysiwyg/wysiwyg_iframe.js | 133 +- .../js/wysiwyg/wysiwyg_jquery_extention.js | 101 ++ addons/web_editor/static/src/xml/editor.xml | 86 +- addons/web_editor/static/src/xml/snippets.xml | 69 +- addons/web_editor/static/src/xml/wysiwyg.xml | 581 ++++---- addons/web_editor/static/tests/ajax_loader.js | 8 - addons/web_editor/static/tests/test_utils.js | 98 +- addons/web_editor/views/editor.xml | 10 - addons/website/__manifest__.py | 7 +- .../configurator/configurator.js | 8 +- .../static/src/components/dialog/edit_menu.js | 4 +- .../static/src/components/editor/editor.js | 8 +- .../static/src/components/editor/editor.xml | 5 +- .../wysiwyg_adapter/toolbar_patch.js | 13 + .../wysiwyg_adapter/wysiwyg_adapter.js | 480 +++++-- .../src/js/content/website_root_instance.js | 4 +- addons/website/static/src/js/editor/editor.js | 22 +- .../static/src/js/editor/snippets.editor.js | 83 ++ .../static/src/js/editor/snippets.options.js | 92 +- .../static/src/js/editor/widget_link.js | 50 +- .../website/static/src/js/editor/wysiwyg.js | 362 ----- .../src/js/widgets/link_popover_widget.js | 95 +- .../static/src/services/website_service.js | 11 +- .../static/src/snippets/s_chart/options.js | 4 +- .../static/src/snippets/s_countdown/000.js | 4 +- .../src/snippets/s_image_gallery/options.js | 2 +- addons/website/static/src/xml/web_editor.xml | 47 + .../website/static/src/xml/website.editor.xml | 44 - .../static/tests/tours/edit_link_popover.js | 6 +- .../website/static/tests/tours/link_tools.js | 29 +- .../tours/public_user_editor_dep_widget.js | 4 +- addons/website/static/tests/tours/rte.js | 4 +- .../static/tests/tours/website_form_editor.js | 2 +- .../tests/tours/website_text_edition.js | 2 +- addons/website_blog/static/src/js/wysiwyg.js | 40 +- addons/website_forum/__manifest__.py | 3 - .../static/src/js/website_forum.js | 17 +- .../static/src/xml/forum_wysiwyg.xml | 15 - .../views/snippets_templates.xml | 6 - .../static/src/js/website_profile.js | 4 +- .../static/src/js/website_sale.editor.js | 89 +- addons/website_slides/__manifest__.py | 2 - 92 files changed, 4209 insertions(+), 3886 deletions(-) delete mode 100644 addons/mass_mailing/static/src/js/mass_mailing_link_dialog_fix.js create mode 100644 addons/mass_mailing/static/src/scss/mass_mailing_mail.scss delete mode 100644 addons/mass_mailing/static/src/xml/mass_mailing.editor.xml delete mode 100644 addons/mass_mailing/views/assets.xml delete mode 100644 addons/web_editor/static/src/components/emoji_popover/emoji_popover.js create mode 100644 addons/web_editor/static/src/js/frontend/loadWysiwygFromTextarea.js create mode 100644 addons/web_editor/static/src/js/wysiwyg/conflict_dialog.js create mode 100644 addons/web_editor/static/src/js/wysiwyg/conflict_dialog.xml delete mode 100644 addons/web_editor/static/src/js/wysiwyg/dialog.js delete mode 100644 addons/web_editor/static/src/js/wysiwyg/widget.js create mode 100644 addons/web_editor/static/src/js/wysiwyg/widgets/alt_dialog.xml delete mode 100644 addons/web_editor/static/src/js/wysiwyg/widgets/widgets.js create mode 100644 addons/web_editor/static/src/js/wysiwyg/wysiwyg_jquery_extention.js delete mode 100644 addons/web_editor/static/tests/ajax_loader.js create mode 100644 addons/website/static/src/components/wysiwyg_adapter/toolbar_patch.js delete mode 100644 addons/website/static/src/js/editor/wysiwyg.js create mode 100644 addons/website/static/src/xml/web_editor.xml delete mode 100644 addons/website_forum/static/src/xml/forum_wysiwyg.xml diff --git a/addons/mass_mailing/__manifest__.py b/addons/mass_mailing/__manifest__.py index 06710b82b43..ea28dad1d80 100644 --- a/addons/mass_mailing/__manifest__.py +++ b/addons/mass_mailing/__manifest__.py @@ -46,7 +46,6 @@ 'views/res_config_settings_views.xml', 'views/utm_campaign_views.xml', 'views/mailing_menus.xml', - 'views/assets.xml', 'views/mailing_templates_portal_layouts.xml', 'views/mailing_templates_portal_management.xml', 'views/mailing_templates_portal_unsubscribe.xml', @@ -84,10 +83,27 @@ ], 'application': True, 'assets': { + 'mass_mailing.iframe_css_assets_edit': [ + ('include', 'mass_mailing.assets_mail_themes'), + ('include', 'web.assets_frontend'), + ('after', 'web/static/lib/bootstrap/scss/_variables.scss', 'mass_mailing/static/src/scss/mass_mailing.ui.scss'), + ('include', 'web_editor.assets_wysiwyg'), + ('include', 'web_editor.assets_legacy_wysiwyg'), + + 'mass_mailing/static/src/scss/mass_mailing_mail.scss', + ], + 'mass_mailing.iframe_css_assets_readonly': [ + 'mass_mailing/static/src/scss/mass_mailing_mail.scss', + 'mass_mailing/static/src/css/basic_theme_readonly.css' + ], 'mass_mailing.mailing_assets': [ 'mass_mailing/static/src/scss/mailing_portal.scss', 'mass_mailing/static/src/js/mailing_portal.js', ], + 'web_editor.assets_wysiwyg': [ + 'mass_mailing/static/src/js/mass_mailing_wysiwyg.js', + 'mass_mailing/static/src/scss/mass_mailing.wysiwyg.scss', + ], 'web.assets_backend': [ 'mass_mailing/static/src/scss/mailing_filter_widget.scss', 'mass_mailing/static/src/scss/mass_mailing.scss', @@ -112,18 +128,14 @@ 'mass_mailing/static/src/scss/mass_mailing.ui.scss', ], 'mass_mailing.assets_wysiwyg': [ - 'mass_mailing/static/src/js/mass_mailing_link_dialog_fix.js', 'mass_mailing/static/src/js/mass_mailing_snippets.js', 'mass_mailing/static/src/snippets/s_masonry_block/options.js', 'mass_mailing/static/src/snippets/s_media_list/options.js', 'mass_mailing/static/src/snippets/s_showcase/options.js', 'mass_mailing/static/src/snippets/s_rating/options.js' ], - 'web_editor.assets_wysiwyg': [ + 'web_editor.assets_legacy_wysiwyg': [ 'mass_mailing/static/src/js/snippets.editor.js', - 'mass_mailing/static/src/js/wysiwyg.js', - 'mass_mailing/static/src/xml/mass_mailing.editor.xml', - 'mass_mailing/static/src/scss/mass_mailing.wysiwyg.scss', ], 'web.assets_common': [ 'mass_mailing/static/src/js/tours/**/*', diff --git a/addons/mass_mailing/data/mailing_data_templates.xml b/addons/mass_mailing/data/mailing_data_templates.xml index 53baaf0c224..5c2e7d1ee35 100644 --- a/addons/mass_mailing/data/mailing_data_templates.xml +++ b/addons/mass_mailing/data/mailing_data_templates.xml @@ -14,7 +14,7 @@ - + - - - - - - diff --git a/addons/mass_mailing/wizard/mail_compose_message.py b/addons/mass_mailing/wizard/mail_compose_message.py index 70bc9d72c40..937d3482d79 100644 --- a/addons/mass_mailing/wizard/mail_compose_message.py +++ b/addons/mass_mailing/wizard/mail_compose_message.py @@ -1,7 +1,10 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from markupsafe import Markup + from odoo import fields, models +from odoo.tools.misc import file_open class MailComposeMessage(models.TransientModel): @@ -34,11 +37,13 @@ class MailComposeMessage(models.TransientModel): return mail_values_all trace_values_all = self._prepare_mail_values_mailing_traces(mail_values_all) + with file_open("mass_mailing/static/src/scss/mass_mailing_mail.scss", "r") as fd: + styles = fd.read() for res_id, mail_values in mail_values_all.items(): if mail_values.get('body_html'): body = self.env['ir.qweb']._render( 'mass_mailing.mass_mailing_mail_layout', - {'body': mail_values['body_html']}, + {'body': mail_values['body_html'], 'mailing_style': Markup(f'')}, minimal_qcontext=True, raise_if_not_found=False ) diff --git a/addons/mass_mailing/wizard/mailing_mailing_test.py b/addons/mass_mailing/wizard/mailing_mailing_test.py index 5df68d198bd..d450f27442c 100644 --- a/addons/mass_mailing/wizard/mailing_mailing_test.py +++ b/addons/mass_mailing/wizard/mailing_mailing_test.py @@ -4,6 +4,7 @@ from markupsafe import Markup from odoo import _, fields, models, tools +from odoo.tools.misc import file_open class TestMassMailing(models.TransientModel): @@ -49,13 +50,18 @@ class TestMassMailing(models.TransientModel): # Convert links in absolute URLs before the application of the shortener full_body = self.env['mail.render.mixin']._replace_local_links(full_body) + with file_open("mass_mailing/static/src/scss/mass_mailing_mail.scss", "r") as fd: + styles = fd.read() for valid_email in valid_emails: mail_values = { 'email_from': mailing.email_from, 'reply_to': mailing.reply_to, 'email_to': valid_email, 'subject': subject, - 'body_html': self.env['ir.qweb']._render('mass_mailing.mass_mailing_mail_layout', {'body': full_body}, minimal_qcontext=True), + 'body_html': self.env['ir.qweb']._render('mass_mailing.mass_mailing_mail_layout', { + 'body': full_body, + 'mailing_style': Markup(f''), + }, minimal_qcontext=True), 'is_notification': True, 'mailing_id': mailing.id, 'attachment_ids': [(4, attachment.id) for attachment in mailing.attachment_ids], diff --git a/addons/project/__manifest__.py b/addons/project/__manifest__.py index 9c8dd58b15e..9c044756702 100644 --- a/addons/project/__manifest__.py +++ b/addons/project/__manifest__.py @@ -187,7 +187,6 @@ 'web_editor/static/src/scss/web_editor.common.scss', 'web_editor/static/src/scss/web_editor.backend.scss', - 'web_editor/static/src/js/wysiwyg/dialog.js', 'web_editor/static/src/js/frontend/loader.js', 'web_editor/static/src/js/backend/**/*', 'web_editor/static/src/xml/backend.xml', diff --git a/addons/test_website/static/tests/field_html_file_upload.js b/addons/test_website/static/tests/field_html_file_upload.js index a0bb612cbad..395f67ef9c3 100644 --- a/addons/test_website/static/tests/field_html_file_upload.js +++ b/addons/test_website/static/tests/field_html_file_upload.js @@ -9,7 +9,7 @@ import { uploadService } from '@web_editor/components/upload_progress_toast/uplo import { unsplashService } from '@web_unsplash/services/unsplash_service'; import { createWebClient, doAction } from "@web/../tests/webclient/helpers"; import weTestUtils from 'web_editor.test_utils'; -import Wysiwyg from 'web_editor.wysiwyg'; +import { Wysiwyg } from '@web_editor/js/wysiwyg/wysiwyg'; const { useEffect } = owl; @@ -65,6 +65,12 @@ QUnit.module('field html file upload', { onChangeTriggered.resolve(true); } }); + patchWithCleanup(Wysiwyg.prototype, { + async _getColorpickerTemplate() { + return weTestUtils.COLOR_PICKER_TEMPLATE; + } + }); + // create and load form view const serviceRegistry = registry.category("services"); serviceRegistry.add("upload", uploadService); diff --git a/addons/web/__manifest__.py b/addons/web/__manifest__.py index 9e2d38335c5..65cb5f57358 100644 --- a/addons/web/__manifest__.py +++ b/addons/web/__manifest__.py @@ -38,7 +38,7 @@ This module provides the core of the Odoo Web Client. # Examples: # > web.assets_common = assets common to backend clients and others # (not frontend). - # > web_editor.assets_wysiwyg = assets needed by components defined in the "web_editor" module. + # > web_editor.assets_legacy_wysiwyg = assets needed by components defined in the "web_editor" module. # Warning: Layouts using "assets_frontend" assets do not have the # "assets_common" assets anymore. So, if it make sense, files added in @@ -159,8 +159,6 @@ This module provides the core of the Odoo Web Client. 'web/static/src/legacy/js/widgets/name_and_signature.js', 'web/static/src/legacy/xml/name_and_signature.xml', 'web/static/src/legacy/js/core/smooth_scroll_on_drag.js', - 'web/static/src/legacy/js/widgets/colorpicker.js', - 'web/static/src/legacy/xml/colorpicker.xml', 'web/static/src/legacy/translations_loaded.js', ], 'web.assets_emoji': [ @@ -244,6 +242,7 @@ This module provides the core of the Odoo Web Client. 'web/static/src/legacy/js/services/session.js', 'web/static/src/legacy/js/tools/tools.js', 'web/static/src/legacy/js/views/**/*', + 'web/static/src/legacy/js/widgets/colorpicker_dialog.js', 'web/static/src/legacy/js/widgets/date_picker.js', 'web/static/src/legacy/js/widgets/domain_selector_dialog.js', 'web/static/src/legacy/js/widgets/domain_selector.js', @@ -392,8 +391,6 @@ This module provides the core of the Odoo Web Client. 'web/static/src/legacy/js/widgets/name_and_signature.js', 'web/static/src/legacy/xml/name_and_signature.xml', 'web/static/src/legacy/js/core/smooth_scroll_on_drag.js', - 'web/static/src/legacy/js/widgets/colorpicker.js', - 'web/static/src/legacy/xml/colorpicker.xml', 'web/static/src/env.js', 'web/static/src/core/utils/transitions.scss', # included early because used by other files diff --git a/addons/web/static/src/core/colorpicker/colorpicker.js b/addons/web/static/src/core/colorpicker/colorpicker.js index 501d6acae97..d0c71b63a29 100644 --- a/addons/web/static/src/core/colorpicker/colorpicker.js +++ b/addons/web/static/src/core/colorpicker/colorpicker.js @@ -1,58 +1,128 @@ /** @odoo-module **/ -import core from "web.core"; -import utils from "web.utils"; -import Widget from "web.Widget"; +import { + convertCSSColorToRgba, + convertRgbaToCSSColor, + convertRgbToHsl, + convertHslToRgb, +} from '@web/core/utils/colors'; + +import { + Component, + useRef, + onWillStart, + onMounted, + onWillUpdateProps, + onWillDestroy, +} from "@odoo/owl"; import { uniqueId } from "@web/core/utils/functions"; import { debounce, throttleForAnimation } from "@web/core/utils/timing"; -var _t = core._t; -var ColorpickerWidget = Widget.extend({ - template: 'Colorpicker', - events: { - 'click': '_onClick', - 'keypress': '_onKeypress', - 'mousedown .o_color_pick_area': '_onMouseDownPicker', - 'mousedown .o_color_slider': '_onMouseDownSlider', - 'mousedown .o_opacity_slider': '_onMouseDownOpacitySlider', - 'change .o_color_picker_inputs': '_onChangeInputs', - }, +/** + * Confines a value inside an interval + * + * @param {number} [val] the value to confine + * @param {number} [min] the minimum of the interval + * @param {number} [max] the maximum of the interval + * @return {number} val if val is in [min, max], min if val < min and max + * otherwise + */ +function confine(val, min, max) { + return Math.max(min, Math.min(max, val)); +} - /** - * @constructor - * @param {Widget} parent - * @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); - options = options || {}; +export class Colorpicker extends Component { + static template = 'web.Colorpicker'; + static props = { + document: { type: true, optional: true }, + defaultColor: { type: String, optional: true }, + selectedColor: { type: String, optional: true }, + noTransparency: { type: Boolean, optional: true }, + colorPreview: { type: Boolean, optional: true }, + stopClickPropagation: { type: Boolean, optional: true }, + onColorSelect: { type: Function, optional: true }, + onColorPreview: { type: Function, optional: true }, + onInputEnter: { type: Function, optional: true }, + }; + static defaultProps = { + document: window.document, + defaultColor: '#FF0000', + noTransparency: false, + colorPreview: false, + stopClickPropagation: false, + onColorSelect: () => {}, + onColorPreview: () => {}, + onInputEnter: () => {}, + }; + elRef = useRef('el'); + + setup() { + onWillStart(() => { + this.init(); + }); + onMounted(async () => { + if (!this.elRef.el) { + // There is legacy code that can trigger the instantiation of the + // link tool when one of it's parent component is not in the dom. If + // that parent element is not in the dom, owl will not return + // `this.linkComponentWrapperRef.el` because of a check (see + // `inOwnerDocument`). + // Todo: this workaround should be removed when the snippet menu is + // converted to owl. + await new Promise(resolve => { + const observer = new MutationObserver(() => { + if (this.elRef.el) { + observer.disconnect(); + resolve(); + } + }); + observer.observe(document.body, { childList: true, subtree: true }); + }); + } + this.el = this.elRef.el; + this.$el = $(this.el); + + this.$el.on('click', this._onClick.bind(this)); + this.$el.on('keypress', this._onKeypress.bind(this)); + this.$el.on('mousedown', '.o_color_pick_area', this._onMouseDownPicker.bind(this)); + this.$el.on('mousedown', '.o_color_slider', this._onMouseDownSlider.bind(this)); + this.$el.on('mousedown', '.o_opacity_slider', this._onMouseDownOpacitySlider.bind(this)); + this.$el.on('change', '.o_color_picker_inputs', this._onChangeInputs.bind(this)); + + this.start(); + }); + onWillUpdateProps((newProps) => { + if (newProps.selectedColor) { + this.setSelectedColor(newProps.selectedColor); + } + }); + onWillDestroy(() => { + this.destroy(); + }); + } + + init() { this.pickerFlag = false; this.sliderFlag = false; this.opacitySliderFlag = false; this.colorComponents = {}; this.uniqueId = uniqueId("colorpicker"); this.selectedHexValue = ''; - - this.options = Object.assign({}, options); - }, + } /** * @override */ - start: function () { - this.$colorpickerArea = this.$('.o_color_pick_area'); - this.$colorpickerPointer = this.$('.o_picker_pointer'); - this.$colorSlider = this.$('.o_color_slider'); - this.$colorSliderPointer = this.$('.o_slider_pointer'); - this.$opacitySlider = this.$('.o_opacity_slider'); - this.$opacitySliderPointer = this.$('.o_opacity_pointer'); + start() { + this.$colorpickerArea = this.$el.find('.o_color_pick_area'); + this.$colorpickerPointer = this.$el.find('.o_picker_pointer'); + this.$colorSlider = this.$el.find('.o_color_slider'); + this.$colorSliderPointer = this.$el.find('.o_slider_pointer'); + this.$opacitySlider = this.$el.find('.o_opacity_slider'); + this.$opacitySliderPointer = this.$el.find('.o_opacity_pointer'); - var defaultColor = this.options.defaultColor || '#FF0000'; - var rgba = ColorpickerWidget.convertCSSColorToRgba(defaultColor); + const rgba = convertCSSColorToRgba(this.props.defaultColor); if (rgba) { this._updateRgba(rgba.red, rgba.green, rgba.blue, rgba.opacity); } @@ -105,25 +175,23 @@ var ColorpickerWidget = Widget.extend({ }, 10)); this.previewActive = true; - return this._super.apply(this, arguments); - }, + } /** * @override */ - destroy: function () { - this._super.apply(this, arguments); + destroy() { if (this.throttleOnMouseMove) { this.$documents.off(`.${this.uniqueId}`); this.throttleOnMouseMove.cancel(); } - }, + } /** * Sets the currently selected color * * @param {string} color rgb[a] */ - setSelectedColor: function (color) { - var rgba = ColorpickerWidget.convertCSSColorToRgba(color); + setSelectedColor(color) { + const rgba = convertCSSColorToRgba(color); if (rgba) { const oldPreviewActive = this.previewActive; this.previewActive = false; @@ -131,7 +199,7 @@ var ColorpickerWidget = Widget.extend({ this.previewActive = oldPreviewActive; this._updateUI(); } - }, + } //-------------------------------------------------------------------------- // Private @@ -142,62 +210,60 @@ var ColorpickerWidget = Widget.extend({ * * @private */ - _updateUI: function () { - var self = this; - + _updateUI() { // Update inputs for (const [color, value] of Object.entries(this.colorComponents)) { - self.$(`.o_${color}_input`).val(value); + this.$el.find(`.o_${color}_input`).val(value); } // Update preview - this.$('.o_color_preview').css('background-color', this.colorComponents.cssColor); + this.$el.find('.o_color_preview').css('background-color', this.colorComponents.cssColor); // Update picker area and picker pointer position this.$colorpickerArea.css( "background-color", `hsl(${this.colorComponents.hue}, 100%, 50%)` ); - var top = (100 - this.colorComponents.lightness) * this.$colorpickerArea.height() / 100; - var left = this.colorComponents.saturation * this.$colorpickerArea.width() / 100; + const top = (100 - this.colorComponents.lightness) * this.$colorpickerArea.height() / 100; + const left = this.colorComponents.saturation * this.$colorpickerArea.width() / 100; this.$colorpickerPointer.css({ top: (top - 5) + 'px', left: (left - 5) + 'px', }); // Update color slider position - var height = this.$colorSlider.height(); - var y = this.colorComponents.hue * height / 360; + const height = this.$colorSlider.height(); + const y = this.colorComponents.hue * height / 360; this.$colorSliderPointer.css('top', Math.round(y - 2)); - if (! this.options.noTransparency) { + if (!this.props.noTransparency) { // Update opacity slider position - var heightOpacity = this.$opacitySlider.height(); - var z = heightOpacity * (1 - this.colorComponents.opacity / 100.0); + const heightOpacity = this.$opacitySlider.height(); + const z = heightOpacity * (1 - this.colorComponents.opacity / 100.0); this.$opacitySliderPointer.css('top', Math.round(z - 2)); // Add gradient color on opacity slider this.$opacitySlider.css('background', 'linear-gradient(' + this.colorComponents.hex + ' 0%, transparent 100%)'); } - }, + } /** * Updates colors according to given hex value. Opacity is left unchanged. * * @private * @param {string} hex - hexadecimal code */ - _updateHex: function (hex) { - var rgb = ColorpickerWidget.convertCSSColorToRgba(hex); + _updateHex(hex) { + const rgb = convertCSSColorToRgba(hex); if (!rgb) { return; } Object.assign(this.colorComponents, {hex: hex}, rgb, - ColorpickerWidget.convertRgbToHsl(rgb.red, rgb.green, rgb.blue) + convertRgbToHsl(rgb.red, rgb.green, rgb.blue) ); this._updateCssColor(); - }, + } /** * Updates colors according to given RGB values. * @@ -207,7 +273,7 @@ var ColorpickerWidget = Widget.extend({ * @param {integer} b * @param {integer} [a] */ - _updateRgba: function (r, g, b, a) { + _updateRgba(r, g, b, a) { // Remove full transparency in case some lightness is added const opacity = a || this.colorComponents.opacity; if (opacity < 0.1 && (r > 0.1 || g > 0.1 || b > 0.1)) { @@ -217,7 +283,7 @@ var ColorpickerWidget = Widget.extend({ // We update the hexadecimal code by transforming into a css color and // ignoring the opacity (we don't display opacity component in hexa as // not supported on all browsers) - var hex = ColorpickerWidget.convertRgbaToCSSColor(r, g, b); + const hex = convertRgbaToCSSColor(r, g, b); if (!hex) { return; } @@ -225,10 +291,10 @@ var ColorpickerWidget = Widget.extend({ {red: r, green: g, blue: b}, a === undefined ? {} : {opacity: a}, {hex: hex}, - ColorpickerWidget.convertRgbToHsl(r, g, b) + convertRgbToHsl(r, g, b) ); this._updateCssColor(); - }, + } /** * Updates colors according to given HSL values. * @@ -237,19 +303,19 @@ var ColorpickerWidget = Widget.extend({ * @param {integer} s * @param {integer} l */ - _updateHsl: function (h, s, l) { + _updateHsl(h, s, l) { // Remove full transparency in case some lightness is added let a = this.colorComponents.opacity; if (a < 0.1 && l > 0.1) { a = 100; } - var rgb = ColorpickerWidget.convertHslToRgb(h, s, l); + const rgb = convertHslToRgb(h, s, l); if (!rgb) { return; } // We receive an hexa as we ignore the opacity - const hex = ColorpickerWidget.convertRgbaToCSSColor(rgb.red, rgb.green, rgb.blue); + const hex = convertRgbaToCSSColor(rgb.red, rgb.green, rgb.blue); Object.assign(this.colorComponents, {hue: h, saturation: s, lightness: l}, rgb, @@ -257,14 +323,14 @@ var ColorpickerWidget = Widget.extend({ {opacity: a}, ); this._updateCssColor(); - }, + } /** * Updates color opacity. * * @private * @param {integer} a */ - _updateOpacity: function (a) { + _updateOpacity(a) { if (a < 0 || a > 100) { return; } @@ -272,32 +338,32 @@ var ColorpickerWidget = Widget.extend({ {opacity: a} ); this._updateCssColor(); - }, + } /** * Trigger an event to annonce that the widget value has changed * * @private */ - _colorSelected: function () { - this.trigger_up('colorpicker_select', this.colorComponents); - }, + _colorSelected() { + this.props.onColorSelect(this.colorComponents); + } /** * Updates css color representation. * * @private */ - _updateCssColor: function () { + _updateCssColor() { const r = this.colorComponents.red; const g = this.colorComponents.green; const b = this.colorComponents.blue; const a = this.colorComponents.opacity; Object.assign(this.colorComponents, - {cssColor: ColorpickerWidget.convertRgbaToCSSColor(r, g, b, a)} + {cssColor: convertRgbaToCSSColor(r, g, b, a)} ); if (this.previewActive) { - this.trigger_up('colorpicker_preview', this.colorComponents); + this.props.onColorPreview(this.colorComponents); } - }, + } //-------------------------------------------------------------------------- // Handlers @@ -307,20 +373,20 @@ var ColorpickerWidget = Widget.extend({ * @private * @param {Event} ev */ - _onKeypress: function (ev) { + _onKeypress(ev) { if (ev.charCode === $.ui.keyCode.ENTER) { if (ev.target.tagName === 'INPUT') { this._onChangeInputs(ev); } ev.preventDefault(); - this.trigger_up('enter_key_color_colorpicker'); + this.props.onInputEnter(ev); } - }, + } /** * @param {Event} ev */ - _onClick: function (ev) { - if (this.options.stopClickPropagation) { + _onClick(ev) { + if (this.props.stopClickPropagation) { ev.stopPropagation(); } ev.originalEvent.__isColorpickerClick = true; @@ -331,98 +397,98 @@ var ColorpickerWidget = Widget.extend({ return; } this.selectedHexValue = ''; - }, + } /** * Updates color when the user starts clicking on the picker. * * @private * @param {Event} ev */ - _onMouseDownPicker: function (ev) { + _onMouseDownPicker(ev) { this.pickerFlag = true; ev.preventDefault(); this._onMouseMovePicker(ev); - }, + } /** * Updates saturation and lightness values on mouse drag over picker. * * @private * @param {Event} ev */ - _onMouseMovePicker: function (ev) { + _onMouseMovePicker(ev) { if (!this.pickerFlag) { return; } - var offset = this.$colorpickerArea.offset(); - var top = ev.pageY - offset.top; - var left = ev.pageX - offset.left; - var saturation = Math.round(100 * left / this.$colorpickerArea.width()); - var lightness = Math.round(100 * (this.$colorpickerArea.height() - top) / this.$colorpickerArea.height()); - saturation = utils.confine(saturation, 0, 100); - lightness = utils.confine(lightness, 0, 100); + const offset = this.$colorpickerArea.offset(); + const top = ev.pageY - offset.top; + const left = ev.pageX - offset.left; + let saturation = Math.round(100 * left / this.$colorpickerArea.width()); + let lightness = Math.round(100 * (this.$colorpickerArea.height() - top) / this.$colorpickerArea.height()); + saturation = confine(saturation, 0, 100); + lightness = confine(lightness, 0, 100); this._updateHsl(this.colorComponents.hue, saturation, lightness); this._updateUI(); - }, + } /** * Updates color when user starts clicking on slider. * * @private * @param {Event} ev */ - _onMouseDownSlider: function (ev) { + _onMouseDownSlider(ev) { this.sliderFlag = true; ev.preventDefault(); this._onMouseMoveSlider(ev); - }, + } /** * Updates hue value on mouse drag over slider. * * @private * @param {Event} ev */ - _onMouseMoveSlider: function (ev) { + _onMouseMoveSlider(ev) { if (!this.sliderFlag) { return; } - var y = ev.pageY - this.$colorSlider.offset().top; - var hue = Math.round(360 * y / this.$colorSlider.height()); - hue = utils.confine(hue, 0, 360); + const y = ev.pageY - this.$colorSlider.offset().top; + let hue = Math.round(360 * y / this.$colorSlider.height()); + hue = confine(hue, 0, 360); this._updateHsl(hue, this.colorComponents.saturation, this.colorComponents.lightness); this._updateUI(); - }, + } /** * Updates opacity when user starts clicking on opacity slider. * * @private * @param {Event} ev */ - _onMouseDownOpacitySlider: function (ev) { + _onMouseDownOpacitySlider(ev) { this.opacitySliderFlag = true; ev.preventDefault(); this._onMouseMoveOpacitySlider(ev); - }, + } /** * Updates opacity value on mouse drag over opacity slider. * * @private * @param {Event} ev */ - _onMouseMoveOpacitySlider: function (ev) { - if (!this.opacitySliderFlag || this.options.noTransparency) { + _onMouseMoveOpacitySlider(ev) { + if (!this.opacitySliderFlag || this.props.noTransparency) { return; } - var y = ev.pageY - this.$opacitySlider.offset().top; - var opacity = Math.round(100 * (1 - y / this.$opacitySlider.height())); - opacity = utils.confine(opacity, 0, 100); + const y = ev.pageY - this.$opacitySlider.offset().top; + let opacity = Math.round(100 * (1 - y / this.$opacitySlider.height())); + opacity = confine(opacity, 0, 100); this._updateOpacity(opacity); this._updateUI(); - }, + } /** * Called when input value is changed -> Updates UI: Set picker and slider * position and set colors. @@ -430,30 +496,30 @@ var ColorpickerWidget = Widget.extend({ * @private * @param {Event} ev */ - _onChangeInputs: function (ev) { + _onChangeInputs(ev) { switch ($(ev.target).data('colorMethod')) { case 'hex': - this._updateHex(this.$('.o_hex_input').val()); + this._updateHex(this.$el.find('.o_hex_input').val()); break; case 'rgb': this._updateRgba( - parseInt(this.$('.o_red_input').val()), - parseInt(this.$('.o_green_input').val()), - parseInt(this.$('.o_blue_input').val()) + parseInt(this.$el.find('.o_red_input').val()), + parseInt(this.$el.find('.o_green_input').val()), + parseInt(this.$el.find('.o_blue_input').val()) ); break; case 'hsl': this._updateHsl( - parseInt(this.$('.o_hue_input').val()), - parseInt(this.$('.o_saturation_input').val()), - parseInt(this.$('.o_lightness_input').val()) + parseInt(this.$el.find('.o_hue_input').val()), + parseInt(this.$el.find('.o_saturation_input').val()), + parseInt(this.$el.find('.o_lightness_input').val()) ); break; case 'opacity': - this._updateOpacity(parseInt(this.$('.o_opacity_input').val())); + this._updateOpacity(parseInt(this.$el.find('.o_opacity_input').val())); break; } this._updateUI(); this._colorSelected(); - }, -}); + } +} diff --git a/addons/web/static/src/core/colorpicker/colorpicker.xml b/addons/web/static/src/core/colorpicker/colorpicker.xml index 9b819160238..6f8dec7349c 100644 --- a/addons/web/static/src/core/colorpicker/colorpicker.xml +++ b/addons/web/static/src/core/colorpicker/colorpicker.xml @@ -1,20 +1,20 @@ - -
+ +
-
+
-
+
-
+
@@ -26,7 +26,7 @@ - +
+ diff --git a/addons/web/static/src/legacy/js/widgets/colorpicker_dialog.js b/addons/web/static/src/legacy/js/widgets/colorpicker_dialog.js index e6bc6232ca8..ddffd55debc 100644 --- a/addons/web/static/src/legacy/js/widgets/colorpicker_dialog.js +++ b/addons/web/static/src/legacy/js/widgets/colorpicker_dialog.js @@ -1,11 +1,12 @@ /** @odoo-module **/ -import core from "web.core"; +import { _t } from "@web/core/l10n/translation"; +import { ComponentWrapper } from "web.OwlCompatibility"; + import Dialog from "web.Dialog"; +import { Colorpicker } from '@web/core/colorpicker/colorpicker'; -var _t = core._t; - -const ColorpickerDialog = Dialog.extend({ +export const ColorpickerDialog = Dialog.extend({ /** * @override */ @@ -25,10 +26,11 @@ const ColorpickerDialog = Dialog.extend({ */ start: function () { const proms = [this._super(...arguments)]; - this.colorPicker = new ColorpickerWidget(this, Object.assign({ + this.colorpicker = new ComponentWrapper(this, Colorpicker, { colorPreview: true, - }, this.options)); - proms.push(this.colorPicker.appendTo(this.$el)); + }); + this._colorpickerComponent = this.colorpicker.node.component; + proms.push(this.colorpicker.mount(this.$el[0])); return Promise.all(proms); }, @@ -40,10 +42,6 @@ const ColorpickerDialog = Dialog.extend({ * @private */ _onFinalPick: function () { - this.trigger_up('colorpicker:saved', this.colorPicker.colorComponents); + this.trigger_up('colorpicker:saved', this._colorpickerComponent.colorComponents); }, }); - -export default { - ColorpickerDialog: ColorpickerDialog, -}; diff --git a/addons/web_editor/__manifest__.py b/addons/web_editor/__manifest__.py index 885bcadf425..0b497d6f32f 100644 --- a/addons/web_editor/__manifest__.py +++ b/addons/web_editor/__manifest__.py @@ -22,6 +22,23 @@ Odoo Web Editor widget. # MAIN BUNDLES #---------------------------------------------------------------------- + 'web_editor.assets_legacy_wysiwyg': [ + 'web_editor/static/src/js/editor/snippets.editor.js', + 'web_editor/static/src/js/editor/snippets.options.js', + ], + 'web_editor.wysiwyg_iframe_editor_assets': [ + ('include', 'web.assets_common'), + ('include', 'web_editor.assets_wysiwyg'), + ('include', 'web_editor.assets_legacy_wysiwyg'), + ], + 'web_editor.assets_media_dialog': [ + 'web_editor/static/src/components/**/*', + ], + 'web_editor.assets_tests_styles': [ + ('include', 'web._assets_helpers'), + 'web_editor/static/src/js/editor/odoo-editor/src/base_style.scss', + 'web_editor/static/src/js/editor/odoo-editor/src/checklist.scss', + ], 'web_editor.assets_wysiwyg': [ # lib 'web_editor/static/lib/cropperjs/cropper.css', @@ -53,6 +70,8 @@ Odoo Web Editor widget. # utils 'web_editor/static/src/js/wysiwyg/linkDialogCommand.js', 'web_editor/static/src/js/wysiwyg/PeerToPeer.js', + 'web_editor/static/src/js/wysiwyg/conflict_dialog.js', + 'web_editor/static/src/js/wysiwyg/conflict_dialog.xml', # odoo utils ('include', 'web._assets_helpers'), @@ -73,13 +92,11 @@ Odoo Web Editor widget. # widgets & plugins 'web_editor/static/src/js/wysiwyg/widgets/**/*', - 'web_editor/static/src/js/editor/snippets.editor.js', 'web_editor/static/src/js/editor/toolbar.js', - 'web_editor/static/src/js/editor/snippets.options.js', # Launcher + 'web_editor/static/src/js/wysiwyg/wysiwyg_jquery_extention.js', 'web_editor/static/src/js/wysiwyg/wysiwyg.js', - 'web_editor/static/src/js/wysiwyg/widget.js', 'web_editor/static/src/js/wysiwyg/wysiwyg_iframe.js', 'web_editor/static/src/xml/editor.xml', @@ -89,14 +106,6 @@ Odoo Web Editor widget. 'web_editor/static/src/xml/wysiwyg.xml', 'web_editor/static/src/xml/wysiwyg_colorpicker.xml', ], - 'web_editor.assets_media_dialog': [ - 'web_editor/static/src/components/**/*', - ], - 'web_editor.assets_tests_styles': [ - ('include', 'web._assets_helpers'), - 'web_editor/static/src/js/editor/odoo-editor/src/base_style.scss', - 'web_editor/static/src/js/editor/odoo-editor/src/checklist.scss', - ], 'web.assets_common': [ 'web_editor/static/src/js/editor/odoo-editor/src/base_style.scss', 'web_editor/static/lib/vkbeautify/**/*', @@ -116,10 +125,11 @@ Odoo Web Editor widget. 'web_editor/static/src/scss/web_editor.common.scss', 'web_editor/static/src/scss/web_editor.backend.scss', - 'web_editor/static/src/js/wysiwyg/dialog.js', 'web_editor/static/src/js/frontend/loader.js', 'web_editor/static/src/js/backend/**/*', 'web_editor/static/src/xml/backend.xml', + + ('include', 'web_editor.assets_wysiwyg'), ], "web.dark_mode_assets_backend": [ 'web_editor/static/src/scss/odoo-editor/powerbox.dark.scss', @@ -144,8 +154,8 @@ Odoo Web Editor widget. 'web_editor/static/src/scss/web_editor.common.scss', 'web_editor/static/src/scss/web_editor.frontend.scss', - 'web_editor/static/src/js/wysiwyg/dialog.js', 'web_editor/static/src/js/frontend/loader.js', + 'web_editor/static/src/js/frontend/loadWysiwygFromTextarea.js', ], #---------------------------------------------------------------------- @@ -172,7 +182,7 @@ Odoo Web Editor widget. # ---------------------------------------------------------------------- 'web.qunit_suite_tests': [ - ('include', 'web_editor.assets_wysiwyg'), + ('include', 'web_editor.assets_legacy_wysiwyg'), 'web_editor/static/tests/**/*', 'web_editor/static/src/js/editor/odoo-editor/test/utils.js' diff --git a/addons/web_editor/static/src/components/emoji_popover/emoji_popover.js b/addons/web_editor/static/src/components/emoji_popover/emoji_popover.js deleted file mode 100644 index 76555dc01c6..00000000000 --- a/addons/web_editor/static/src/components/emoji_popover/emoji_popover.js +++ /dev/null @@ -1,19 +0,0 @@ -/** @odoo-module */ - -import { useWowlService } from '@web/legacy/utils'; -import { Component, onRendered, xml } from "@odoo/owl"; -import { EmojiPicker } from '@web/core/emoji_picker/emoji_picker'; - -export class EmojiPickerWrapper extends Component { - setup() { - this.popover = useWowlService('popover'); - - onRendered(() => { - this.popover.add(this.props.targetEl, EmojiPicker, { - onSelect: this.props.onSelect, - }, { position: `bottom-${this.props.position}`}); - }); - } -} - -EmojiPickerWrapper.template = xml``; diff --git a/addons/web_editor/static/src/components/media_dialog/media_dialog.js b/addons/web_editor/static/src/components/media_dialog/media_dialog.js index 9eb8b50faa3..9ca7649ad78 100644 --- a/addons/web_editor/static/src/components/media_dialog/media_dialog.js +++ b/addons/web_editor/static/src/components/media_dialog/media_dialog.js @@ -1,7 +1,6 @@ /** @odoo-module **/ import { useService } from '@web/core/utils/hooks'; -import { useWowlService } from '@web/legacy/utils'; import { Dialog } from '@web/core/dialog/dialog'; import { Notebook } from '@web/core/notebook/notebook'; import { ImageSelector } from './image_selector'; @@ -9,7 +8,7 @@ import { DocumentSelector } from './document_selector'; import { IconSelector } from './icon_selector'; import { VideoSelector } from './video_selector'; -import { Component, useState, onRendered, xml } from "@odoo/owl"; +import { Component, useState } from "@odoo/owl"; export const TABS = { IMAGES: { diff --git a/addons/web_editor/static/src/components/media_dialog/video_selector.js b/addons/web_editor/static/src/components/media_dialog/video_selector.js index e6088583b79..89e96dfb594 100644 --- a/addons/web_editor/static/src/components/media_dialog/video_selector.js +++ b/addons/web_editor/static/src/components/media_dialog/video_selector.js @@ -2,7 +2,6 @@ import { useService } from '@web/core/utils/hooks'; import { debounce } from '@web/core/utils/timing'; -import { qweb } from 'web.core'; import { Component, useState, useRef, onMounted, onWillStart } from "@odoo/owl"; @@ -195,9 +194,15 @@ export class VideoSelector extends Component { */ static createElements(selectedMedia) { return selectedMedia.map(video => { - const template = document.createElement('template'); - template.innerHTML = qweb.render('web_editor.videoWrapper', { src: video.src }); - return template.content.firstChild; + const div = document.createElement('div'); + div.dataset.oeExpression = video.src; + div.innerHTML = ` +
+
+ + `; + div.querySelector('iframe').src = video.src; + return div; }); } } diff --git a/addons/web_editor/static/src/components/media_dialog/video_selector.xml b/addons/web_editor/static/src/components/media_dialog/video_selector.xml index ece78946bff..b0a2424a4a5 100644 --- a/addons/web_editor/static/src/components/media_dialog/video_selector.xml +++ b/addons/web_editor/static/src/components/media_dialog/video_selector.xml @@ -1,10 +1,5 @@ -
-
-
-