[REF] website: unify the theme customize dialog color options

Before this commit, color options in the theme customize were treated
as big exception in the system. The whole color panel XML was defined
client-side and used custom design. Now, all colors are defined as
options in the "server-side" template, allowing for more customizations
and better maintenance.

This commit also makes the color saving system generic to allow for more
new options in the future (like customizing the logo height or the font
size, which is the next task).

PR https://github.com/odoo/odoo/pull/29624
task-1904244
This commit is contained in:
qsm-odoo
2019-02-08 14:37:52 +00:00
parent 065957b981
commit 45a54104bf
4 changed files with 276 additions and 229 deletions
+179 -140
View File
@@ -3,9 +3,9 @@ odoo.define('website.theme', function (require) {
var config = require('web.config');
var core = require('web.core');
var ColorpickerDialog = require('wysiwyg.widgets.ColorpickerDialog');
var Dialog = require('web.Dialog');
var weWidgets = require('wysiwyg.widgets');
var ColorpickerDialog = require('wysiwyg.widgets.ColorpickerDialog');
var websiteNavbarData = require('website.navbar');
var _t = core._t;
@@ -19,10 +19,11 @@ var ThemeCustomizeDialog = Dialog.extend({
template: 'website.theme_customize',
events: {
'change .o_theme_customize_option_input': '_onChange',
'click .o_theme_customize_option_input[type="radio"]:checked': '_onChange',
'click .o_theme_customize_color': '_onColorClick',
'click .checked .o_theme_customize_option_input[type="radio"]': '_onChange',
},
CUSTOM_BODY_IMAGE_XML_ID: 'option_custom_body_image',
/**
* @constructor
*/
@@ -76,21 +77,31 @@ var ThemeCustomizeDialog = Dialog.extend({
this.opened().then(function () {
$tabs.eq(self.defaultTab).tab('show');
var $colorPreview = self.$('.o_theme_customize_color_previews:visible');
var $primary = $colorPreview.find('.o_theme_customize_color[data-color="primary"]');
var $alpha = $colorPreview.find('.o_theme_customize_color[data-color="alpha"]');
var $secondary = $colorPreview.find('.o_theme_customize_color[data-color="secondary"]');
var $beta = $colorPreview.find('.o_theme_customize_color[data-color="beta"]');
var sameAlphaPrimary = $primary.find('.o_color_preview').css('background-color') === $alpha.find('.o_color_preview').css('background-color');
var sameBetaSecondary = $secondary.find('.o_color_preview').css('background-color') === $beta.find('.o_color_preview').css('background-color');
// Hack to hide primary/secondary if they are equal to alpha/beta
// (this is the case with default values but not in some themes).
var $primary = self.$('.o_theme_customize_color[data-color="primary"]');
var $alpha = self.$('.o_theme_customize_color[data-color="alpha"]');
var $secondary = self.$('.o_theme_customize_color[data-color="secondary"]');
var $beta = self.$('.o_theme_customize_color[data-color="beta"]');
var sameAlphaPrimary = $primary.css('background-color') === $alpha.css('background-color');
var sameBetaSecondary = $secondary.css('background-color') === $beta.css('background-color');
if (!sameAlphaPrimary) {
$alpha.find('.o_color_name').text(_t("Extra Color"));
$primary.removeClass('d-none').addClass('d-flex');
$alpha.prev().text(_t("Extra Color"));
}
if (!sameBetaSecondary) {
$beta.find('.o_color_name').text(_t("Extra Color"));
$secondary.removeClass('d-none').addClass('d-flex');
$beta.prev().text(_t("Extra Color"));
}
$primary = $primary.closest('.o_theme_customize_option');
$alpha = $alpha.closest('.o_theme_customize_option');
$secondary = $secondary.closest('.o_theme_customize_option');
$beta = $beta.closest('.o_theme_customize_option');
$primary.toggleClass('d-none', sameAlphaPrimary);
$secondary.toggleClass('d-none', sameBetaSecondary);
if (!sameAlphaPrimary && sameBetaSecondary) {
$beta.insertBefore($alpha);
} else if (sameAlphaPrimary && !sameBetaSecondary) {
@@ -105,6 +116,44 @@ var ThemeCustomizeDialog = Dialog.extend({
// Private
//--------------------------------------------------------------------------
/**
* @private
*/
_chooseBodyCustomImage: function () {
var self = this;
var def = $.Deferred();
var $image = $('<img/>');
var editor = new weWidgets.MediaDialog(this, {
onlyImages: true,
firstFilters: ['background'],
}, $image[0]);
editor.on('save', this, function (media) { // TODO use scss customization instead (like for user colors)
var src = $(media).attr('src');
self._rpc({
model: 'ir.model.data',
method: 'get_object_reference',
args: ['website', this.CUSTOM_BODY_IMAGE_XML_ID],
}).then(function (data) {
return self._rpc({
model: 'ir.ui.view',
method: 'save',
args: [
data[1],
'#wrapwrap { background-image: url("' + src + '"); }',
'//style',
],
});
}).always(def.resolve.bind(def));
});
editor.on('cancel', this, function () {
def.resolve();
});
editor.open();
return def;
},
/**
* @private
* @param {Object} data - @see this._loadViews
@@ -150,7 +199,7 @@ var ThemeCustomizeDialog = Dialog.extend({
this.$('[title]').tooltip();
this.$inputs = self.$('[data-xmlid], [data-enable], [data-disable]');
this.$inputs = self.$('.o_theme_customize_option_input');
// Enable data-xmlid="" inputs if none of their neighbors were enabled
_.each(this.$inputs.filter('[data-xmlid=""]'), function (input) {
var $input = $(input);
@@ -175,19 +224,20 @@ var ThemeCustomizeDialog = Dialog.extend({
var xmlid = $item.data('xmlid');
// Build the options template
var $option = $(core.qweb.render('website.theme_customize_modal_option', {
alone: alone,
name: optionsName,
id: $item.attr('id') || _.uniqueId('o_theme_customize_input_id_'),
checked: xmlid && (!_.difference(self._getXMLIDs($item), data.enabled).length),
var renderingOptions = _.extend({
string: $item.attr('string') || data.names[xmlid.split(',')[0].trim()],
icon: $item.data('icon'),
font: $item.data('font'),
}, $item.data());
// Build the options template
var $option = $(core.qweb.render('website.theme_customize_modal_option', _.extend({
alone: alone,
name: xmlid === undefined ? _.uniqueId('option-') : optionsName,
id: $item.attr('id') || _.uniqueId('o_theme_customize_input_id_'),
checked: xmlid === undefined || xmlid && (!_.difference(self._getXMLIDs($item), data.enabled).length),
widget: widgetName,
}));
}, renderingOptions)));
$option.find('input')
.addClass('o_theme_customize_option_input')
.attr({
@@ -198,8 +248,8 @@ var ThemeCustomizeDialog = Dialog.extend({
});
if (widgetName) {
var $widget = $(core.qweb.render('website.theme_customize_' + widgetName));
$option.append($widget);
var $widget = $(core.qweb.render('website.theme_customize_widget_' + widgetName, renderingOptions));
$option.find('label').append($widget);
}
if ($container.hasClass('form-row')) {
@@ -267,51 +317,113 @@ var ThemeCustomizeDialog = Dialog.extend({
/**
* @private
*/
_processChange: function ($inputs) {
_makeSCSSCusto: function (url, values) {
var self = this;
this.$modal.addClass('o_theme_customize_loading');
return this._rpc({ // TODO improve to be more efficient
route: '/web_editor/get_assets_editor_resources',
params: {
'key': 'website.layout',
'get_views': false,
'get_scss': true,
'get_js': false,
'bundles': false,
'bundles_restriction': [],
'only_user_custom_files': false,
},
}).then(function (data) {
var file = _.find(data.scss[0][1], function (file) {
return file.url === url;
});
var bodyCustomImageXMLID = 'option_custom_body_image';
var $inputBodyCustomImage = $inputs.filter('[data-xmlid*="website.' + bodyCustomImageXMLID + '"]:checked');
if (!$inputBodyCustomImage.length) {
return $.when();
}
var updatedFileContent = file.arch;
_.each(values, function (value, name) {
var pattern = _.str.sprintf("'%s': %%s,\n", name);
var regex = new RegExp(_.str.sprintf(pattern, ".+"));
var replacement = _.str.sprintf(pattern, value);
if (regex.test(updatedFileContent)) {
updatedFileContent = updatedFileContent
.replace(regex, replacement);
} else {
updatedFileContent = updatedFileContent
.replace(/( *)(.*hook.*)/, _.str.sprintf('$1%s$1$2', replacement));
}
});
var def = $.Deferred();
var $image = $('<img/>');
var editor = new weWidgets.MediaDialog(this, {
onlyImages: true,
firstFilters: ['background'],
}, $image[0]);
editor.on('save', this, function (media) { // TODO use scss customization instead (like for user colors)
var src = $(media).attr('src');
self._rpc({
model: 'ir.model.data',
method: 'get_object_reference',
args: ['website', bodyCustomImageXMLID],
}).then(function (data) {
return self._rpc({
model: 'ir.ui.view',
method: 'save',
args: [
data[1],
'#wrapwrap { background-image: url("' + src + '"); }',
'//style',
],
});
}).then(function () {
def.resolve();
return self._rpc({
route: '/web_editor/save_scss_or_js',
params: {
'url': file.url,
'bundle_xmlid': 'web.assets_common',
'content': updatedFileContent,
'file_type': 'scss',
},
});
});
editor.on('cancel', this, function () {
def.resolve();
});
},
/**
* @private
*/
_pickColor: function (colorElement) {
var self = this;
var $color = $(colorElement);
var colorName = $color.data('color');
var colorType = $color.data('colorType');
editor.open();
var def = $.Deferred();
var colorpicker = new ColorpickerDialog(this, {
defaultColor: $color.css('background-color'),
});
var chosenColor = undefined;
colorpicker.on('colorpicker:saved', this, function (ev) {
ev.stopPropagation();
chosenColor = ev.data.cssColor;
});
colorpicker.on('closed', this, function (ev) {
if (chosenColor === undefined) {
def.resolve();
return;
}
var baseURL = '/website/static/src/scss/options/colors/';
var url = _.str.sprintf('%suser_%scolor_palette.scss', baseURL, (colorType ? (colorType + '_') : ''));
var colors = {};
colors[colorName] = chosenColor;
if (colorName === 'alpha') {
colors['beta'] = 'null';
colors['gamma'] = 'null';
colors['delta'] = 'null';
colors['epsilon'] = 'null';
}
self._makeSCSSCusto(url, colors).always(def.resolve.bind(def));
});
colorpicker.open();
return def;
},
/**
* @private
*/
_processChange: function ($inputs) {
var self = this;
var defs = [];
// Handle body image changes
var $bodyImageInputs = $inputs.filter('[data-xmlid*="website.' + this.CUSTOM_BODY_IMAGE_XML_ID + '"]:checked');
defs = defs.concat(_.map($bodyImageInputs, function () {
return self._chooseBodyCustomImage();
}));
// Handle color changes
var $colors = $inputs.closest('.o_theme_customize_option').find('.o_theme_customize_color');
defs = defs.concat(_.map($colors, function (colorElement) {
return self._pickColor($(colorElement));
}));
return $.when.apply($, defs);
},
/**
* @private
*/
@@ -319,7 +431,7 @@ var ThemeCustomizeDialog = Dialog.extend({
var self = this;
// Look at all options to see if they are enabled or disabled
var $enable = this.$inputs.filter('[data-xmlid]:checked');
var $enable = this.$inputs.filter(':checked');
// Mark the labels as checked accordingly
this.$('label').removeClass('checked');
@@ -362,7 +474,6 @@ var ThemeCustomizeDialog = Dialog.extend({
return $.Deferred();
}
var self = this;
return this._rpc({
route: '/website/theme_customize',
params: {
@@ -394,9 +505,7 @@ var ThemeCustomizeDialog = Dialog.extend({
});
});
return $.when.apply($, defs).then(function () {
self.$modal.removeClass('o_theme_customize_loading');
});
return $.when.apply($, defs);
});
},
@@ -443,87 +552,17 @@ var ThemeCustomizeDialog = Dialog.extend({
this._setActive();
// Update the style according to the whole set of options
this.$modal.addClass('o_theme_customize_loading');
self._processChange($options).then(function () {
self._updateStyle(
return self._updateStyle(
self._getXMLIDs($enable),
self._getXMLIDs($disable),
$option.data('reload') && window.location.href.match(new RegExp($option.data('reload')))
);
}).always(function () {
self.$modal.removeClass('o_theme_customize_loading');
});
},
/**
* @private
* @param {Event} ev
*/
_onColorClick: function (ev) {
var self = this;
var $color = $(ev.currentTarget);
var colorName = $color.data('color');
var colorType = $color.data('colorType');
var colorpicker = new ColorpickerDialog(this, {
defaultColor: $color.find('.o_color_preview').css('background-color'),
});
colorpicker.on('colorpicker:saved', this, function (ev) {
ev.stopPropagation();
// TODO improve to be more efficient
self._rpc({
route: '/web_editor/get_assets_editor_resources',
params: {
'key': 'website.layout',
'get_views': false,
'get_scss': true,
'get_js': false,
'bundles': false,
'bundles_restriction': [],
'only_user_custom_files': false,
},
}).then(function (data) {
var files = data.scss[0][1];
var file = _.find(files, function (file) {
var baseURL = '/website/static/src/scss/options/colors/';
return file.url === _.str.sprintf('%suser_%scolor_palette.scss', baseURL, (colorType ? (colorType + '_') : ''));
});
var colors = {};
colors[colorName] = ev.data.cssColor;
if (colorName === 'alpha') {
colors['beta'] = 'null';
colors['gamma'] = 'null';
colors['delta'] = 'null';
colors['epsilon'] = 'null';
}
var updatedFileContent = file.arch;
_.each(colors, function (colorValue, colorName) {
var pattern = _.str.sprintf("'%s': %%s,\n", colorName);
var regex = new RegExp(_.str.sprintf(pattern, ".+"));
var replacement = _.str.sprintf(pattern, colorValue);
if (regex.test(updatedFileContent)) {
updatedFileContent = updatedFileContent
.replace(regex, replacement);
} else {
updatedFileContent = updatedFileContent
.replace(/( *)(.*hook.*)/, _.str.sprintf('$1%s$1$2', replacement));
}
});
return self._rpc({
route: '/web_editor/save_scss_or_js',
params: {
'url': file.url,
'bundle_xmlid': 'web.assets_common',
'content': updatedFileContent,
'file_type': 'scss',
},
});
}).then(function () {
return self._updateStyle();
});
});
colorpicker.open();
},
});
var ThemeCustomizeMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
@@ -71,25 +71,40 @@
+ label {
@include o-position-absolute(0, 0, 0, 0);
width: auto;
background: transparent;
background: transparent !important;
}
}
label {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
min-height: 24px;
margin: 0;
padding: 8px;
border: 2px solid transparent;
background-color: $o-brand-lightsecondary;
text-align: center;
font-size: 14px;
line-height: 1;
cursor: pointer;
&:hover::after {
content: "";
pointer-events: none;
@include o-position-absolute(0, 0, 0, 0);
background-color: rgba(0, 32, 64, 0.03);
}
&.checked {
border-color: $o-brand-odoo;
color: $o-brand-odoo;
}
&.o_switch {
justify-content: flex-start;
}
}
$i: 1;
@@ -103,57 +118,59 @@
}
$i: $i + 1;
}
}
.o_theme_customize_color_previews {
h6, a {
font-size: 12px;
margin: 4px 0 0;
}
&.o_theme_customize_with_widget {
label {
justify-content: flex-start;
text-align: left;
.o_theme_customize_color {
> .o_color_preview {
position: relative;
display: inline-block;
width: 48px;
height: 27px;
border: 1px solid black;
&::before, &::after {
content: "";
@include o-position-absolute(0, 0, 0, 0);
}
&::before {
background-image: url('/web_editor/static/src/img/transparent.png');
}
&::after {
background-color: inherit;
&.checked {
border-color: transparent;
color: inherit;
}
}
}
}
$palette: nth($o-color-palettes, $o-color-palette-number);
$-text-color: if(map-get($palette, 'text'), map-get($palette, 'text'), #212529); // BS default as fallback (TODO find real value)
$-h1-color: if(map-get($palette, 'h1'), map-get($palette, 'h1'), $-text-color);
$-h2-color: if(map-get($palette, 'h2'), map-get($palette, 'h2'), $-h1-color);
$-h3-color: if(map-get($palette, 'h3'), map-get($palette, 'h3'), $-h2-color);
$-h4-color: if(map-get($palette, 'h4'), map-get($palette, 'h4'), $-h3-color);
$-h5-color: if(map-get($palette, 'h5'), map-get($palette, 'h5'), $-h4-color);
$-h6-color: if(map-get($palette, 'h6'), map-get($palette, 'h6'), $-h5-color);
$-defaults: (
'body': white, // BS default as fallback (TODO find real value)
'text': $-text-color,
'h1': $-h1-color,
'h2': $-h2-color,
'h3': $-h3-color,
'h4': $-h4-color,
'h5': $-h5-color,
'h6': $-h6-color,
);
.o_theme_customize_color {
position: relative;
width: 30px;
height: 20px;
margin-left: auto;
border: 1px solid black;
@each $name, $value in map-merge($-defaults, o-map-omit($palette)) {
&.o_theme_customize_color_#{$name} > .o_color_preview {
background-color: $value;
}
&::before, &::after {
content: "";
@include o-position-absolute(0, 0, 0, 0);
}
&::before {
background-image: url('/web_editor/static/src/img/transparent.png');
}
&::after {
background-color: inherit;
}
$palette: nth($o-color-palettes, $o-color-palette-number);
$-text-color: if(map-get($palette, 'text'), map-get($palette, 'text'), #212529); // BS default as fallback (TODO find real value)
$-h1-color: if(map-get($palette, 'h1'), map-get($palette, 'h1'), $-text-color);
$-h2-color: if(map-get($palette, 'h2'), map-get($palette, 'h2'), $-h1-color);
$-h3-color: if(map-get($palette, 'h3'), map-get($palette, 'h3'), $-h2-color);
$-h4-color: if(map-get($palette, 'h4'), map-get($palette, 'h4'), $-h3-color);
$-h5-color: if(map-get($palette, 'h5'), map-get($palette, 'h5'), $-h4-color);
$-h6-color: if(map-get($palette, 'h6'), map-get($palette, 'h6'), $-h5-color);
$-defaults: (
'body': white, // BS default as fallback (TODO find real value)
'text': $-text-color,
'h1': $-h1-color,
'h2': $-h2-color,
'h3': $-h3-color,
'h4': $-h4-color,
'h5': $-h5-color,
'h6': $-h6-color,
);
@each $name, $value in map-merge($-defaults, o-map-omit($palette)) {
&.o_theme_customize_color_#{$name} {
background-color: $value;
}
}
}
@@ -37,58 +37,21 @@
<div class="form-row justify-content-between o_options_container"/>
</div>
<t t-name="website.theme_customize_modal_option">
<div t-attf-class="o_theme_customize_option my-1 flex-grow-0 #{font ? 'o_theme_customize_option_font_' + font : ''}">
<div t-attf-class="o_theme_customize_option my-1 flex-grow-0 #{font ? 'o_theme_customize_option_font_' + font : ''} #{widget ? 'o_theme_customize_with_widget' : ''}">
<img t-if="icon" t-att-src="icon"/>
<t t-set="label" t-value="font ? '' : string"/>
<t t-set="_classes" t-value="widget ? 'd-none' : 'd-flex'"/>
<t t-if="alone and !widget" t-call="website.components.switch"/>
<label t-else="" t-attf-class="#{_classes} align-items-center justify-content-center text-center">
<label t-else="">
<input t-att-id="id" t-att-name="name" type="radio" t-att-checked="checked ? 'checked' : undefined"/>
<span><t t-esc="label"/></span>
</label>
</div>
</t>
<t t-name="website.theme_customize_color_palette">
<div class="o_theme_customize_color_previews">
<div>
<h6>Main colors</h6>
<t t-foreach="{'primary': _t('Primary'), 'secondary': _t('Secondary'), 'alpha': _t('Primary'), 'beta': _t('Secondary'), 'gamma': _t('Extra Color'), 'delta': _t('Extra Color'), 'epsilon': _t('Extra Color')}" t-as="color">
<t t-call="website.theme_customize_color_palette_button">
<t t-set="color_type" t-value="'theme'"/>
</t>
</t>
</div>
<div>
<h6>Background colors</h6>
<t t-foreach="{'body': _t('Body'), 'menu': _t('Menus'), 'footer': _t('Footer')}" t-as="color">
<t t-call="website.theme_customize_color_palette_button"/>
</t>
</div>
<div>
<h6>Text colors</h6>
<t t-foreach="{'text': _t('Text'), 'h1': _t('Headings 1'), 'h2': _t('Headings 2'), 'h3': _t('Headings 3'), 'h4': _t('Headings 4'), 'h5': _t('Headings 5'), 'h6': _t('Headings 6')}" t-as="color">
<t t-call="website.theme_customize_color_palette_button"/>
</t>
</div>
<div>
<h6>State colors</h6>
<t t-foreach="{'success': _t('Success'), 'info': _t('Info'), 'warning': _t('Warning'), 'danger': _t('Error')}" t-as="color">
<t t-call="website.theme_customize_color_palette_button">
<t t-set="color_type" t-value="'theme'"/>
</t>
</t>
</div>
</div>
</t>
<t t-name="website.theme_customize_color_palette_button">
<button type="button"
t-attf-class="btn btn-secondary #{color == 'primary' or color == 'secondary' ? 'd-none' : 'd-flex'} align-items-center w-100 mt-2 justify-content-between o_theme_customize_color o_theme_customize_color_#{color}"
<t t-name="website.theme_customize_widget_color">
<div t-attf-class="bg-#{color} o_theme_customize_color o_theme_customize_color_#{color}"
t-att-data-color="color"
t-att-data-color-type="color_type">
<span class="o_color_name"><t t-esc="color_value"/></span>
<span t-attf-class="o_color_preview bg-#{color}"/>
</button>
t-att-data-color-type="colorType"/>
</t>
<t t-extend="wysiwyg.widgets.link">
+29 -1
View File
@@ -485,7 +485,35 @@
<div>
<!-- Color options -->
<content id="theme_customize_content_colors" string="Colors" title="Choose the theme colors">
<opt data-widget="color_palette" data-col="12"/>
<list string="Main colors">
<opt data-widget="color" data-color-type="theme" data-color="primary" string="Primary"/>
<opt data-widget="color" data-color-type="theme" data-color="secondary" string="Secondary"/>
<opt data-widget="color" data-color-type="theme" data-color="alpha" string="Primary"/>
<opt data-widget="color" data-color-type="theme" data-color="beta" string="Secondary"/>
<opt data-widget="color" data-color-type="theme" data-color="gamma" string="Extra Color"/>
<opt data-widget="color" data-color-type="theme" data-color="delta" string="Extra Color"/>
<opt data-widget="color" data-color-type="theme" data-color="epsilon" string="Extra Color"/>
</list>
<list string="Status colors">
<opt data-widget="color" data-color-type="theme" data-color="success" string="Success"/>
<opt data-widget="color" data-color-type="theme" data-color="info" string="Info"/>
<opt data-widget="color" data-color-type="theme" data-color="warning" string="Warning"/>
<opt data-widget="color" data-color-type="theme" data-color="danger" string="Error"/>
</list>
<list string="Text colors">
<opt data-widget="color" data-color="text" string="Text"/>
<opt data-widget="color" data-color="h1" string="Headings 1"/>
<opt data-widget="color" data-color="h2" string="Headings 2"/>
<opt data-widget="color" data-color="h3" string="Headings 3"/>
<opt data-widget="color" data-color="h4" string="Headings 4"/>
<opt data-widget="color" data-color="h5" string="Headings 5"/>
<opt data-widget="color" data-color="h6" string="Headings 6"/>
</list>
<list string="Background colors">
<opt data-widget="color" data-color="body" string="Body"/>
<opt data-widget="color" data-color="menu" string="Menus"/>
<opt data-widget="color" data-color="footer" string="Footer"/>
</list>
</content>
<!-- Menu options -->