From fdac73e99d6edce1544c25534f8903c637a5507e Mon Sep 17 00:00:00 2001 From: qsm-odoo Date: Tue, 19 Nov 2019 15:23:44 +0000 Subject: [PATCH] [IMP] web_editor, *: add new user value colorpicker widget * web, mass_mailing, website Introduced a new sub-widget for snippet options: we-colorpicker. This allows to retrieve a color value (either a normalized css color or a theme color name) to be used for a snippet option method. This commit replaces the old 'colorpicker' snippet option whose behavior is now simply achieved by defining an xml option using a we-colorpicker widget in combination with the 'selectStyle' standard method. This commit also adds the possibility to control the border-color of elements alongside their border-width. Again, just a we-colorpicker widget in combination with the 'selectStyle' standard method. Note: this commit reviewed what the colorpalette palette widget notifies. This is not a cssColor and a 'isClass' flag to say if it is a css class or not. The colorpalette always returns a color string which can be the name of a color like 'alpha'. This is up to the caller to check if the color string is a css color or not with isCSSColor. In the case it is not, the related code can read the actual color by reading the related css variable. Part of https://github.com/odoo/odoo/pull/40282 In preparation of task-2122935 and other tasks --- addons/mass_mailing/views/snippets_themes.xml | 10 +- .../src/js/widgets/colorpicker_dialog.js | 15 +- .../web_editor/static/src/js/common/utils.js | 1 + .../static/src/js/editor/rte.summernote.js | 11 +- .../static/src/js/editor/snippets.options.js | 355 +++++++++++------- .../src/js/wysiwyg/widgets/color_palette.js | 40 +- .../static/src/scss/web_editor.common.scss | 6 + .../static/src/scss/wysiwyg_snippets.scss | 71 ++-- addons/web_editor/static/tests/test_utils.js | 8 +- .../static/src/js/editor/snippets.options.js | 2 +- addons/website/static/src/js/widgets/theme.js | 8 +- addons/website/views/snippets.xml | 91 +++-- 12 files changed, 376 insertions(+), 242 deletions(-) diff --git a/addons/mass_mailing/views/snippets_themes.xml b/addons/mass_mailing/views/snippets_themes.xml index 1e11893256c..3e21a24baf6 100644 --- a/addons/mass_mailing/views/snippets_themes.xml +++ b/addons/mass_mailing/views/snippets_themes.xml @@ -807,8 +807,12 @@ data-selector=".note-editable > div:not(.o_layout), .note-editable .oe_structure > div, td, th" data-exclude=".o_mail_no_resize, .o_mail_no_options"/> -
+
+ +
diff --git a/addons/web/static/src/js/widgets/colorpicker_dialog.js b/addons/web/static/src/js/widgets/colorpicker_dialog.js index 4f25710db68..6982f9c6349 100644 --- a/addons/web/static/src/js/widgets/colorpicker_dialog.js +++ b/addons/web/static/src/js/widgets/colorpicker_dialog.js @@ -15,7 +15,7 @@ var ColorpickerDialog = Dialog.extend({ 'mousedown .o_color_pick_area': '_onMouseDownPicker', 'mousedown .o_color_slider': '_onMouseDownSlider', 'mousedown .o_opacity_slider': '_onMouseDownOpacitySlider', - 'change .o_color_picker_inputs' : '_onChangeInputs', + 'change .o_color_picker_inputs': '_onChangeInputs', }), /** @@ -560,6 +560,9 @@ ColorpickerDialog.convertCSSColorToRgba = function (cssColor) { }; } + // TODO maybe implement a support for receiving css color like 'red' or + // 'transparent' (which are now considered non-css color by isCSSColor...) + return false; }; /** @@ -580,6 +583,16 @@ ColorpickerDialog.normalizeCSSColor = function (cssColor) { } return ColorpickerDialog.convertRgbaToCSSColor(rgba.red, rgba.green, rgba.blue, rgba.opacity); }; +/** + * Checks if a given string is a css color. + * + * @static + * @param {string} cssColor + * @returns {boolean} + */ +ColorpickerDialog.isCSSColor = function (cssColor) { + return ColorpickerDialog.convertCSSColorToRgba(cssColor) !== false; +}; return ColorpickerDialog; }); diff --git a/addons/web_editor/static/src/js/common/utils.js b/addons/web_editor/static/src/js/common/utils.js index 67267d670ee..c05e7a2064e 100644 --- a/addons/web_editor/static/src/js/common/utils.js +++ b/addons/web_editor/static/src/js/common/utils.js @@ -14,6 +14,7 @@ const ColorpickerDialog = require('web.ColorpickerDialog'); const CSS_SHORTHANDS = { 'border-width': ['border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width'], 'border-radius': ['border-top-left-radius', 'border-top-right-radius', 'border-bottom-right-radius', 'border-bottom-left-radius'], + 'border-color': ['border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color'], }; /** * Key-value mapping to list converters from an unit A to an unit B. diff --git a/addons/web_editor/static/src/js/editor/rte.summernote.js b/addons/web_editor/static/src/js/editor/rte.summernote.js index 233a38edb8c..befea94f328 100644 --- a/addons/web_editor/static/src/js/editor/rte.summernote.js +++ b/addons/web_editor/static/src/js/editor/rte.summernote.js @@ -4,6 +4,7 @@ odoo.define('web_editor.rte.summernote', function (require) { var Class = require('web.Class'); const concurrency = require('web.concurrency'); var core = require('web.core'); +const ColorpickerDialog = require('web.ColorpickerDialog'); var ColorPaletteWidget = require('web_editor.ColorPalette').ColorPaletteWidget; var mixins = require('web.mixins'); var fonts = require('wysiwyg.fonts'); @@ -50,13 +51,17 @@ renderer.createPalette = function ($container, options) { const targetNode = r.sc; const targetElement = targetNode.nodeType === Node.ELEMENT_NODE ? targetNode : targetNode.parentNode; colorpicker = new ColorPaletteWidget(parent, { - colorPrefix: eventName === "foreColor" ? 'text-' : 'bg-', excluded: ['transparent_grayscale'], $editable: rte.Class.prototype.editable(), // Our parent is the root widget, we can't retrieve the editable section from it... selectedColor: $(targetElement).css(eventName === "foreColor" ? 'color' : 'backgroundColor'), - targetClasses: [...targetElement.classList], }); - colorpicker.on('color_picked', null, ev => applyColor(ev.data.target, eventName, ev.data.cssColor)); + colorpicker.on('color_picked', null, ev => { + let color = ev.data.color; + if (!ColorpickerDialog.isCSSColor(color)) { + color = (eventName === "foreColor" ? 'text-' : 'bg-') + color; + } + applyColor(ev.data.target, eventName, color); + }); colorpicker.on('color_reset', null, ev => applyColor(ev.data.target, eventName, 'inherit')); return colorpicker.replace(hookEl).then(() => { if (oldColorpicker) { 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 d4fd5c11e1b..6da88c2247a 100644 --- a/addons/web_editor/static/src/js/editor/snippets.options.js +++ b/addons/web_editor/static/src/js/editor/snippets.options.js @@ -567,6 +567,7 @@ const SelectUserValueWidget = UserValueWidget.extend({ return; } } + this._super(...arguments); }, //-------------------------------------------------------------------------- @@ -827,16 +828,169 @@ const MultiUserValueWidget = UserValueWidget.extend({ }, }); +const ColorpickerUserValueWidget = SelectUserValueWidget.extend({ // FIXME should be reloaded on focus + custom_events: { + 'color_picked': '_onColorPicked', + 'color_hover': '_onColorHovered', + 'color_leave': '_onColorLeft', + 'color_reset': '_onColorReset', + }, + + /** + * @override + */ + init: function () { + this._super(...arguments); + if (!this.title) { + this.title = ' '; + } + }, + /** + * @override + */ + start: async function () { + const _super = this._super.bind(this); + const args = arguments; + + this.el.classList.add('o_we_so_color_palette'); + + // Pre-instanciate the color palette widget + await this._renderColorPalette(); + + // Build the select element with a custom span to hold the color preview + this.colorPreviewEl = document.createElement('span'); + this.options.childNodes = [this.colorPalette.el]; + this.options.valueEl = this.colorPreviewEl; + + return _super(...args); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + getMethodsParams: function () { + return _.extend(this._super(...arguments), { + colorNames: this.colorPalette.getColorNames(), + }); + }, + /** + * @override + */ + getValue: function (methodName) { + return this._previewColor || this._super(...arguments); + }, + /** + * @override + */ + isContainer: function () { + return false; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + * @returns {Promise} + */ + _renderColorPalette: function () { + const options = {}; + options.selectedColor = this._value; + if (this.options.dataAttributes.excluded) { + options.excluded = this.options.dataAttributes.excluded.replace(/ /g, '').split(','); + } + const oldColorPalette = this.colorPalette; + this.colorPalette = new ColorPaletteWidget(this, options); + if (oldColorPalette) { + return this.colorPalette.insertAfter(oldColorPalette.el).then(() => { + oldColorPalette.destroy(); + }); + } + return this.colorPalette.appendTo(document.createDocumentFragment()); + }, + /** + * Updates the color preview + re-render the whole color palette widget. + * + * @override + */ + _updateUI: async function (color) { + await this._super(...arguments); + + this.colorPreviewEl.classList.remove(...this.colorPalette.getColorNames().map(c => 'bg-' + c)); + this.colorPreviewEl.style.removeProperty('background-color'); + + if (this._value) { + if (ColorpickerDialog.isCSSColor(this._value)) { + this.colorPreviewEl.style.backgroundColor = this._value; + } else { + this.colorPreviewEl.classList.add('bg-' + this._value); + } + } + + await this._renderColorPalette(); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Called when a color button is clicked -> confirms the preview. + * + * @private + * @param {Event} ev + */ + _onColorPicked: function (ev) { + this._previewColor = false; + this._value = ev.data.color; + this._notifyValueChange(false); + }, + /** + * Called when a color button is entered -> previews the background color. + * + * @private + * @param {Event} ev + */ + _onColorHovered: function (ev) { + this._previewColor = ev.data.color; + this._notifyValueChange(true); + }, + /** + * Called when a color button is left -> cancels the preview. + * + * @private + * @param {Event} ev + */ + _onColorLeft: function (ev) { + this._previewColor = false; + this._notifyValueChange('reset'); + }, + /** + * Called when the color reset button is clicked -> removes all color + * classes and color styles. + * + * @private + */ + _onColorReset: function () { + this._value = ''; + this._notifyValueChange(false); + }, +}); + const userValueWidgetsRegistry = { 'we-button': ButtonUserValueWidget, 'we-checkbox': CheckboxUserValueWidget, 'we-select': SelectUserValueWidget, 'we-input': InputUserValueWidget, 'we-multi': MultiUserValueWidget, + 'we-colorpicker': ColorpickerUserValueWidget, }; -//:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: - /** * Handles a set of options for one snippet. The registry returned by this * module contains the names of the specialized SnippetOptionWidget which can be @@ -1024,7 +1178,54 @@ const SnippetOptionWidget = Widget.extend({ * @param {Object} params */ selectStyle: function (previewMode, widgetValue, params) { + if (params.cssProperty === 'background-color') { + this.$target.trigger('background-color-event', previewMode); + } + const cssProps = weUtils.CSS_SHORTHANDS[params.cssProperty] || [params.cssProperty]; + for (const cssProp of cssProps) { + // Always reset the inline style first to not put inline style on an + // element which already have this style through css stylesheets. + this.$target[0].style.setProperty(cssProp, ''); + } + if (params.extraClass) { + this.$target.removeClass(params.extraClass); + } + + // Only allow to use a color name as a className if we know about the + // other potential color names (to remove) and if we know about a prefix + // (otherwise we suppose that we should use the actual related color). + if (params.colorNames && params.colorPrefix) { + const classes = params.colorNames.map(c => params.colorPrefix + c); + this.$target[0].classList.remove(...classes); + + if (params.colorNames.includes(widgetValue)) { + const originalCSSValue = window.getComputedStyle(this.$target[0])[cssProps[0]]; + const className = params.colorPrefix + widgetValue; + this.$target[0].classList.add(className); + if (originalCSSValue !== window.getComputedStyle(this.$target[0])[cssProps[0]]) { + // If applying the class did indeed changed the css + // property we are editing, nothing more has to be done. + // (except adding the extra class) + this.$target.addClass(params.extraClass); + return; + } + // Otherwise, it means that class probably does not exist, + // we remove it and continue. Especially useful for some + // prefixes which only work with some color names but not all. + this.$target[0].classList.remove(className); + } + } + + // At this point, the widget value is either a property/color name or + // an actual css property value. If it is a property/color name, we will + // apply a css variable as style value. + const htmlStyle = window.getComputedStyle(document.documentElement); + const htmlPropValue = htmlStyle.getPropertyValue('--' + widgetValue); + if (htmlPropValue) { + widgetValue = `var(--${widgetValue})`; + } + const values = widgetValue.split(/\s+/g); while (values.length < cssProps.length) { switch (values.length) { @@ -1043,15 +1244,6 @@ const SnippetOptionWidget = Widget.extend({ } } - for (const cssProp of cssProps) { - // Always reset the inline style first to not put inline style on an - // element which already have this style through css stylesheets. - this.$target[0].style.setProperty(cssProp, ''); - } - if (params.extraClass) { - this.$target.removeClass(params.extraClass); - } - const styles = window.getComputedStyle(this.$target[0]); let hasUserValue = false; for (let i = cssProps.length - 1; i > 0; i--) { @@ -1160,6 +1352,14 @@ const SnippetOptionWidget = Widget.extend({ return dataValue || params.attributeDefaultValue || ''; } case 'selectStyle': { + if (params.colorPrefix && params.colorNames) { + for (const c of params.colorNames) { + if (this.$target[0].classList.contains(params.colorPrefix + c)) { + return c; + } + } + } + const styles = window.getComputedStyle(this.$target[0]); const cssProps = weUtils.CSS_SHORTHANDS[params.cssProperty] || [params.cssProperty]; const cssValues = cssProps.map(cssProp => { @@ -1198,7 +1398,7 @@ const SnippetOptionWidget = Widget.extend({ * @returns {string} */ _normalizeWidgetValue: function (value) { - value = `${value}`; // Force to string + value = `${value}`.trim(); // Force to a trimmed string value = ColorpickerDialog.normalizeCSSColor(value); // If is a css color, normalize it return value; }, @@ -1682,137 +1882,6 @@ registry['sizing_y'] = registry.sizing.extend({ }, }); -/** - * Handles the edition of snippet's background color classes. - */ -registry.colorpicker = SnippetOptionWidget.extend({ - xmlDependencies: ['/web_editor/static/src/xml/snippets.xml'], - custom_events: { - 'color_picked': '_onColorPicked', - 'color_hover': '_onColorHovered', - 'color_leave': '_onColorLeft', - 'color_reset': '_onColorReset', - }, - - /** - * @override - */ - start: async function () { - const _super = this._super.bind(this); - const args = arguments; - - // Pre-instanciate the color palette widget - const options = { - selectedColor: this.$target.css('background-color'), - targetClasses: [...this.$target[0].classList], - }; - if (this.data.paletteExclude) { - options.excluded = this.data.paletteExclude.replace(/ /g, '').split(','); - } - if (this.data.colorPrefix) { - options.colorPrefix = this.data.colorPrefix; - } - this.colorPalette = new ColorPaletteWidget(this, options); - await this.colorPalette.appendTo(document.createDocumentFragment()); - - // Build the select element with a custom span to hold the color preview - this.colorPreviewEl = document.createElement('span'); - const selectWidget = new SelectUserValueWidget(this, this.data.string, { - classes: ['o_we_so_color_palette'], - childNodes: [this.colorPalette.el], - valueEl: this.colorPreviewEl, - }); - await selectWidget.appendTo(document.createDocumentFragment()); - - // Replace the colorpicker UI with the select element - this.$el.empty().append(selectWidget.el); - return _super(...args); - }, - /** - * @override - */ - onFocus: function () { - this.colorPalette.reloadColorPalette(); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Change the color of the targeted background - * - * @private - * @param {string} cssColor - * @param {boolean} isClass - */ - _changeTargetColor: function (cssColor, isClass) { - this.$target[0].classList.remove(...this.colorPalette.getClasses()); - if (isClass) { - this.$target.addClass(cssColor); - } else { - this.$target.css('background-color', cssColor); - } - }, - /** - * @override - */ - _updateUI: async function () { - await this._super.apply(this, arguments); - this.colorPreviewEl.style.backgroundColor = this.$target.css('background-color'); - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * Called when a color button is clicked -> confirm the preview. - * - * @private - * @param {Event} ev - */ - _onColorPicked: function (ev) { - this._changeTargetColor(ev.data.cssColor); - this._updateUI().then(() => { - this.$target.closest('.o_editable').trigger('content_changed'); - this.$target.trigger('background-color-event', false); - }); - }, - /** - * Called when a color button is entered -> preview the background color. - * - * @private - * @param {Event} ev - */ - _onColorHovered: function (ev) { - this._changeTargetColor(ev.data.cssColor, ev.data.isClass); - this.$target.trigger('background-color-event', true); - }, - /** - * Called when a color button is left -> cancel the preview. - * - * @private - * @param {Event} ev - */ - _onColorLeft: function (ev) { - this._changeTargetColor(ev.data.cssColor, ev.data.isClass); - this.$target.trigger('background-color-event', 'reset'); - }, - /** - * Called when the color reset button is clicked -> remove all background - * color classes. - * - * @private - */ - _onColorReset: function () { - this._changeTargetColor(''); - this._updateUI().then(() => { - this.$target.trigger('content_changed'); - }); - }, -}); - /** * Handles the edition of snippet's background image. */ diff --git a/addons/web_editor/static/src/js/wysiwyg/widgets/color_palette.js b/addons/web_editor/static/src/js/wysiwyg/widgets/color_palette.js index 38edef463b6..04a21c79378 100644 --- a/addons/web_editor/static/src/js/wysiwyg/widgets/color_palette.js +++ b/addons/web_editor/static/src/js/wysiwyg/widgets/color_palette.js @@ -27,8 +27,6 @@ const ColorPaletteWidget = Widget.extend({ * * @param {Object} [options] * @param {string} [options.selectedColor] The class or css attribute color selected by default. - * @param {string[]} [options.targetClasses] - * @param {string} [options.colorPrefix='bg-'] Used for the class based colors (theme). * @param {boolean} [options.resetButton=true] Whether to display or not the reset button. * @param {string[]} [options.excluded=[]] Sections not to display. * @param {string[]} [options.excludeSectionOf] Extra section to exclude: the one containing the named color. @@ -39,8 +37,6 @@ const ColorPaletteWidget = Widget.extend({ this.summernoteCustomColorsArray = [].concat(...summernoteCustomColors); this.options = _.extend({ selectedColor: false, - targetClasses: [], - colorPrefix: 'bg-', resetButton: true, excluded: [], excludeSectionOf: null, @@ -124,31 +120,26 @@ const ColorPaletteWidget = Widget.extend({ this.$el.append($('