odoo.define('website.theme', function (require) { 'use strict'; var config = require('web.config'); var core = require('web.core'); var ColorpickerDialog = require('web.colorpicker'); var Dialog = require('web.Dialog'); var widgets = require('web_editor.widget'); var websiteNavbarData = require('website.navbar'); var _t = core._t; var templateDef = 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', }, /** * @constructor */ init: function (parent, options) { options = options || {}; this._super(parent, _.extend({ title: _t("Customize this theme"), buttons: [], }, options)); this.defaultTab = options.tab || 0; }, /** * @override */ willStart: function () { if (templateDef === null) { templateDef = this._rpc({ model: 'ir.ui.view', method: 'read_template', args: ['website.theme_customize'], }).then(function (data) { return core.qweb.add_template(data); }); } return $.when(this._super.apply(this, arguments), templateDef); }, /** * @override */ start: function () { var self = this; this._generateDialogHTML(); this.$modal.addClass('o_theme_customize_modal'); // 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'); if (!sameAlphaPrimary) { $alpha.find('.o_color_name').text(_t("Extra Color")); $primary.removeClass('d-none').addClass('d-flex'); } if (!sameBetaSecondary) { $beta.find('.o_color_name').text(_t("Extra Color")); $secondary.removeClass('d-none').addClass('d-flex'); } if (!sameAlphaPrimary && sameBetaSecondary) { $beta.insertBefore($alpha); } else if (sameAlphaPrimary && !sameBetaSecondary) { $secondary.insertAfter($alpha); } }); // 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() ); }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- /** * @private */ _generateDialogHTML: function () { var $contents = this.$el.children('content'); if ($contents.length === 0) { return; } $contents.remove(); this.$el.append(core.qweb.render('website.theme_customize_modal_layout')); var $navLinksContainer = this.$('.nav'); var $navContents = this.$('.tab-content'); _.each($contents, function (content) { var $content = $(content); var contentID = _.uniqueId('content-'); // Build the nav tab for the content $navLinksContainer.append($('
  • ', { class: 'nav-item mb-1', }).append($('', { href: '#' + contentID, class: 'nav-link', 'data-toggle': 'tab', text: $content.attr('string'), }))); // Build the tab pane for the content var $navContent = $(core.qweb.render('website.theme_customize_modal_content', { id: contentID, title: $content.attr('title'), })); $navContents.append($navContent); var $optionsContainer = $navContent.find('.o_options_container'); // Process content items _processItems($content.children(), $optionsContainer); }); this.$('[title]').tooltip(); function _processItems($items, $container) { var optionsName = _.uniqueId('option-'); _.each($items, function (item) { var $item = $(item); var $col; switch (item.tagName) { 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_'), string: $item.attr('string'), icon: $item.data('icon'), font: $item.data('font'), xmlid: $item.data('xmlid'), enable: $item.data('enable'), disable: $item.data('disable'), reload: $item.data('reload'), widget: widgetName, })); if (widgetName) { var $widget = $(core.qweb.render('website.theme_customize_' + widgetName)); $option.append($widget); } if ($container.hasClass('form-row')) { $col = $('
    ', { class: _.str.sprintf('col-%s', $item.data('col') || 6), }); $col.append($option); $container.append($col); } else { $container.append($option); } break; case 'LIST': var $listContainer = $('
    ', {class: 'py-1 px-2 o_theme_customize_option_list'}); $col = $('
    ', { class: _.str.sprintf('col-%s mt-2', $item.data('col') || 6), 'data-depends': $item.data('depends'), }).append($('
    ', {text: $item.attr('string')}), $listContainer); $container.append($col); _processItems($item.children(), $listContainer); break; } }); } }, /** * @private */ _loadViews: function () { var self = this; return this._rpc({ route: '/website/theme_customize_get', params: { xml_ids: this._getXMLIDs(this.$inputs), }, }).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); }); }, /** * @private */ _getInputs: function (string) { if (!string) { return $(); } return this.$inputs.filter('#' + string.replace(/\s*,\s*/g, ', #')); }, /** * @private */ _getXMLIDs: function ($inputs) { var xmlIDs = []; _.each($inputs, function (input) { var $input = $(input); var xmlID = $input.data('xmlid'); if (xmlID) { xmlIDs = xmlIDs.concat(xmlID.split(/\s*,\s*/)); } }); return xmlIDs; }, /** * @private */ _processChange: function ($inputs) { 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 def = $.Deferred(); var $image = $(''); var editor = new widgets.MediaDialog(this, {onlyImages: true, firstFilters: ['background']}, null, $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(); }); }); editor.on('cancel', this, function () { def.resolve(); }); editor.open(); 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'); // Mark the labels as checked accordingly this.$('label').removeClass('checked'); $enable.closest('label').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]'); _.each($sets, function (set) { var $set = $(set); var checked = true; if (self._getInputs($set.data('enable')).not(':checked').length) { checked = false; } if (self._getInputs($set.data('disable')).filter(':checked').length) { checked = false; } $set.prop('checked', checked).closest('label').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; $hidden.toggleClass('d-none', !enabled); }); }, /** * @private */ _updateStyle: function (enable, disable, reload) { if (reload || config.debug === 'assets') { window.location.href = $.param.querystring('/website/theme_customize_reload', { href: window.location.href, enable: (enable || []).join(','), disable: (disable || []).join(','), tab: this.$('.nav-link.active').parent().index(), }); return $.Deferred(); } var self = this; return this._rpc({ route: '/website/theme_customize', params: { enable: enable, disable: disable, get_bundle: true, }, }).then(function (bundles) { var defs = _.map(bundles, function (bundleContent, bundleName) { var linkSelector = 'link[href*="' + bundleName + '"]'; var $links = $(linkSelector); var $newLinks = $(bundleContent).filter(linkSelector); var linksLoaded = $.Deferred(); var nbLoaded = 0; $newLinks.on('load', function (e) { if (++nbLoaded >= $newLinks.length) { linksLoaded.resolve(); } }); $newLinks.on('error', function (e) { linksLoaded.reject(); window.location.hash = 'theme=true'; window.location.reload(); }); $links.last().after($newLinks); return linksLoaded.then(function () { $links.remove(); }); }); return $.when.apply($, defs).then(function () { self.$modal.removeClass('o_theme_customize_loading'); }); }); }, //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- /** * @private * @param {Event} ev */ _onChange: function (ev) { var self = this; // Checkout the option that changed var $option = $(ev.currentTarget); var $options = $option; var checked = $option.is(':checked'); // If it was enabled, enable/disable the related input (see data-enable, // data-disable) and retain the ones that actually changed if (checked) { var $inputs; // Input to enable $inputs = this._getInputs($option.data('enable')); $options = $options.add($inputs.filter(':not(:checked)')); $inputs.prop('checked', true); // Input to disable $inputs = this._getInputs($option.data('disable')); $options = $options.add($inputs.filter(':checked')); $inputs.prop('checked', false); } var optionNames = _.uniq(_.map($options, function (option) { return option.name; })); $options = this.$inputs.filter(function (i, input) { return _.contains(optionNames, input.name); }); // Look at all options to see if they are enabled or disabled var $enable = $options.filter('[data-xmlid]:checked'); var $disable = $options.filter('[data-xmlid]:not(:checked)'); this._setActive(); // Update the style according to the whole set of options self._processChange($options).then(function () { self._updateStyle( self._getXMLIDs($enable), self._getXMLIDs($disable), $option.data('reload') && window.location.href.match(new RegExp($option.data('reload'))) ); }); }, /** * @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, bundles: false, bundles_restriction: [], }, }).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', params: { url: file.url, bundle_xmlid: 'web.assets_common', content: updatedFileContent, }, }); }).then(function () { return self._updateStyle(); }); }); colorpicker.open(); }, }); var ThemeCustomizeMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({ actions: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.actions || {}, { customize_theme: '_openThemeCustomizeDialog', }), /** * Automatically opens the theme customization dialog if the corresponding * hash is in the page URL. * * @override */ start: function () { if ((window.location.hash || '').indexOf('theme=true') > 0) { var tab = window.location.hash.match(/tab=(\d+)/); this._openThemeCustomizeDialog(tab ? tab[1] : false); window.location.hash = ''; } return this._super.apply(this, arguments); }, //-------------------------------------------------------------------------- // Actions //-------------------------------------------------------------------------- /** * Instantiates and opens the theme customization dialog. * * @private * @param {string} tab * @returns {Deferred} */ _openThemeCustomizeDialog: function (tab) { return new ThemeCustomizeDialog(this, {tab: tab}).open(); }, }); websiteNavbarData.websiteNavbarRegistry.add(ThemeCustomizeMenu, '#theme_customize'); return ThemeCustomizeDialog; });