summaryrefslogtreecommitdiff
path: root/addons/web_editor/static/src
diff options
context:
space:
mode:
authorfja-odoo <fja@odoo.com>2019-10-21 13:27:16 +0000
committerqsm-odoo <qsm@odoo.com>2019-11-18 09:56:40 +0000
commit22c5fc5dd2fcc7e37f103876a219727122d23293 (patch)
tree7fb91b5832971643e8b97974574ec34a66dfdacd /addons/web_editor/static/src
parent2006959761a0fb584f3dcfc45c7cac1aebd12430 (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')
-rw-r--r--addons/web_editor/static/src/js/editor/editor.js8
-rw-r--r--addons/web_editor/static/src/js/editor/rte.summernote.js202
-rw-r--r--addons/web_editor/static/src/js/editor/snippets.options.js159
-rw-r--r--addons/web_editor/static/src/js/editor/summernote.js22
-rw-r--r--addons/web_editor/static/src/js/wysiwyg/widgets/color_palette.js342
-rw-r--r--addons/web_editor/static/src/scss/web_editor.variables.scss2
-rw-r--r--addons/web_editor/static/src/scss/wysiwyg_snippets.scss12
-rw-r--r--addons/web_editor/static/src/xml/snippets.xml14
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>