diff options
| author | fja-odoo <fja@odoo.com> | 2019-10-21 13:27:16 +0000 |
|---|---|---|
| committer | qsm-odoo <qsm@odoo.com> | 2019-11-18 09:56:40 +0000 |
| commit | 22c5fc5dd2fcc7e37f103876a219727122d23293 (patch) | |
| tree | 7fb91b5832971643e8b97974574ec34a66dfdacd /addons/web_editor/static/src | |
| parent | 2006959761a0fb584f3dcfc45c7cac1aebd12430 (diff) | |
[IMP] web_editor, website: generalize colorpicker
Currently we have:
- a colorpicker for background color of snippets
- a colorpicker for foreground and background text (topbar)
- a colorpicker for menu/footer/navbar/etc
These 3 are separate colorpickers with different options.
After this Commit
All 3 will use the same color palette wich allow you to select
theme color, common colors, custom color and transparent filter.
Part of https://github.com/odoo/odoo/pull/38959
maintask-2066614
task-2087383
Diffstat (limited to 'addons/web_editor/static/src')
8 files changed, 471 insertions, 290 deletions
diff --git a/addons/web_editor/static/src/js/editor/editor.js b/addons/web_editor/static/src/js/editor/editor.js index c3a2e3de444..ea00a05af26 100644 --- a/addons/web_editor/static/src/js/editor/editor.js +++ b/addons/web_editor/static/src/js/editor/editor.js @@ -18,6 +18,7 @@ var EditorMenuBar = Widget.extend({ 'click button[data-action=cancel]': '_onCancelClick', }, custom_events: { + request_editable: '_onRequestEditable', request_history_undo_record: '_onHistoryUndoRecordRequest', request_save: '_onSaveRequest', }, @@ -324,6 +325,13 @@ var EditorMenuBar = Widget.extend({ ev.stopPropagation(); this.save(ev.data.reload).then(ev.data.onSuccess, ev.data.onFailure); }, + /** + * @private + * @param {OdooEvent} ev + */ + _onRequestEditable: function (ev) { + ev.data.callback(this.rte.editable()); + }, }); return { 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 e63d87e54b2..913d35575ab 100644 --- a/addons/web_editor/static/src/js/editor/rte.summernote.js +++ b/addons/web_editor/static/src/js/editor/rte.summernote.js @@ -1,30 +1,17 @@ odoo.define('web_editor.rte.summernote', function (require) { 'use strict'; -var ajax = require('web.ajax'); var Class = require('web.Class'); +const concurrency = require('web.concurrency'); var core = require('web.core'); -var ColorpickerDialog = require('web.ColorpickerDialog'); +var ColorPaletteWidget = require('web_editor.ColorPalette').ColorPaletteWidget; var mixins = require('web.mixins'); var fonts = require('wysiwyg.fonts'); -var weContext = require('web_editor.context'); var rte = require('web_editor.rte'); var weWidgets = require('wysiwyg.widgets'); -var QWeb = core.qweb; var _t = core._t; -ajax.jsonRpc('/web/dataset/call_kw', 'call', { - 'model': 'ir.ui.view', - 'method': 'read_template', - 'args': ['web_editor.colorpicker'], - 'kwargs': { - 'context': weContext.get(), - } -}).then(function (data) { - QWeb.add_template('<templates>' + data + '</templates>'); -}); - // Summernote Lib (neek change to make accessible: method and object) var dom = $.summernote.core.dom; var range = $.summernote.core.range; @@ -35,106 +22,53 @@ var tplButton = renderer.getTemplate().button; var tplIconButton = renderer.getTemplate().iconButton; var tplDropdown = renderer.getTemplate().dropdown; -function _rgbToHex(cssColor) { - var rgba = cssColor.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/); - if (!rgba) { - return cssColor; - } - if (rgba[4]) { - return cssColor; - } - var hex = ColorpickerDialog.convertRgbToHex( - parseInt(rgba[1]), - parseInt(rgba[2]), - parseInt(rgba[3]) - ); - if (!hex) { - return cssColor; // TODO handle error - } - return hex.hex.toUpperCase(); -} +var applyColor = function (target, eventName, color) { + var layoutInfo = dom.makeLayoutInfo(target); + $.summernote.pluginEvents[eventName](undefined, eventHandler.modules.editor, layoutInfo, color); +}; // Update and change the popovers content, and add history button -var fn_createPalette = renderer.createPalette; renderer.createPalette = function ($container, options) { - fn_createPalette.call(this, $container, options); - - if (!QWeb.has_template('web_editor.colorpicker')) { - return; - } - - var $clpicker = $(QWeb.render('web_editor.colorpicker')); - - var groups; - if ($clpicker.is("colorpicker")) { - groups = _.map($clpicker.find('[data-name="theme"]'), function (el) { - return $(el).find("button").empty(); + const $dropdownContent = $container.find(".colorPalette"); + // The editor's root widget can be website or web's root widget and cannot be properly retrieved... + const parent = odoo.__DEBUG__.services['root.widget']; + _.each($dropdownContent, elem => { + const eventName = elem.dataset.eventName; + let colorpicker = null; + const mutex = new concurrency.MutexedDropPrevious(); + const $dropdown = $(elem).closest('.btn-group, .dropdown'); + let manualOpening = false; + $dropdown.on('show.bs.dropdown', () => { + if (manualOpening) { + return true; + } + mutex.exec(() => { + const oldColorpicker = colorpicker; + const hookEl = oldColorpicker ? oldColorpicker.el : elem; + + const r = range.create(); + 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 custom_color_picked', null, ev => applyColor(ev.data.target, eventName, ev.data.cssColor)); + colorpicker.on('color_reset', null, ev => applyColor(ev.data.target, eventName, 'inherit')); + return colorpicker.replace(hookEl).then(() => { + if (oldColorpicker) { + oldColorpicker.destroy(); + } + manualOpening = true; + $dropdown.children('.dropdown-toggle').dropdown('show'); + manualOpening = false; + }); + }); + return false; }); - } else { - groups = [$clpicker.find("button").empty()]; - } - - var html = "<h6 class='mt-2'>" + _t("Theme colors") + "</h6>" + _.map(groups, function ($group) { - var $row = $("<div/>", {"class": "note-color-row mb8"}).append($group); - var $after_breaks = $row.find(".o_small + :not(.o_small)"); - if ($after_breaks.length === 0) { - $after_breaks = $row.find(":nth-child(8n+9)"); - } - $after_breaks.addClass("o_clear"); - return $row[0].outerHTML; - }).join("") + "<h6 class='mt-2'>" + _t("Common colors") + "</h6>"; - var $palettes = $container.find(".note-color .note-color-palette"); - $palettes.prepend(html); - - // Find the custom colors which are used in the page and add them to the color palette - var colors = []; - var $editable = window.__EditorMenuBar_$editable || $(); - _.each($editable.find('[style*="color"]'), function (element) { - if (element.style.color) { - colors.push(element.style.color); - } - if (element.style.backgroundColor) { - colors.push(element.style.backgroundColor); - } - }); - - var $customColorPalettes = $container.find('.note-color .note-custom-color-palette').append($('<div/>', {class: "note-color-row"})); - var $customColorRows = $customColorPalettes.find('.note-color-row'); - _.each(_.uniq(colors), function (color) { - var hexColor = _rgbToHex(color); - if (_.indexOf(_.flatten(options.colors), hexColor) < 0) { - // Create button for used custom color for backColor and foreColor both and add them into palette - $customColorRows.append('<button type="button" class="o_custom_color" data-color="' + color + '" style="background-color:' + color + ';" />'); - } - }); - - $palettes.push.apply($palettes, $customColorPalettes); - - var $forePalette = $palettes.filter(":even").closest('li').addClass('note-palette'); - var $bgPalette = $palettes.filter(":odd").closest('li').addClass('note-palette'); - - $forePalette.add($bgPalette).find('.note-color-reset').remove(); - $forePalette.children().each(function () { - var $reset = $('<div class="note-color-reset" data-event="foreColor" data-value="inherit"></div>').text(_t('Reset to default')); - $(this).prepend($reset); - }) - $bgPalette.children().each(function () { - var $reset = $('<div class="note-color-reset" data-event="backColor" data-value="inherit"></div>').text(_t('Reset to default')); - $(this).prepend($reset); - }) - - var $fore = $forePalette.find("button:not(.note-color-btn)").addClass("note-color-btn"); - $fore.each(function () { - var $el = $(this); - var className = $el.hasClass('o_custom_color') ? $el.data('color') : 'text-' + $el.data('color'); - $el.attr('data-event', 'foreColor').attr('data-value', className).addClass($el.hasClass('o_custom_color') ? '' : 'bg-' + $el.data('color')); - }); - - var $bg = $bgPalette.find("button:not(.note-color-btn)").addClass("note-color-btn"); - $bg.each(function () { - var $el = $(this); - var className = $el.hasClass('o_custom_color') ? $el.data('color') : 'bg-' + $el.data('color'); - $el.attr('data-event', 'backColor').attr('data-value', className).addClass($el.hasClass('o_custom_color') ? '' : className); }); }; @@ -291,8 +225,6 @@ eventHandler.modules.popover.button.update = function ($container, oStyle) { $(oStyle.image).addClass('o_we_selected_image'); if (dom.isImgFont(oStyle.image)) { - $container.find('[data-event="customColor"][data-value="foreColor"]').attr('data-color', $(oStyle.image).css('color')); - $container.find('[data-event="customColor"][data-value="backColor"]').attr('data-color', $(oStyle.image).css('background-color')); $container.find('.note-fore-color-preview > button').css('border-bottom-color', $(oStyle.image).css('color')); $container.find('.note-back-color-preview > button').css('border-bottom-color', $(oStyle.image).css('background-color')); @@ -335,8 +267,6 @@ eventHandler.modules.popover.button.update = function ($container, oStyle) { $(oStyle.image).trigger('attributes_change'); } else { - $container.find('[data-event="customColor"][data-value="foreColor"]').attr('data-color', oStyle.color); - $container.find('[data-event="customColor"][data-value="backColor"]').attr('data-color', oStyle['background-color']); $container.find('.note-fore-color-preview > button').css('border-bottom-color', oStyle.color); $container.find('.note-back-color-preview > button').css('border-bottom-color', oStyle['background-color']); } @@ -505,25 +435,6 @@ $.summernote.pluginEvents.alt = function (event, editor, layoutInfo, sorted) { media: $selection.data('target'), }); }; -$.summernote.pluginEvents.customColor = function (event, editor, layoutInfo, customColor) { - var defaultColor = event.target.dataset.color; - core.bus.trigger('color_picker_dialog_demand', { - color: defaultColor === 'rgba(0, 0, 0, 0)' ? 'rgb(255, 0, 0)' : defaultColor, - onSave: function (color) { - var $palettes = $(event.currentTarget).find('.note-custom-color-palette > .note-color-row') - .append(('<button type="button" class="note-color-btn" data-value="' + color + '" style="background-color:' + color + ';" />')); - $palettes.filter(':odd').find('button:not([data-event])').attr('data-event', 'backColor'); - $palettes.filter(':even').find('button:not([data-event])').attr('data-event', 'foreColor'); - if (customColor === 'foreColor') { - $(event.currentTarget).find('.note-fore-color-preview > button').css('border-bottom-color', color); - $.summernote.pluginEvents.foreColor(event, editor, layoutInfo, color); - } else { - $(event.currentTarget).find('.note-back-color-preview > button').css('border-bottom-color', color); - $.summernote.pluginEvents.backColor(event, editor, layoutInfo, color); - } - } - }); -}; $.summernote.pluginEvents.cropImage = function (event, editor, layoutInfo, sorted) { var $editable = layoutInfo.editable(); var $selection = layoutInfo.handle().find('.note-control-selection'); @@ -1142,7 +1053,6 @@ var SummernoteManager = Class.extend(mixins.EventDispatcherMixin, { this.setParent(parent); core.bus.on('alt_dialog_demand', this, this._onAltDialogDemand); - core.bus.on('color_picker_dialog_demand', this, this._onColorPickerDialogDemand); core.bus.on('crop_image_dialog_demand', this, this._onCropImageDialogDemand); core.bus.on('link_dialog_demand', this, this._onLinkDialogDemand); core.bus.on('media_dialog_demand', this, this._onMediaDialogDemand); @@ -1154,7 +1064,6 @@ var SummernoteManager = Class.extend(mixins.EventDispatcherMixin, { mixins.EventDispatcherMixin.destroy.call(this); core.bus.off('alt_dialog_demand', this, this._onAltDialogDemand); - core.bus.off('color_picker_dialog_demand', this, this._onColorPickerDialogDemand); core.bus.off('crop_image_dialog_demand', this, this._onCropImageDialogDemand); core.bus.off('link_dialog_demand', this, this._onLinkDialogDemand); core.bus.off('media_dialog_demand', this, this._onMediaDialogDemand); @@ -1187,27 +1096,6 @@ var SummernoteManager = Class.extend(mixins.EventDispatcherMixin, { } altDialog.open(); }, -/** - * Called when a demand to open a color picker dialog is received on the bus. - * - * @private - * @param {Object} data - */ - _onColorPickerDialogDemand: function (data) { - if (data.__alreadyDone) { - return; - } - data.__alreadyDone = true; - var colorpicker = new ColorpickerDialog(this, { - defaultColor: data.color, - }); - if (data.onSave) { - colorpicker.on('colorpicker:saved', this, function (ev) { - data.onSave(ev.data.cssColor); - }); - } - colorpicker.open(); - }, /** * Called when a demand to open a crop dialog is received on the bus. * 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 126273dc28b..4c592157c71 100644 --- a/addons/web_editor/static/src/js/editor/snippets.options.js +++ b/addons/web_editor/static/src/js/editor/snippets.options.js @@ -2,10 +2,11 @@ odoo.define('web_editor.snippets.options', function (require) { 'use strict'; var core = require('web.core'); +var ColorPaletteWidget = require('web_editor.ColorPalette').ColorPaletteWidget; var Dialog = require('web.Dialog'); +var rte = require('web_editor.rte'); var Widget = require('web.Widget'); var weWidgets = require('wysiwyg.widgets'); -var summernoteCustomColors = require('web_editor.rte.summernote_custom_colors'); var qweb = core.qweb; var _t = core._t; @@ -1146,93 +1147,57 @@ registry['sizing_y'] = registry.sizing.extend({ */ registry.colorpicker = SnippetOption.extend({ xmlDependencies: ['/web_editor/static/src/xml/snippets.xml'], - events: _.extend({}, SnippetOption.prototype.events || {}, { - 'click .colorpicker button': '_onColorButtonClick', - 'mouseenter .colorpicker button': '_onColorButtonEnter', - 'mouseleave .colorpicker button': '_onColorButtonLeave', - 'click .note-color-reset': '_onColorResetButtonClick', - }), - colorPrefix: 'bg-', + custom_events: { + color_picked: '_onColorPicked', + custom_color_picked: '_onCustomColor', + color_hover: '_onColorHovered', + color_leave: '_onColorLeft', + color_reset: '_onColorReset', + }, /** * @override */ start: function () { - var self = this; - var res = this._super.apply(this, arguments); - + const options = { + selectedColor: this.$target.css('background-color'), + targetClasses: [...this.$target[0].classList], + }; + const proms = [this._super(...arguments)]; + if (this.data.paletteExclude) { + options.excluded = this.data.paletteExclude.replace(/ /g, '').split(','); + } if (this.data.colorPrefix) { - this.colorPrefix = this.data.colorPrefix; + options.colorPrefix = this.data.colorPrefix; } + this.colorPalette = new ColorPaletteWidget(this, options); + proms.push(this.colorPalette.appendTo(this.$el.find('we-collapse'))); - if (!this.$el.find('.colorpicker').length) { - // TODO remove old UI's code that survived - var $pt = $(qweb.render('web_editor.snippet.option.colorpicker')); - var $clpicker = $(qweb.render('web_editor.colorpicker')); - - _.each($clpicker.find('.o_colorpicker_section'), function (elem) { - $(elem).prepend("<div>" + elem.dataset.display + "</div>"); - }); - - // Retrieve excluded palettes list - var excluded = []; - if (this.data.paletteExclude) { - excluded = this.data.paletteExclude.replace(/ /g, '').split(','); - } + return Promise.all(proms); + }, - // Remove excluded palettes - _.each(excluded, function (exc) { - $clpicker.find('[data-name="' + exc + '"]').remove(); - }); + onFocus: function () { + this.colorPalette.reloadColorPalette(); + }, - // Add common colors to palettes if not excluded - if (!('common' in excluded)) { - var $commonColorSection = $clpicker.find('[data-name="common"]'); - _.each(summernoteCustomColors, function (colorRow, i) { - var $div = $('<div/>', {class: 'clearfix'}).appendTo($commonColorSection); - if (i === 0) { - // Ignore the summernote gray palette and use ours - return; - } - _.each(colorRow, function (color) { - $div.append('<button class="o_custom_color" style="background-color: ' + color + '" />'); - }); - }); - } + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- - $pt.find('.o_colorpicker_section_tabs').append($clpicker); - this.$el.find('we-collapse').append($pt); + /** + * 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); } - - - // TODO refactor in master - // The primary and secondary are hardcoded here (but marked as hidden) - // so they can be removed from snippets when selecting another color. - // Normally, the chosable colors do not contain them, which prevents - // them to be removed. Indeed, normally, the 'alpha' and 'beta' colors - // (which are the same) are displayed instead... but not for all themes. - var $colorpicker = this.$el.find('.colorpicker'); - $colorpicker.append($('<button/>', {'class': 'd-none', 'data-color': 'primary'})); - $colorpicker.append($('<button/>', {'class': 'd-none', 'data-color': 'secondary'})); - - var classes = []; - this.$el.find('.colorpicker button').each(function () { - var $color = $(this); - var color = $color.data('color'); - if (!color) { - return; - } - - $color.addClass('bg-' + color); - var className = self.colorPrefix + color; - if (self.$target.hasClass(className)) { - $color.addClass('selected'); - } - classes.push(className); - }); - this.classes = classes.join(' '); - - return res; }, //-------------------------------------------------------------------------- @@ -1245,28 +1210,26 @@ registry.colorpicker = SnippetOption.extend({ * @private * @param {Event} ev */ - _onColorButtonClick: function (ev) { - this.$el.find('.colorpicker button.selected').removeClass('selected'); - $(ev.currentTarget).addClass('selected'); + _onColorPicked: function () { this.$target.closest('.o_editable').trigger('content_changed'); this.$target.trigger('background-color-event', false); }, /** + * Called when a custom color is selected + * @param {*} ev + */ + _onCustomColor: function (ev) { + this._changeTargetColor(ev.data.cssColor); + this._onColorPicked(); + }, + /** * Called when a color button is entered -> preview the background color. * * @private * @param {Event} ev */ - _onColorButtonEnter: function (ev) { - this.$target.removeClass(this.classes); - var color = $(ev.currentTarget).data('color'); - if (color) { - this.$target.addClass(this.colorPrefix + color); - } else if ($(ev.target).hasClass('o_custom_color')) { - this.$target - .removeClass(this.classes) - .css('background-color', ev.currentTarget.style.backgroundColor); - } + _onColorHovered: function (ev) { + this._changeTargetColor(ev.data.cssColor, ev.data.isClass); this.$target.trigger('background-color-event', true); }, /** @@ -1275,17 +1238,8 @@ registry.colorpicker = SnippetOption.extend({ * @private * @param {Event} ev */ - _onColorButtonLeave: function (ev) { - this.$target.removeClass(this.classes); - this.$target.css('background-color', ''); - var $selected = this.$el.find('.colorpicker button.selected'); - if ($selected.length) { - if ($selected.data('color')) { - this.$target.addClass(this.colorPrefix + $selected.data('color')); - } else if ($selected.hasClass('o_custom_color')) { - this.$target.css('background-color', $selected.css('background-color')); - } - } + _onColorLeft: function (ev) { + this._changeTargetColor(ev.data.cssColor, ev.data.isClass); this.$target.trigger('background-color-event', 'reset'); }, /** @@ -1294,10 +1248,9 @@ registry.colorpicker = SnippetOption.extend({ * * @private */ - _onColorResetButtonClick: function () { - this.$target.removeClass(this.classes).css('background-color', ''); + _onColorReset: function () { + this._targetColorChange(''); this.$target.trigger('content_changed'); - this.$el.find('.colorpicker button.selected').removeClass('selected'); }, }); diff --git a/addons/web_editor/static/src/js/editor/summernote.js b/addons/web_editor/static/src/js/editor/summernote.js index 4228d353d1a..06a282d0076 100644 --- a/addons/web_editor/static/src/js/editor/summernote.js +++ b/addons/web_editor/static/src/js/editor/summernote.js @@ -983,33 +983,15 @@ renderer.tplButtonInfo.color = function (lang, options) { event: 'color', value: '{"backColor":"#B35E9B"}' }); - var foreColorItems = [ - '<li><div class="btn-group flex-column">', - '<div class="note-color-palette" data-target-event="foreColor"></div>', - '<h6 class="note-custom-color mt8" data-event="customColor" data-value="foreColor" title="' + lang.color.custom + '">', - lang.color.custom + '</h6>', - '<div class="note-custom-color-palette" data-target-event="foreColor"></div>', - '</div></li>', - ]; - var backColorItems = [ - '<li><div class="btn-group flex-column">', - '<div class="note-color-reset" data-event="backColor" data-value="inherit" title="' + lang.color.transparent + '">', - lang.color.setTransparent + '</div>', - '<div class="note-color-palette" data-target-event="backColor"></div>', - '<h6 class="note-custom-color mt8" data-event="customColor" data-value="backColor" title="' + lang.color.custom + '">', - lang.color.custom + '</h6>', - '<div class="note-custom-color-palette" data-target-event="backColor"></div>', - '</div></li>', - ]; var foreColorButton = renderer.getTemplate().button(foreColorButtonLabel, { className: 'note-fore-color-preview mx-1', title: lang.color.foreground, - dropdown: renderer.getTemplate().dropdown(foreColorItems) + dropdown: renderer.getTemplate().dropdown('<li><div data-event-name="foreColor" class="colorPalette"/></li>'), }); var backColorButton = renderer.getTemplate().button(backColorButtonLabel, { className: 'note-back-color-preview mx-1', title: lang.color.background, - dropdown: renderer.getTemplate().dropdown(backColorItems) + dropdown: renderer.getTemplate().dropdown('<li><div data-event-name="backColor" class="colorPalette"/></li>'), }); return recentColorButton + foreColorButton + backColorButton; }; 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 new file mode 100644 index 00000000000..def39a53b32 --- /dev/null +++ b/addons/web_editor/static/src/js/wysiwyg/widgets/color_palette.js @@ -0,0 +1,342 @@ +odoo.define('web_editor.ColorPalette', function (require) { +'use strict'; + +const core = require('web.core'); +const ColorPickerDialog = require('web.ColorpickerDialog'); +const Dialog = require('web.Dialog'); +const Widget = require('web.Widget'); +const summernoteCustomColors = require('web_editor.rte.summernote_custom_colors'); + +const qweb = core.qweb; +const _t = core._t; + +let templatePromise; + +const ColorPaletteWidget = Widget.extend({ + xmlDependencies: ['/web_editor/static/src/xml/snippets.xml'], + template: 'web_editor.snippet.option.colorpicker', + events: { + 'click button': '_onColorButtonClick', + 'mouseenter button': '_onColorButtonEnter', + 'mouseleave button': '_onColorButtonLeave', + 'click .o_colorpicker_reset': '_onColorResetButtonClick', + 'click .o_add_custom_color': '_onCustomColorButtonClick', + }, + /** + * @override + * + * @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 {JQuery} [options.$editable=$()] Editable content from which the custom colors are retrieved. + */ + init: function (parent, options) { + this._super.apply(this, arguments); + this.summernoteCustomColorsArray = [].concat(...summernoteCustomColors).map((color) => this._convertHexToCssRgba(color)); + this.options = _.extend({ + selectedColor: false, + targetClasses: [], + colorPrefix: 'bg-', + resetButton: true, + excluded: [], + $editable: $(), + }, options || {}); + + this.trigger_up('request_editable', {callback: val => this.options.$editable = val}); + }, + /** + * @override + */ + willStart: async function () { + const res = this._super.apply(this, arguments); + // We can call the colorPalette multiple times but only need 1 rpc + if (!templatePromise && !qweb.has_template('web_editor.colorpicker')) { + templatePromise = this._rpc({ + model: 'ir.ui.view', + method: 'read_template', + args: ['web_editor.colorpicker'], + }).then(template => { + return qweb.add_template('<templates>' + template + '</templates>'); + }); + } + await templatePromise; + return res; + }, + + /** + * @override + */ + start: function () { + const res = this._super.apply(this, arguments); + + const $wrapper = this.$('.o_colorpicker_section_tabs'); + $(qweb.render('web_editor.colorpicker')).appendTo($wrapper); + + this.el.querySelectorAll('.o_colorpicker_section').forEach(elem => { + $(elem).prepend('<div>' + (elem.dataset.display || '') + '</div>'); + }); + + if (this.options.resetButton) { + this.$('.o_colorpicker_reset').removeClass('d-none'); + } + + // Remove excluded palettes + _.each(this.options.excluded, function (exc) { + $wrapper.find('[data-name="' + exc + '"]').remove(); + }); + + // Render common colors + if (!this.options.excluded.includes('common')) { + const $commonColorSection = this.$('[data-name="common"]'); + summernoteCustomColors.forEach((colorRow, i) => { + if (i === 0) { + return; // Ignore the summernote gray palette and use ours + } + const $div = $('<div/>', {class: 'clearfix'}).appendTo($commonColorSection); + colorRow.forEach(color => { + $div.append(this._createColorButton(color, ['o_common_color'])); + }); + }); + } + + // Render custom colors + this._buildCustomColor(); + + // TODO refactor in master + // The primary and secondary are hardcoded here (but marked as hidden) + // so they can be removed from snippets when selecting another color. + // Normally, the chosable colors do not contain them, which prevents + // them to be removed. Indeed, normally, the 'alpha' and 'beta' colors + // (which are the same) are displayed instead... but not for all themes. + this.$el.append($('<button/>', {'class': 'd-none', 'data-color': 'primary'})); + this.$el.append($('<button/>', {'class': 'd-none', 'data-color': 'secondary'})); + + // Compute class colors + this.classes = []; + this.el.querySelectorAll('button[data-color]').forEach(elem => { + const color = elem.dataset.color; + const $color = $(elem); + $color.addClass('bg-' + color); + const className = this.options.colorPrefix + color; + if (this.options.targetClasses.includes(className)) { + $color.addClass('selected'); + } + this.classes.push(className); + }); + + return res; + }, + /** + * Return a list of the color classes used in the color palette + */ + getClasses: function () { + return this.classes; + }, + /** + * Reloads the color palette to get other custom colors on the page + */ + reloadColorPalette: function () { + this._buildCustomColor(); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + */ + _buildCustomColor: function () { + const existingColors = new Set(this.summernoteCustomColorsArray.concat(Array.from(this.el.querySelectorAll('.o_custom_color')).map(el => el.style.backgroundColor))); + if (!this.options.excluded.includes('custom')) { + Array.from(this.options.$editable.find('[style*="color"]')).forEach(el => { + const color = el.style.color || el.style.backgroundColor; + if (color && !existingColors.has(color)) { + this._addCustomColorButton(color); + existingColors.add(color); + } + }); + } + }, + /** + * Add a custom button in the coresponding section. + * + * @private + * @param {string} color Css rgb(a) string + * @param {Array<string>} classes Classes added to the button + */ + _addCustomColorButton: function (color, classes = []) { + classes.push('o_custom_color'); + this._createColorButton(color, classes).appendTo(this.$('.o_colorpicker_section[data-name="theme"]')); + }, + /** + * Return a color button. + * + * @param {string} color Css rgb(a) string + * @param {Array<string>} classes Classes added to the button + */ + _createColorButton: function (color, classes) { + const cssColor = this._convertHexToCssRgba(color); + const selectedColor = this.options.selectedColor && this._convertHexToCssRgba(this.options.selectedColor); + if (selectedColor === cssColor) { + classes.push('selected'); + } + return $('<button/>', { + class: classes.join(' '), + style: 'background-color:' + color + ';', + }); + }, + /** + * Return the hexadecimal css color as an rgb(a) css color: rgb((r, g, b) | rgba(r, g, b, a) + * + * @private + * @param {string} hex Css hex string (#FFFFFF(FF)) + */ + _convertHexToCssRgba: function (hex) { + if (!/^#([0-9A-F]{6}|[0-9A-F]{8})$/i.test(hex)) { + return hex; + } + + const red = parseInt(hex.substr(1, 2), 16); + const blue = parseInt(hex.substr(3, 2), 16); + const green = parseInt(hex.substr(5, 2), 16); + if (hex.length === 9) { + return _.str.sprintf('rgba(%s, %s, %s, %s)', + red, + blue, + green, + parseInt(hex.substr(7, 2), 16) + ); + } else { + return _.str.sprintf('rgb(%s, %s, %s)', + red, + blue, + green + ); + } + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Called when a color button is clicked. + * + * @private + * @param {Event} ev + */ + _onColorButtonClick: function (ev) { + const target = ev.currentTarget; + this.$('button.selected').removeClass('selected'); + $(target).addClass('selected'); + this.trigger_up('color_picked', { + cssColor: target.style.backgroundColor || this.options.colorPrefix + target.dataset.color, + isClass: !!target.dataset.color, + target: ev.target, + }); + }, + /** + * Called when a color button is entered. + * + * @private + * @param {Event} ev + */ + _onColorButtonEnter: function (ev) { + ev.stopPropagation(); + const target = ev.currentTarget; + this.trigger_up('color_hover', { + cssColor: target.style.backgroundColor || this.options.colorPrefix + target.dataset.color, + isClass: !!target.dataset.color, + target: ev.target, + }); + }, + /** + * Called when a color button is left. + * + * @private + * @param {Event} ev + */ + _onColorButtonLeave: function (ev) { + ev.stopPropagation(); + let params = { + cssColor: '', + isClass: false, + target: ev.target, + }; + const selected = this.el.querySelector('button.selected'); + if (selected) { + params = { + cssColor: selected.style.backgroundColor || this.options.colorPrefix + selected.dataset.color, + isClass: !!selected.dataset.color, + target: ev.target, + }; + } + this.trigger_up('color_leave', params); + }, + /** + * Called when the color reset button is clicked. + * + * @private + * @param {Event} ev + */ + _onColorResetButtonClick: function (ev) { + this.$('button.selected').removeClass('selected'); + this.trigger_up('color_reset', { + target: ev.target, + }); + }, + /** + * Called when the custom color button is clicked. + * + * @private + * @param {Event} ev + */ + _onCustomColorButtonClick: async function (ev) { + const originalEvent = ev; + const $selected = this.$('button.selected'); + const colorpicker = new ColorPickerDialog(this, { + defaultColor: this.options.defaultColor || $selected.css('background-color'), + }); + colorpicker.on('colorpicker:saved', this, ev => { + $selected.removeClass('selected'); + this._addCustomColorButton(ev.data.cssColor, ['selected']); + this.trigger_up('custom_color_picked', { + cssColor: ev.data.cssColor, + target: originalEvent.target, + }); + }); + colorpicker.open(); + }, +}); + +const ColorPaletteDialog = Dialog.extend({ + /** + * @override + */ + init: function (parent, options) { + this.options = options; + this._super(parent, { + size: 'small', + title: _t('Pick a color'), + }); + }, + /** + * @override + */ + start: function () { + const proms = [this._super(...arguments)]; + const colorPalette = new ColorPaletteWidget(this, this.options); + proms.push(colorPalette.appendTo(this.$el)); + return Promise.all(proms); + }, +}); + +return { + ColorPaletteWidget: ColorPaletteWidget, + ColorPaletteDialog: ColorPaletteDialog, +}; +}); diff --git a/addons/web_editor/static/src/scss/web_editor.variables.scss b/addons/web_editor/static/src/scss/web_editor.variables.scss index 11b7d7b84ff..5abbc9ac284 100644 --- a/addons/web_editor/static/src/scss/web_editor.variables.scss +++ b/addons/web_editor/static/src/scss/web_editor.variables.scss @@ -303,12 +303,14 @@ $o-theme-color-palette-number: 1; // Greyscale transparent colours $o-transparent-grays: ( + 'black-15': rgba(black, 0.15), 'black-25': rgba(black, 0.25), 'black-50': rgba(black, 0.5), 'black-75': rgba(black, 0.75), 'white-25': rgba(white, 0.25), 'white-50': rgba(white, 0.5), 'white-75': rgba(white, 0.75), + 'white-85': rgba(white, 0.85), ) !default; $o-gray-color-palettes: ( ( diff --git a/addons/web_editor/static/src/scss/wysiwyg_snippets.scss b/addons/web_editor/static/src/scss/wysiwyg_snippets.scss index e373c8e3b45..14df9435f48 100644 --- a/addons/web_editor/static/src/scss/wysiwyg_snippets.scss +++ b/addons/web_editor/static/src/scss/wysiwyg_snippets.scss @@ -479,7 +479,7 @@ body.editor_enable.editor_has_snippets { .o_colorpicker_section { > :first-child { - margin: 8px 0 4px; + margin: 4px 0; } button { position: relative; @@ -508,11 +508,13 @@ body.editor_enable.editor_has_snippets { } } - &[data-name="transparent_grayscale"] button { - @include o-alpha-button-preview; + &[data-name="transparent_grayscale"], &[data-name="theme"] { + button { + @include o-alpha-button-preview; - &::before, &::after { - box-shadow: inherit; + &::before, &::after { + box-shadow: inherit; + } } } } diff --git a/addons/web_editor/static/src/xml/snippets.xml b/addons/web_editor/static/src/xml/snippets.xml index 70ecd00c67e..b1f5835c599 100644 --- a/addons/web_editor/static/src/xml/snippets.xml +++ b/addons/web_editor/static/src/xml/snippets.xml @@ -37,12 +37,16 @@ <!-- options --> <div t-name="web_editor.snippet.option.colorpicker" class="colorpicker"> - <div class="btn-group palette-reset"> - <div class="note-color-reset" data-event="foreColor" data-value="inherit" title="None" role="img" aria-label="No color"> - <i class="fa fa-ban"/> - </div> - </div> + <div class="o_colorpicker_sections px-1 pb8"> + <div class="o_colorpicker_section"> + <a class="btn w-50 float-left o_colorpicker_reset d-none"> + <i class="fa fa-ban"/> Reset + </a> + <a class="btn w-50 o_add_custom_color" data-name="custom"> + <i class="fa fa-plus"/> Custom + </a> + </div> <div class="tab-content o_colorpicker_section_tabs"/> </div> </div> |
