summaryrefslogtreecommitdiff
path: root/addons/website/static/src
diff options
context:
space:
mode:
Diffstat (limited to 'addons/website/static/src')
-rw-r--r--addons/website/static/src/js/content/menu.js35
-rw-r--r--addons/website/static/src/js/menu/customize.js6
-rw-r--r--addons/website/static/src/js/utils.js19
-rw-r--r--addons/website/static/src/js/widgets/theme.js582
-rw-r--r--addons/website/static/src/scss/options/user_values.scss7
-rw-r--r--addons/website/static/src/scss/primary_variables.scss16
-rw-r--r--addons/website/static/src/scss/secondary_variables.scss16
-rw-r--r--addons/website/static/src/scss/website.edit_mode.scss129
-rw-r--r--addons/website/static/src/scss/website.scss67
-rw-r--r--addons/website/static/src/xml/website.editor.xml73
10 files changed, 646 insertions, 304 deletions
diff --git a/addons/website/static/src/js/content/menu.js b/addons/website/static/src/js/content/menu.js
index d64a3891bfd..7e06140f657 100644
--- a/addons/website/static/src/js/content/menu.js
+++ b/addons/website/static/src/js/content/menu.js
@@ -3,6 +3,7 @@ odoo.define('website.content.menu', function (require) {
var dom = require('web.dom');
var sAnimation = require('website.content.snippets.animation');
+var wUtils = require('website.utils');
sAnimation.registry.affixMenu = sAnimation.Class.extend({
selector: 'header.o_affix_enabled',
@@ -23,6 +24,11 @@ sAnimation.registry.affixMenu = sAnimation.Class.extend({
this.$dropdowns = this.$headers.find('.dropdown');
this.$navbarCollapses = this.$headers.find('.navbar-collapse');
+ this._adaptDefaultOffset();
+ wUtils.onceAllImagesLoaded(this.$headerClone).then(function () {
+ self._adaptDefaultOffset();
+ });
+
// Handle events for the collapse menus
_.each(this.$headerClone.find('[data-toggle="collapse"]'), function (el) {
var $source = $(el);
@@ -50,6 +56,24 @@ sAnimation.registry.affixMenu = sAnimation.Class.extend({
},
//--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ */
+ _adaptDefaultOffset: function () {
+ var bottom = this.$target.offset().top + this._getHeaderHeight();
+ this.$headerClone.css('margin-top', Math.min(-200, -bottom) + 'px');
+ },
+ /**
+ * @private
+ */
+ _getHeaderHeight: function () {
+ return this.$headerClone.outerHeight();
+ },
+
+ //--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
@@ -92,16 +116,7 @@ sAnimation.registry.autohideMenu = sAnimation.Class.extend({
this.noAutohide = this.$el.closest('.o_no_autohide_menu').length;
if (!this.noAutohide) {
var $navbar = this.$el.closest('.navbar');
- _.each($navbar.find('img'), function (img) {
- if (img.complete) {
- return; // Already loaded
- }
- var def = $.Deferred();
- defs.push(def);
- $(img).one('load', function () {
- def.resolve();
- });
- });
+ defs.push(wUtils.onceAllImagesLoaded($navbar));
// The previous code will make sure we wait for images to be fully
// loaded before initializing the auto more menu. But in some cases,
diff --git a/addons/website/static/src/js/menu/customize.js b/addons/website/static/src/js/menu/customize.js
index fc254626dd2..aced336de2c 100644
--- a/addons/website/static/src/js/menu/customize.js
+++ b/addons/website/static/src/js/menu/customize.js
@@ -81,6 +81,12 @@ var CustomizeMenu = Widget.extend({
},
}).then(function (result) {
var currentGroup = '';
+ if (result.length) {
+ $menu.append($('<div/>', {
+ class: 'dropdown-divider',
+ role: 'separator',
+ }));
+ }
_.each(result, function (item) {
if (currentGroup !== item.inherit_id[1]) {
currentGroup = item.inherit_id[1];
diff --git a/addons/website/static/src/js/utils.js b/addons/website/static/src/js/utils.js
index 9cf511fe160..01f0e7797a4 100644
--- a/addons/website/static/src/js/utils.js
+++ b/addons/website/static/src/js/utils.js
@@ -60,6 +60,24 @@ function autocompleteWithPages(self, $input) {
},
});
}
+
+/**
+ * @param {jQuery} $element
+ */
+function onceAllImagesLoaded($element) {
+ var defs = _.map($element.find('img').addBack('img'), function (img) {
+ if (img.complete) {
+ return; // Already loaded
+ }
+ var def = $.Deferred();
+ $(img).one('load', function () {
+ def.resolve();
+ });
+ return def;
+ });
+ return $.when.apply($, defs);
+}
+
/**
* @deprecated
* @todo create Dialog.prompt instead of this
@@ -168,6 +186,7 @@ function prompt(options, _qweb) {
return {
loadAnchors: loadAnchors,
autocompleteWithPages: autocompleteWithPages,
+ onceAllImagesLoaded: onceAllImagesLoaded,
prompt: prompt,
};
});
diff --git a/addons/website/static/src/js/widgets/theme.js b/addons/website/static/src/js/widgets/theme.js
index 36f59a23762..a5e0731e4b0 100644
--- a/addons/website/static/src/js/widgets/theme.js
+++ b/addons/website/static/src/js/widgets/theme.js
@@ -3,26 +3,118 @@ 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 Widget = require('web.Widget');
var weWidgets = require('wysiwyg.widgets');
+var ColorpickerDialog = require('wysiwyg.widgets.ColorpickerDialog');
var websiteNavbarData = require('website.navbar');
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']),
template: 'website.theme_customize',
events: {
- 'change [data-xmlid], [data-enable], [data-disable]': '_onChange',
- 'click .checked [data-xmlid], .checked [data-enable], .checked [data-disable]': '_onChange',
- 'click .o_theme_customize_color': '_onColorClick',
+ 'change .o_theme_customize_option_input': '_onChange',
+ 'click .checked .o_theme_customize_option_input[type="radio"]': '_onChange',
},
+ CUSTOM_BODY_IMAGE_XML_ID: 'option_custom_body_image',
+
/**
* @constructor
*/
@@ -58,29 +150,53 @@ var ThemeCustomizeDialog = Dialog.extend({
*/
start: function () {
var self = this;
- this._generateDialogHTML();
+
+ 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);
+ $tabs = self.$('[data-toggle="tab"]');
+
+ // Hide the tab navigation if only one tab
+ if ($tabs.length <= 1) {
+ $tabs.closest('.nav').addClass('d-none');
+ }
+ });
+
// Enable the first option tab or the given default tab
- var $tabs = this.$('[data-toggle="tab"]');
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) {
@@ -88,17 +204,7 @@ var ThemeCustomizeDialog = Dialog.extend({
}
});
- // Hide the tab navigation if only one tab
- if ($tabs.length <= 1) {
- $tabs.closest('.nav').addClass('d-none');
- }
-
- this.$inputs = this.$('[data-xmlid], [data-enable], [data-disable]');
-
- return $.when(
- this._super.apply(this, arguments),
- this._loadViews()
- );
+ return $.when(this._super.apply(this, arguments), loadDef);
},
//--------------------------------------------------------------------------
@@ -108,7 +214,47 @@ var ThemeCustomizeDialog = Dialog.extend({
/**
* @private
*/
- _generateDialogHTML: function () {
+ _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
+ */
+ _generateDialogHTML: function (data) {
+ var self = this;
var $contents = this.$el.children('content');
if ($contents.length === 0) {
return;
@@ -148,8 +294,21 @@ var ThemeCustomizeDialog = Dialog.extend({
this.$('[title]').tooltip();
+ 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);
+ var $neighbors = self.$inputs.filter('[name="' + $input.attr('name') + '"]').not($input);
+ if ($neighbors.length && !$neighbors.filter(':checked').length) {
+ $input.prop('checked', true);
+ }
+ });
+ this._setActive();
+ this._updateValues();
+
function _processItems($items, $container) {
var optionsName = _.uniqueId('option-');
+ var alone = ($items.length === 1);
_.each($items, function (item) {
var $item = $(item);
@@ -159,37 +318,47 @@ var ThemeCustomizeDialog = Dialog.extend({
case 'OPT':
var widgetName = $item.data('widget');
- // Build the options template
- var $option = $(core.qweb.render('website.theme_customize_modal_option', {
- name: optionsName,
- id: $item.attr('id') || _.uniqueId('o_theme_customize_input_id_'),
+ var xmlid = $item.data('xmlid');
- string: $item.attr('string'),
+ var renderingOptions = _.extend({
+ string: $item.attr('string') || data.names[xmlid.split(',')[0].trim()],
icon: $item.data('icon'),
font: $item.data('font'),
+ }, $item.data());
- xmlid: $item.data('xmlid'),
- enable: $item.data('enable'),
- disable: $item.data('disable'),
- reload: $item.data('reload'),
-
+ // 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({
+ 'data-xmlid': xmlid,
+ 'data-enable': $item.data('enable'),
+ 'data-disable': $item.data('disable'),
+ 'data-reload': $item.data('reload'),
+ });
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);
}
+ var $final;
if ($container.hasClass('form-row')) {
- $col = $('<div/>', {
+ $final = $('<div/>', {
class: _.str.sprintf('col-%s', $item.data('col') || 6),
});
- $col.append($option);
- $container.append($col);
+ $final.append($option);
} else {
- $container.append($option);
+ $final = $option;
}
+ $final.attr('data-depends', $item.data('depends'));
+ $container.append($final);
break;
case 'LIST':
@@ -201,6 +370,10 @@ var ThemeCustomizeDialog = Dialog.extend({
$container.append($col);
_processItems($item.children(), $listContainer);
break;
+
+ default:
+ _processItems($item.children(), $container);
+ break;
}
});
}
@@ -209,29 +382,11 @@ var ThemeCustomizeDialog = Dialog.extend({
* @private
*/
_loadViews: function () {
- var self = this;
return this._rpc({
route: '/website/theme_customize_get',
params: {
- xml_ids: this._getXMLIDs(this.$inputs),
+ 'xml_ids': this._getXMLIDs(this.$inputs || this.$('[data-xmlid]')),
},
- }).done(function (data) {
- self.$inputs.prop('checked', false);
- _.each(self.$inputs.filter('[data-xmlid]:not([data-xmlid=""])'), function (input) {
- var $input = $(input);
- if (!_.difference(self._getXMLIDs($input), data[0]).length) {
- $input.prop('checked', true);
- }
- });
- _.each(self.$inputs.filter('[data-xmlid=""]'), function (input) {
- var $input = $(input);
- if (!self.$inputs.filter('[name="' + $input.attr('name') + '"]:checked').length) {
- $input.prop('checked', true);
- }
- });
- self._setActive();
- }).fail(function (d, error) {
- Dialog.alert(this, error.data.message);
});
},
/**
@@ -260,63 +415,126 @@ var ThemeCustomizeDialog = Dialog.extend({
/**
* @private
*/
- _processChange: function ($inputs) {
+ _makeSCSSCusto: function (url, values) {
+ return this._rpc({
+ route: '/website/make_scss_custo',
+ params: {
+ 'url': url,
+ 'values': values,
+ },
+ });
+ },
+ /**
+ * @private
+ */
+ _pickColor: function (colorElement) {
var self = this;
- this.$modal.addClass('o_theme_customize_loading');
-
- var bodyCustomImageXMLID = 'option_custom_body_image';
- var $inputBodyCustomImage = $inputs.filter('[data-xmlid*="website.' + bodyCustomImageXMLID + '"]:checked');
- if (!$inputBodyCustomImage.length) {
- return $.when();
- }
+ var $color = $(colorElement);
+ var colorName = $color.data('color');
+ var colorType = $color.data('colorType');
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();
- });
+ var colorpicker = new ColorpickerDialog(this, {
+ defaultColor: $color.css('background-color'),
});
- editor.on('cancel', this, function () {
- def.resolve();
+ 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;
+ }
- editor.open();
+ 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 = [];
+
+ 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 () {
+ return self._chooseBodyCustomImage();
+ }));
+
+ // Handle color changes
+ 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
+ */
_setActive: function () {
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');
- $enable.closest('label').addClass('checked');
+ $enable.closest('label:not(.o_switch)').addClass('checked');
// Mark the option sets as checked if all their option are checked/unchecked
var $sets = this.$inputs.filter('[data-enable], [data-disable]').not('[data-xmlid]');
@@ -329,15 +547,21 @@ var ThemeCustomizeDialog = Dialog.extend({
if (self._getInputs($set.data('disable')).filter(':checked').length) {
checked = false;
}
- $set.prop('checked', checked).closest('label').toggleClass('checked', checked);
+ $set.prop('checked', checked).closest('label:not(.o_switch)').toggleClass('checked', checked);
});
// Make the hidden sections visible if their dependencies are met
_.each(this.$('[data-depends]'), function (hidden) {
var $hidden = $(hidden);
var depends = $hidden.data('depends');
- var nbDependencies = depends ? depends.split(',').length : 0;
- var enabled = self._getInputs(depends).filter(':checked').length === nbDependencies;
+ var dependencies = depends ? depends.split(/\s*,\s*/g) : [];
+ var enabled = _.all(dependencies, function (dep) {
+ var toBeChecked = (dep[0] !== '!');
+ if (!toBeChecked) {
+ dep = dep.substr(1);
+ }
+ return self._getInputs(dep).is(':checked') === toBeChecked;
+ });
$hidden.toggleClass('d-none', !enabled);
});
},
@@ -345,6 +569,11 @@ var ThemeCustomizeDialog = Dialog.extend({
* @private
*/
_updateStyle: function (enable, disable, reload) {
+ var self = this;
+
+ var $loading = $('<i/>', {class: 'fa fa-refresh fa-spin'});
+ this.$modal.find('.modal-title').append($loading);
+
if (reload || config.debug === 'assets') {
window.location.href = $.param.querystring('/website/theme_customize_reload', {
href: window.location.href,
@@ -355,18 +584,19 @@ var ThemeCustomizeDialog = Dialog.extend({
return $.Deferred();
}
- var self = this;
return this._rpc({
route: '/website/theme_customize',
params: {
- enable: enable,
- disable: disable,
- get_bundle: true,
+ 'enable': enable,
+ 'disable': disable,
+ 'get_bundle': true,
},
}).then(function (bundles) {
+ var $allLinks = $();
var defs = _.map(bundles, function (bundleContent, bundleName) {
var linkSelector = 'link[href*="' + bundleName + '"]';
var $links = $(linkSelector);
+ $allLinks = $allLinks.add($links);
var $newLinks = $(bundleContent).filter(linkSelector);
var linksLoaded = $.Deferred();
@@ -382,14 +612,51 @@ var ThemeCustomizeDialog = Dialog.extend({
window.location.reload();
});
$links.last().after($newLinks);
- return linksLoaded.then(function () {
- $links.remove();
- });
+ return linksLoaded;
});
-
- return $.when.apply($, defs).then(function () {
- self.$modal.removeClass('o_theme_customize_loading');
+ 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);
+ }
});
},
@@ -406,6 +673,11 @@ var ThemeCustomizeDialog = Dialog.extend({
// Checkout the option that changed
var $option = $(ev.currentTarget);
+ if ($option.is(':disabled')) {
+ return;
+ }
+ this.$inputs.prop('disabled', true);
+
var $options = $option;
var checked = $option.is(':checked');
@@ -437,91 +709,21 @@ var ThemeCustomizeDialog = Dialog.extend({
// Update the style according to the whole set of options
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')))
);
+ }).then(function () {
+ self._updateValues();
+ self.$inputs.prop('disabled', false);
});
},
- /**
- * @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({
actions: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.actions || {}, {
- customize_theme: '_openThemeCustomizeDialog',
+ 'customize_theme': '_openThemeCustomizeDialog',
}),
/**
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..72bb93ba8e6 100644
--- a/addons/website/static/src/scss/primary_variables.scss
+++ b/addons/website/static/src/scss/primary_variables.scss
@@ -41,6 +41,22 @@ $o-social-colors: (
$o-theme-figcaption-opacity: 0.6;
//------------------------------------------------------------------------------
+// Website customizations
+//------------------------------------------------------------------------------
+
+$o-website-values-palettes: (
+ (
+ 'logo-height': null, // Default to navbar height (see portal)
+ 'header-font-size': null, // Default to BS (normal font-size)
+ ),
+) !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 85ee8dc8fd4..664dcf037d0 100644
--- a/addons/website/static/src/scss/website.edit_mode.scss
+++ b/addons/website/static/src/scss/website.edit_mode.scss
@@ -6,17 +6,6 @@
}
.modal.o_theme_customize_modal {
-
- &.o_theme_customize_loading {
- pointer-events: none;
-
- &::after {
- content: "";
- @include o-position-absolute(0, 0, 0, 0);
- background-color: rgba(0, 0, 0, 0.2);
- }
- }
-
.modal-dialog {
@include o-position-absolute(0, 2%);
width: 96%;
@@ -71,25 +60,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 +107,68 @@
}
$i: $i + 1;
}
+
+ &.o_theme_customize_with_widget {
+ label {
+ justify-content: flex-start;
+ text-align: left;
+
+ &.checked {
+ border-color: transparent;
+ color: inherit;
+ }
+ }
+ }
}
- .o_theme_customize_color_previews {
- h6, a {
- font-size: 12px;
- margin: 4px 0 0;
+ .o_theme_customize_color {
+ position: relative;
+ width: 30px;
+ height: 20px;
+ margin-left: auto;
+ 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;
+ }
+ }
- .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;
- }
+ .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;
}
- $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} > .o_color_preview {
- background-color: $value;
- }
+ &.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 71619d3aaa3..2aec4f03251 100644
--- a/addons/website/static/src/scss/website.scss
+++ b/addons/website/static/src/scss/website.scss
@@ -19,6 +19,44 @@ $-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) Handle default values
+ --header-font-size: #{$font-size-base};
+
+ // 2) The values in the $theme-colors map are already printed by Bootstrap.
+
+ // 3) The values in the $colors map are also printed by Bootstrap.
+
+ // 4) The Odoo values map, $o-website-values, must be printed.
+ @each $key, $value in $o-website-values {
+ --#{$key}: #{$value};
+ }
+
+ // 5) Use final value used by the theme
+ $-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 +102,31 @@ $-font-numbers: (
}
}
+$-header-nav-link-height: $nav-link-height;
+@if o-website-value('header-font-size') {
+ $-header-nav-link-height: o-website-value('header-font-size') * $line-height-base + $nav-link-padding-y * 2;
+ header {
+ font-size: o-website-value('header-font-size');
+
+ .dropdown-menu {
+ font-size: inherit;
+ }
+ }
+}
+@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, $-header-nav-link-height - $o-theme-navbar-logo-height) / 2;
+ &, &.logo {
+ padding-top: $-logo-padding-y;
+ padding-bottom: $-logo-padding-y;
+ }
+ }
+}
+
.o_footer {
@if color('footer') {
@include o-bg-color(color('footer'));
@@ -985,10 +1048,10 @@ table.table_desc tr td {
position: fixed;
z-index: $zindex-modal - 20;
background: $light;
- margin-top: -200px;
+ margin-top: -999px;
transition: margin-top 500ms ease 0s;
&.affixed {
- margin-top: 0px;
+ margin-top: 0px !important; // the default margin-top is adapted in JS
}
}
#oe_main_menu_navbar + #wrapwrap .o_header_affix {
diff --git a/addons/website/static/src/xml/website.editor.xml b/addons/website/static/src/xml/website.editor.xml
index 46d5de05c12..2f8473d90c3 100644
--- a/addons/website/static/src/xml/website.editor.xml
+++ b/addons/website/static/src/xml/website.editor.xml
@@ -22,9 +22,9 @@
<!-- Custom checkbox (material-design-like toggle) -->
<t t-name="website.components.switch">
<label class="o_switch" t-att-for="id">
- <input type="checkbox" t-att-id="id"/>
+ <input type="checkbox" t-att-id="id" t-att-checked="checked ? 'checked' : undefined"/>
<span/>
- <t t-if="label"><t t-esc="label"/></t>
+ <div t-if="label"><t t-esc="label"/></div>
</label>
</t>
@@ -37,54 +37,39 @@
<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"/>
- <label t-attf-class="#{widget ? 'd-none' : 'd-flex'} align-items-center justify-content-center text-center">
- <input t-att-id="id" t-att-name="name" type="radio" class="o_theme_customize_option_input" t-att-data-xmlid="xmlid" t-att-data-enable="enable" t-att-data-disable="disable" t-att-data-reload="reload"/>
- <span><t t-esc="string"/></span>
+
+ <t t-set="label" t-value="font ? '' : string"/>
+ <t t-if="alone and !widget" t-call="website.components.switch"/>
+ <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>
+ <t t-name="website.theme_customize_widget_color">
+ <div t-attf-class="o_theme_customize_color o_theme_customize_color_#{color}"
+ t-att-data-color="color"
+ t-att-data-color-type="colorType"/>
+ </t>
+ <t t-name="website.theme_customize_widget_input">
+ <div t-attf-class="o_theme_customize_input o_theme_customize_input_#{value}"
+ t-att-data-value="value"
+ t-att-data-unit="unit">
+ <i class="fa fa-edit"/>
+ <span/>
</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-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 t-name="website.theme_customize_active_input">
+ <div class="input-group input-group-sm align-items-center o_theme_customize_active_input">
+ <input type="text" class="form-control" t-att-value="widget.value"/>
+ <div class="input-group-append">
+ <div t-if="widget.unit" class="input-group-text"><t t-esc="widget.unit"/></div>
+ <button type="button" class="btn btn-primary fa fa-check" title="Save"/>
+ <button type="button" class="btn btn-secondary fa fa-undo" title="Reset"/>
+ </div>
+ </div>
</t>
<t t-extend="wysiwyg.widgets.link">