diff --git a/addons/portal/static/src/scss/portal.scss b/addons/portal/static/src/scss/portal.scss
index 350df828cd1..11016434184 100644
--- a/addons/portal/static/src/scss/portal.scss
+++ b/addons/portal/static/src/scss/portal.scss
@@ -5,6 +5,7 @@
// ====== Variables =========
$-navbar-height: $nav-link-height !default;
+$o-theme-navbar-logo-height: $-navbar-height !default;
// Portal toolbar (filters, search bar)
$o-portal-mobile-toolbar: true; // Enable/Disable custom design
@@ -42,15 +43,23 @@ $o-portal-use-default-colors: $body-bg == $o-portal-default-body-bg;
header {
.navbar-brand {
flex: 0 0 auto;
+ max-width: 75%;
&.logo {
padding-top: 0;
padding-bottom: 0;
img {
+ // object-fit does not work on IE but is only used as a fallback
+ object-fit: contain;
+ display: block;
width: auto;
- height: $-navbar-height;
- max-width: none;
+ height: $o-theme-navbar-logo-height;
+
+ @include media-breakpoint-down(sm) {
+ height: auto;
+ max-height: min($o-theme-navbar-logo-height, 5rem);
+ }
}
}
}
diff --git a/addons/portal/static/src/scss/primary_variables.scss b/addons/portal/static/src/scss/primary_variables.scss
index 68a1664e93c..da994350c97 100644
--- a/addons/portal/static/src/scss/primary_variables.scss
+++ b/addons/portal/static/src/scss/primary_variables.scss
@@ -1,3 +1,5 @@
$o-portal-default-body-bg: #FCFCFC;
+$o-theme-navbar-logo-height: null;
+
$o-theme-btn-icon-hover-decoration: none;
diff --git a/addons/web/static/src/scss/utils.scss b/addons/web/static/src/scss/utils.scss
index 5ac917fcd75..49485067037 100644
--- a/addons/web/static/src/scss/utils.scss
+++ b/addons/web/static/src/scss/utils.scss
@@ -177,11 +177,11 @@
// Mixin which allows to extend the BS4 bg-variant mixin
@mixin o-bg-color-extension($color, $text-color, $with-muted) {}
-// Function to remove all falsy values of a map
+// Function to remove all null values of a map
@function o-map-omit($map) {
$-map: ();
@each $key, $value in $map {
- @if $value {
+ @if $value != null {
$-map: map-merge($-map, (
$key: $value,
));
diff --git a/addons/website/static/src/js/widgets/theme.js b/addons/website/static/src/js/widgets/theme.js
index e118366d009..89bab6bb058 100644
--- a/addons/website/static/src/js/widgets/theme.js
+++ b/addons/website/static/src/js/widgets/theme.js
@@ -4,6 +4,7 @@ odoo.define('website.theme', function (require) {
var config = require('web.config');
var core = require('web.core');
var Dialog = require('web.Dialog');
+var Widget = require('web.Widget');
var weWidgets = require('wysiwyg.widgets');
var ColorpickerDialog = require('wysiwyg.widgets.ColorpickerDialog');
var websiteNavbarData = require('website.navbar');
@@ -12,6 +13,96 @@ var _t = core._t;
var templateDef = null;
+var QuickEdit = Widget.extend({
+ xmlDependencies: ['/website/static/src/xml/website.editor.xml'],
+ template: 'website.theme_customize_active_input',
+ events: {
+ 'keydown input': '_onInputKeydown',
+ 'click .btn-primary': '_onSaveClick',
+ 'click .btn-secondary': '_onResetClick',
+ },
+
+ /**
+ * @constructor
+ */
+ init: function (parent, value, unit) {
+ this._super.apply(this, arguments);
+ this.value = value;
+ this.unit = unit;
+ },
+ /**
+ * @override
+ */
+ start: function () {
+ this.$input = this.$('input');
+ this.$input.select();
+ return this._super.apply(this, arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {string} [value]
+ */
+ _save: function (value) {
+ if (value === undefined) {
+ value = parseFloat(this.$input.val());
+ value = isNaN(value) ? 'null' : (value + this.unit);
+ }
+ this.trigger_up('QuickEdit:save', {
+ value: value,
+ });
+ this.destroy();
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {Event} ev
+ */
+ _onInputKeydown: function (ev) {
+ var inputValue = this.$input.val();
+ var value = 0;
+ if (inputValue !== '') {
+ value = parseFloat(this.$input.val());
+ if (isNaN(value)) {
+ return;
+ }
+ }
+ switch (ev.which) {
+ case $.ui.keyCode.UP:
+ this.$input.val(value + 1);
+ break;
+ case $.ui.keyCode.DOWN:
+ this.$input.val(value - 1);
+ break;
+ case $.ui.keyCode.ENTER:
+ // Do not listen to change events, we want the user to be able
+ // to confirm in all cases.
+ this._save();
+ break;
+ }
+ },
+ /**
+ * @private
+ */
+ _onSaveClick: function () {
+ this._save();
+ },
+ /**
+ * @private
+ */
+ _onResetClick: function () {
+ this._save('null');
+ },
+});
+
var ThemeCustomizeDialog = Dialog.extend({
xmlDependencies: (Dialog.prototype.xmlDependencies || [])
.concat(['/website/static/src/xml/website.editor.xml']),
@@ -60,8 +151,12 @@ var ThemeCustomizeDialog = Dialog.extend({
start: function () {
var self = this;
+ this.PX_BY_REM = parseFloat($(document.documentElement).css('font-size'));
+
this.$modal.addClass('o_theme_customize_modal');
+ this.style = window.getComputedStyle(document.documentElement);
+
var $tabs;
var loadDef = this._loadViews().then(function (data) {
self._generateDialogHTML(data);
@@ -209,6 +304,7 @@ var ThemeCustomizeDialog = Dialog.extend({
}
});
this._setActive();
+ this._updateValues();
function _processItems($items, $container) {
var optionsName = _.uniqueId('option-');
@@ -412,6 +508,8 @@ var ThemeCustomizeDialog = Dialog.extend({
var self = this;
var defs = [];
+ var $options = $inputs.closest('.o_theme_customize_option');
+
// Handle body image changes
var $bodyImageInputs = $inputs.filter('[data-xmlid*="website.' + this.CUSTOM_BODY_IMAGE_XML_ID + '"]:checked');
defs = defs.concat(_.map($bodyImageInputs, function () {
@@ -419,13 +517,46 @@ var ThemeCustomizeDialog = Dialog.extend({
}));
// Handle color changes
- var $colors = $inputs.closest('.o_theme_customize_option').find('.o_theme_customize_color');
+ var $colors = $options.find('.o_theme_customize_color');
defs = defs.concat(_.map($colors, function (colorElement) {
return self._pickColor($(colorElement));
}));
+ // Handle input changes
+ var $inputsData = $options.find('.o_theme_customize_input');
+ defs = defs.concat(_.map($inputsData, function (inputData, i) {
+ return self._quickEdit($(inputData));
+ }));
+
return $.when.apply($, defs);
},
+ /**
+ * @private
+ */
+ _quickEdit: function ($inputData) {
+ var text = $inputData.text().trim();
+ var value = parseFloat(text) || '';
+ var unit = text.match(/([^\s\d]+)$/)[1];
+
+ var def = $.Deferred();
+ var qEdit = new QuickEdit(this, value, unit);
+ qEdit.on('QuickEdit:save', this, function (ev) {
+ ev.stopPropagation();
+
+ var value = ev.data.value;
+ // Convert back to rem if needed
+ if ($inputData.data('unit') === 'rem' && unit === 'px' && value !== 'null') {
+ value = parseFloat(value) / this.PX_BY_REM + 'rem';
+ }
+
+ var values = {};
+ values[$inputData.data('value')] = value;
+ this._makeSCSSCusto('/website/static/src/scss/options/user_values.scss', values)
+ .always(def.resolve.bind(def));
+ });
+ qEdit.appendTo($inputData.closest('.o_theme_customize_option'));
+ return def;
+ },
/**
* @private
*/
@@ -466,6 +597,8 @@ var ThemeCustomizeDialog = Dialog.extend({
* @private
*/
_updateStyle: function (enable, disable, reload) {
+ var self = this;
+
var $loading = $('', {class: 'fa fa-refresh fa-spin'});
this.$modal.find('.modal-title').append($loading);
@@ -509,11 +642,49 @@ var ThemeCustomizeDialog = Dialog.extend({
$links.last().after($newLinks);
return linksLoaded;
});
-
return $.when.apply($, defs).always(function () {
$loading.remove();
$allLinks.remove();
});
+ }).then(function () {
+ // Some animations may depend on the variables that were
+ // customized, so we have to restart them.
+ self.trigger_up('animation_start_demand');
+ });
+ },
+ /**
+ * @private
+ */
+ _updateValues: function () {
+ var self = this;
+ // Put user values
+ _.each(this.$('.o_theme_customize_color'), function (el) {
+ var $el = $(el);
+ var value = self.style.getPropertyValue('--' + $el.data('color')).trim();
+ $el.css('background-color', value);
+ });
+ _.each(this.$('.o_theme_customize_input'), function (el) {
+ var $el = $(el);
+ var value = self.style.getPropertyValue('--' + $el.data('value')).trim();
+
+ // Convert rem values to px values
+ if (_.str.endsWith(value, 'rem')) {
+ value = parseFloat(value) * self.PX_BY_REM + 'px';
+ }
+
+ var $span = $el.find('span');
+ $span.removeClass().text('');
+ switch (value) {
+ case '':
+ case 'false':
+ case 'true':
+ // When null or a boolean value, shows an icon which tells
+ // the user that there is no numeric/text value
+ $span.addClass('fa fa-ban text-danger');
+ break;
+ default:
+ $span.text(value);
+ }
});
},
@@ -572,6 +743,7 @@ var ThemeCustomizeDialog = Dialog.extend({
$option.data('reload') && window.location.href.match(new RegExp($option.data('reload')))
);
}).then(function () {
+ self._updateValues();
self.$inputs.prop('disabled', false);
});
},
diff --git a/addons/website/static/src/scss/options/user_values.scss b/addons/website/static/src/scss/options/user_values.scss
new file mode 100644
index 00000000000..3b6899ad091
--- /dev/null
+++ b/addons/website/static/src/scss/options/user_values.scss
@@ -0,0 +1,7 @@
+// This file is meant to be edited automatically by the user. The variables it
+// contains should not be renamed otherwise it would break existing customers
+// customizations.
+
+$o-user-website-values: map-merge($o-user-website-values, o-map-omit((
+ // -- hook --
+)));
diff --git a/addons/website/static/src/scss/primary_variables.scss b/addons/website/static/src/scss/primary_variables.scss
index 835acc6b3f9..f469cae36ad 100644
--- a/addons/website/static/src/scss/primary_variables.scss
+++ b/addons/website/static/src/scss/primary_variables.scss
@@ -40,6 +40,21 @@ $o-social-colors: (
$o-theme-figcaption-opacity: 0.6;
+//------------------------------------------------------------------------------
+// Website customizations
+//------------------------------------------------------------------------------
+
+$o-website-values-palettes: (
+ (
+ 'logo-height': null, // Default to navbar height (see portal)
+ ),
+) !default;
+$o-website-values-palette-number: 1 !default;
+
+// By default, all user website values are null. Each null value is
+// automatically replaced with corresponsing value of chosen values palette.
+$o-user-website-values: () !default;
+
//------------------------------------------------------------------------------
// Fonts
//------------------------------------------------------------------------------
diff --git a/addons/website/static/src/scss/secondary_variables.scss b/addons/website/static/src/scss/secondary_variables.scss
index 517a541c99a..168965c56f1 100644
--- a/addons/website/static/src/scss/secondary_variables.scss
+++ b/addons/website/static/src/scss/secondary_variables.scss
@@ -34,6 +34,20 @@ $o-theme-color-palettes: append($o-theme-color-palettes, map-merge($-palette-def
// Enable last color and theme color palettes, which are now the user customized
-// color palettes
+// color palettes.
$o-color-palette-number: length($o-color-palettes);
$o-theme-color-palette-number: length($o-theme-color-palettes);
+
+//------------------------------------------------------------------------------
+// Website customizations
+//------------------------------------------------------------------------------
+
+$-website-values-default: nth($o-website-values-palettes, $o-website-values-palette-number);
+$o-website-values: map-merge($-website-values-default, o-map-omit($o-user-website-values)) !default;
+$o-website-values-palettes: append($o-website-values-palettes, $o-website-values);
+$o-website-values-palette-number: length($o-website-values-palettes);
+@function o-website-value($key) {
+ @return map-get($o-website-values, $key);
+}
+
+$o-theme-navbar-logo-height: o-website-value('logo-height') !default;
diff --git a/addons/website/static/src/scss/website.edit_mode.scss b/addons/website/static/src/scss/website.edit_mode.scss
index 2d0254beb2b..664dcf037d0 100644
--- a/addons/website/static/src/scss/website.edit_mode.scss
+++ b/addons/website/static/src/scss/website.edit_mode.scss
@@ -138,28 +138,37 @@
&::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;
+ .o_theme_customize_input {
+ flex: 1 1 auto;
+ text-align: right;
+
+ .fa-edit {
+ visibility: hidden;
+ color: $o-brand-primary;
+ }
+ }
+ .o_theme_customize_option:hover .o_theme_customize_input .fa-edit {
+ visibility: visible;
+ }
+
+ .o_theme_customize_active_input {
+ @include o-position-absolute(0, 0, 0, 0);
+ width: auto;
+ padding: 3px;
+
+ > * {
+ height: 100% !important;
+
+ &.form-control {
+ background: white !important;
+ }
+
+ &.form-control, .btn {
+ padding: 2px 8px !important;
+ text-align: right !important;
+ font-size: $font-size-sm !important;
}
}
}
diff --git a/addons/website/static/src/scss/website.scss b/addons/website/static/src/scss/website.scss
index 58b34312a27..706b25ac34a 100644
--- a/addons/website/static/src/scss/website.scss
+++ b/addons/website/static/src/scss/website.scss
@@ -19,6 +19,41 @@ $-font-numbers: (
}
}
+:root {
+ // The theme customize modal JS will need to know the value of some scss
+ // variables used to render the user website, and those may have been
+ // customized by themes, the user or anything else (so there is no file to
+ // parse to get them). Those will be printed here as CSS variables.
+
+ // 1) The values in the $theme-colors map are already printed by Bootstrap.
+
+ // 2) The values in the $colors map are also printed by Bootstrap.
+
+ // 3) The Odoo values map, $o-website-values, must be printed.
+ @each $key, $value in $o-website-values {
+ --#{$key}: #{$value};
+ }
+
+ // Handle keys which can be null but default to other values
+ $-h1-color: if($headings-color == inherit, $body-color, $headings-color);
+ $-h2-color: if(color('h2'), color('h2'), $-h1-color);
+ $-h3-color: if(color('h3'), color('h3'), $-h2-color);
+ $-h4-color: if(color('h4'), color('h4'), $-h3-color);
+ $-h5-color: if(color('h5'), color('h5'), $-h4-color);
+ $-h6-color: if(color('h6'), color('h6'), $-h5-color);
+
+ --body: #{$body-bg};
+ --text: #{$body-color};
+ --h1: #{$-h1-color};
+ --h2: #{$-h2-color};
+ --h3: #{$-h3-color};
+ --h4: #{$-h4-color};
+ --h5: #{$-h5-color};
+ --h6: #{$-h6-color};
+
+ --logo-height: #{$o-theme-navbar-logo-height};
+}
+
#wrapwrap {
background-size: cover;
background-repeat: no-repeat;
@@ -64,6 +99,17 @@ $-font-numbers: (
}
}
+@if $o-theme-navbar-logo-height {
+ // With default values, this makes it slightly bigger than standard
+ // navbar-brand, which is what we want
+ header .navbar-brand {
+ font-size: $o-theme-navbar-logo-height / $line-height-base;
+ $-logo-padding-y: max(0, $nav-link-height - $o-theme-navbar-logo-height) / 2;
+ padding-top: $-logo-padding-y;
+ padding-bottom: $-logo-padding-y;
+ }
+}
+
.o_footer {
@if color('footer') {
@include o-bg-color(color('footer'));
diff --git a/addons/website/static/src/xml/website.editor.xml b/addons/website/static/src/xml/website.editor.xml
index d02d82eb5aa..2f8473d90c3 100644
--- a/addons/website/static/src/xml/website.editor.xml
+++ b/addons/website/static/src/xml/website.editor.xml
@@ -49,10 +49,28 @@
-
+
+
+
+
+
+
+
+
+
diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml
index cf437b281ef..d8650d7656d 100644
--- a/addons/website/views/website_templates.xml
+++ b/addons/website/views/website_templates.xml
@@ -4,6 +4,9 @@
+
+
+
@@ -524,6 +527,7 @@
+