From 3bb666c226e60eec54f695061bb50fae379239a2 Mon Sep 17 00:00:00 2001 From: Samuel Degueldre Date: Wed, 23 Oct 2019 10:32:28 +0000 Subject: [PATCH] [REF] website, web_editor: move theme customization to edit mode Previously, theme customization was done through a modal dialog in the website module, this was not very user friendly since the rest of the customization for the website design was done from edit mode, meaning the user had to exit edit mode to customize things such as theme colors or fonts. It was also rather confusing to have these seemingly related things in completely unrelated places. This commit moves all of the options that used to be in the theme customization modal into a new tab in the editor's left panel. This commit is mainly just about rendering the old modal as a third left panel content. See next commit for deeper changes. Note: same as before, those changes do apply the color/size/layout immediately on the website, even if not saved. Changing that behavior is complex and might be the job of another task. Part of https://github.com/odoo/odoo/pull/41166 task-2088298 --- .../static/src/js/editor/snippets.editor.js | 60 +++-- addons/website/controllers/main.py | 5 - .../static/src/js/editor/snippets.editor.js | 52 ++++ addons/website/static/src/js/widgets/theme.js | 245 ++++++++---------- .../static/src/scss/website.editor.ui.scss | 64 +---- .../website/static/src/xml/website.editor.xml | 8 +- addons/website/views/assets.xml | 1 + addons/website/views/snippets/snippets.xml | 6 + addons/website/views/website_templates.xml | 15 +- 9 files changed, 224 insertions(+), 232 deletions(-) create mode 100644 addons/website/static/src/js/editor/snippets.editor.js diff --git a/addons/web_editor/static/src/js/editor/snippets.editor.js b/addons/web_editor/static/src/js/editor/snippets.editor.js index b8ab92a3d8d..50a14ae6cba 100644 --- a/addons/web_editor/static/src/js/editor/snippets.editor.js +++ b/addons/web_editor/static/src/js/editor/snippets.editor.js @@ -779,6 +779,7 @@ var SnippetsMenu = Widget.extend({ cacheSnippetTemplate: {}, events: { 'click .o_install_btn': '_onInstallBtnClick', + 'click .o_we_add_snippet_btn': '_onBlocksTabClick', 'click .o_we_invisible_entry': '_onInvisibleEntryClick', 'click #snippet_custom .o_delete_btn': '_onDeleteBtnClick', }, @@ -804,6 +805,11 @@ var SnippetsMenu = Widget.extend({ 'user_value_widget_opening': '_onUserValueWidgetOpening', 'reload_snippet_template': '_onReloadSnippetTemplate', }, + // enum of the SnippetsMenu's tabs. + tabs: { + BLOCKS: 'blocks', + OPTIONS: 'options', + }, /** * @param {Widget} parent @@ -896,10 +902,6 @@ var SnippetsMenu = Widget.extend({ }); var $target = $(srcElement); - if ($target.closest('#snippets_menu').length) { - this._activateSnippet(false); - return; - } if (!$target.closest('body > *').length) { return; } @@ -1833,6 +1835,32 @@ var SnippetsMenu = Widget.extend({ return this.nodeType === 3 && this.textContent.match(/\S/); }).parent().addClass('o_default_snippet_text'); }, + /** + * Changes the content of the left panel and selects a tab. + * + * @private + * @param {htmlString | Element | Text | Array | jQuery} [content] + * the new content of the customizePanel + * @param {this.tabs.VALUE} [tab='blocks'] - the tab to select + */ + _updateLeftPanelContent: function ({content, tab}) { + this._closeWidgets(); + + tab = tab || this.tabs.BLOCKS; + + while (this.customizePanel.firstChild) { + this.customizePanel.removeChild(this.customizePanel.firstChild); + } + if (content) { + $(this.customizePanel).append(content); + } + + this.$('#o_scroll').toggleClass('d-none', tab !== this.tabs.BLOCKS); + this.customizePanel.classList.toggle('d-none', tab === this.tabs.BLOCKS); + + this.$('.o_we_add_snippet_btn').toggleClass('active', tab === this.tabs.BLOCKS); + this.$('.o_we_customize_snippet_btn').toggleClass('active', tab === this.tabs.OPTIONS); + }, //-------------------------------------------------------------------------- // Handlers @@ -2000,6 +2028,16 @@ var SnippetsMenu = Widget.extend({ .toggleClass('fa-eye-slash', !isVisible); return this._activateSnippet(isVisible ? $snippet : false); }, + /** + * @private + */ + _onBlocksTabClick: function (ev) { + this._activateSnippet(false); + this._updateLeftPanelContent({ + content: [], + tab: this.tabs.BLOCKS, + }); + }, /** * @private */ @@ -2099,18 +2137,10 @@ var SnippetsMenu = Widget.extend({ * @param {OdooEvent} ev */ _onUpdateCustomizeElements: function (ev) { - this._closeWidgets(); - while (this.customizePanel.firstChild) { - this.customizePanel.removeChild(this.customizePanel.firstChild); - } - ev.data.customize$Elements.forEach($el => { - this.customizePanel.appendChild($el[0]); + this._updateLeftPanelContent({ + content: ev.data.customize$Elements, + tab: ev.data.customize$Elements.length ? this.tabs.OPTIONS : this.tabs.BLOCKS, }); - var customize = !!ev.data.customize$Elements.length; - this.$('#o_scroll').toggleClass('d-none', customize); - this.$('.o_we_add_snippet_btn').toggleClass('active', !customize); - this.customizePanel.classList.toggle('d-none', !customize); - this.$('.o_we_customize_snippet_btn').toggleClass('active', customize); }, /** * Called when an user value widget is being opened -> close all the other diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py index 83c0df51ff8..2ebc4900a6c 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -384,11 +384,6 @@ class Website(Home): return True - @http.route(['/website/theme_customize_reload'], type='http', auth="public", website=True) - def theme_customize_reload(self, href, enable, disable, tab=0, **kwargs): - self.theme_customize(enable and enable.split(",") or [], disable and disable.split(",") or []) - return request.redirect(href + ("&theme=true" if "#" in href else "#theme=true") + ("&tab=" + tab)) - @http.route(['/website/make_scss_custo'], type='json', auth='user', website=True) def make_scss_custo(self, url, values): """ diff --git a/addons/website/static/src/js/editor/snippets.editor.js b/addons/website/static/src/js/editor/snippets.editor.js new file mode 100644 index 00000000000..b7776a3df13 --- /dev/null +++ b/addons/website/static/src/js/editor/snippets.editor.js @@ -0,0 +1,52 @@ +odoo.define('website.snippet.editor', function (require) { +'use strict'; + +const weSnippetEditor = require('web_editor.snippet.editor'); +const ThemeCustomizationMenu = require('website.theme'); + +weSnippetEditor.Class.include({ + events: _.extend({}, weSnippetEditor.Class.prototype.events, { + 'click .o_we_customize_theme_btn': '_onThemeTabClick', + }), + tabs: _.extend({}, weSnippetEditor.Class.prototype.tabs, { + THEME: 'theme', + }), + + /** + * @override + */ + start: function () { + const prom1 = this._super(...arguments); + this.themeCustomizationMenu = new ThemeCustomizationMenu(this); + const prom2 = this.themeCustomizationMenu.appendTo(document.createDocumentFragment()); + return Promise.all([prom1, prom2]); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _updateLeftPanelContent: function ({content, tab}) { + this._super(...arguments); + this.$('.o_we_customize_theme_btn').toggleClass('active', tab === this.tabs.THEME); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onThemeTabClick: function (ev) { + this._activateSnippet(false); + this._updateLeftPanelContent({ + content: this.themeCustomizationMenu.el, + tab: this.tabs.THEME, + }); + }, +}); +}); diff --git a/addons/website/static/src/js/widgets/theme.js b/addons/website/static/src/js/widgets/theme.js index ab05221e8b0..7d976ac4136 100644 --- a/addons/website/static/src/js/widgets/theme.js +++ b/addons/website/static/src/js/widgets/theme.js @@ -6,7 +6,6 @@ var core = require('web.core'); var Dialog = require('web.Dialog'); var Widget = require('web.Widget'); var weWidgets = require('wysiwyg.widgets'); -var websiteNavbarData = require('website.navbar'); var ColorPaletteDialog = require('web_editor.ColorPalette').ColorPaletteDialog; const ColorpickerDialog = require('web.ColorpickerDialog'); @@ -107,11 +106,10 @@ var QuickEdit = Widget.extend({ }, }); -var ThemeCustomizeDialog = Dialog.extend({ - xmlDependencies: (Dialog.prototype.xmlDependencies || []) - .concat(['/website/static/src/xml/website.editor.xml']), +var ThemeCustomizationMenu = Widget.extend({ + xmlDependencies: ['/website/static/src/xml/website.editor.xml'], - template: 'website.theme_customize', + className: 'd-flex flex-column align-items-start o_theme_customization_menu p-2', events: { 'change .o_theme_customize_option_input': '_onChange', 'click .checked .o_theme_customize_option_input[type="radio"]': '_onChange', @@ -124,14 +122,8 @@ var ThemeCustomizeDialog = Dialog.extend({ /** * @constructor */ - init: function (parent, options) { - options = options || {}; - this._super(parent, _.extend({ - title: _t("Customize Theme"), - buttons: [], - }, options)); - - this.defaultTab = options.tab || 0; + init: function (parent) { + this._super(...arguments); this.fontVariables = []; }, /** @@ -156,69 +148,19 @@ var ThemeCustomizeDialog = Dialog.extend({ * @override */ start: function () { - var self = this; - this.PX_BY_REM = parseFloat($(document.documentElement).css('font-size')); - this.$modal.addClass('o_theme_customize_modal'); - this.$modal.find('.modal-footer').append($('', { - href: '/web#action=website.theme_install_kanban_action', - text: _t("Choose another theme..."), - })); - this.style = window.getComputedStyle(document.documentElement); this.nbFonts = parseInt(this.style.getPropertyValue('--number-of-fonts')); var googleFontsProperty = this.style.getPropertyValue('--google-fonts').trim(); this.googleFonts = googleFontsProperty ? googleFontsProperty.split(/\s*,\s*/g) : []; - 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'); - } + var loadDef = this._loadViews().then(data => { + this._generateDialogHTML(data); + this._removeDuplicateColors(); }); - // Enable the first option tab or the given default tab - this.opened().then(function () { - $tabs.eq(self.defaultTab).tab('show'); - - // 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.prev().text(_t("Extra Color")); - } - if (!sameBetaSecondary) { - $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) { - $secondary.insertAfter($alpha); - } - }); - - return Promise.all([this._super.apply(this, arguments), loadDef]); + return Promise.all([this._super(...arguments), loadDef]); }, //-------------------------------------------------------------------------- @@ -270,38 +212,21 @@ var ThemeCustomizeDialog = Dialog.extend({ */ _generateDialogHTML: function (data) { var self = this; - var $contents = this.$el.children('content'); + var $contents = $(core.qweb.render('website.theme_customize')).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); + const $content = $(content); + const contentID = _.uniqueId('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', { + const $menuSection = $(core.qweb.render('website.theme_customize_section', { id: contentID, - title: $content.attr('title'), + title: $content.attr('string'), })); - $navContents.append($navContent); - var $optionsContainer = $navContent.find('.o_options_container'); + self.$el.append($menuSection); + var $optionsContainer = $menuSection.find('.o_options_container'); // Process content items _processItems($content.children(), $optionsContainer, false); @@ -350,7 +275,7 @@ var ThemeCustomizeDialog = Dialog.extend({ } // Build the options template - $element = $(core.qweb.render('website.theme_customize_modal_option', _.extend({ + $element = $(core.qweb.render('website.theme_customize_menu_option', _.extend({ alone: alone, name: xmlid === undefined && widgetName !== 'auto' ? _.uniqueId('option-') : optionsName, id: $item.attr('id') || _.uniqueId('o_theme_customize_input_id_'), @@ -380,7 +305,7 @@ var ThemeCustomizeDialog = Dialog.extend({ break; case 'LIST': - $element = $('
    ', {class: 'py-1 px-2 o_theme_customize_option_list'}); + $element = $('
    ', {class: 'py-1 o_theme_customize_option_list'}); _processItems($item.children(), $element, false); break; @@ -418,6 +343,21 @@ var ThemeCustomizeDialog = Dialog.extend({ }))); break; + case 'A': + $element = $item.clone(); + $element.on('click', (ev) => { + ev.preventDefault(); + Dialog.confirm(self, _t("Switching theme cannot be done directly from edit mode, click ok to save your current changes and go to the theme selection page."), { + confirm_callback: () => self.trigger_up('request_save', { + reload: false, + onSuccess: function () { + window.location.href = ev.target.href; + }, + }), + }); + }); + break; + default: _processItems($item.children(), $container, false); return; @@ -425,7 +365,7 @@ var ThemeCustomizeDialog = Dialog.extend({ if ($container.hasClass('form-row')) { var $col = $('
    ', { - class: _.str.sprintf('col-%s', $item.data('col') || 6), + class: _.str.sprintf('col-%s', $item.data('col') || 12), }); if (item.tagName === 'LIST') { @@ -620,6 +560,42 @@ var ThemeCustomizeDialog = Dialog.extend({ qEdit.appendTo($inputData.closest('.o_theme_customize_option')); }); }, + /** + * Hides primary/secondary if they are equal to alpha/beta + * (this is the case with default values but not in some themes). + * + * @private + */ + _removeDuplicateColors: function () { + var $primary = this.$el.find('.o_theme_customize_color[data-color="primary"]'); + var $alpha = this.$el.find('.o_theme_customize_color[data-color="alpha"]'); + var $secondary = this.$el.find('.o_theme_customize_color[data-color="secondary"]'); + var $beta = this.$el.find('.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.prev().text(_t("Extra Color")); + } + if (!sameBetaSecondary) { + $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) { + $secondary.insertAfter($alpha); + } + }, /** * @private */ @@ -711,17 +687,37 @@ var ThemeCustomizeDialog = Dialog.extend({ _updateStyle: function (enable, disable, reload) { var self = this; - var $loading = $('', {class: 'fa fa-refresh fa-spin'}); - this.$modal.find('.modal-title').append($loading); - if (reload || config.isDebug('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 new Promise(resolve => { + const confirm = Dialog.confirm(self, _t("This change needs to reload the page, this will save all your changes and reload the page, are you sure you want to proceed?") + + (config.isDebug('assets') ? _t(" It appears you are in debug=assets mode, all theme customization options require a page reload in this mode.") : ''), { + confirm_callback: () => resolve(true), + }); + confirm.on('closed', self, () => resolve(false)); + }).then(save => { + if (!save) { + if (this.$lastToggled.attr('type') === 'checkbox') { + this.$lastToggled.prop('checked', !this.$lastToggled.prop('checked')); + } + return; + } + self.trigger_up('request_save', { + reload: false, + onSuccess: () => { + this._rpc({ + route: '/website/theme_customize', + params: { + 'enable': enable, + 'disable': disable, + 'get_bundle': true, + }, + }).then(() => { + window.location.href = window.location.origin + window.location.pathname + '?enable_editor=1'; + }); + }, + }); + return new Promise(resolve => {}); }); - return Promise.resolve(); } return this._rpc({ @@ -762,10 +758,8 @@ var ThemeCustomizeDialog = Dialog.extend({ return linksLoaded; }); return Promise.all(defs).then(function () { - $loading.remove(); $allLinks.remove(); }).guardedCatch(function () { - $loading.remove(); $allLinks.remove(); }); }).then(function () { @@ -838,6 +832,7 @@ var ThemeCustomizeDialog = Dialog.extend({ * @param {Event} ev */ _onChange: function (ev) { + this.$lastToggled = $(ev.target); var self = this; // Checkout the option that changed @@ -957,43 +952,5 @@ var ThemeCustomizeDialog = Dialog.extend({ }, }); -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 {Promise} - */ - _openThemeCustomizeDialog: function (tab) { - return new ThemeCustomizeDialog(this, {tab: tab}).open(); - }, -}); - -websiteNavbarData.websiteNavbarRegistry.add(ThemeCustomizeMenu, '#theme_customize'); - -return ThemeCustomizeDialog; +return ThemeCustomizationMenu; }); diff --git a/addons/website/static/src/scss/website.editor.ui.scss b/addons/website/static/src/scss/website.editor.ui.scss index 1d1728714dc..f7c516dbb59 100644 --- a/addons/website/static/src/scss/website.editor.ui.scss +++ b/addons/website/static/src/scss/website.editor.ui.scss @@ -4,48 +4,7 @@ } } -.modal.o_theme_customize_modal { - .modal-dialog { - @include o-position-absolute(0, 2%); - width: 96%; - max-width: 550px; - } - .modal-header { - background-color: $o-brand-odoo; - color: white; - border-radius: 0; - - * { - color: inherit; - } - } - - .close:hover, .close:focus { - color: white; - } - - .nav { - padding: 0; - - .nav-item { - background-color: $o-brand-lightsecondary; - - .nav-link { - color: $o-main-text-color; - - &.active { - font-weight: bold; - background-color: white; - border: 1px solid #d9d7d7; - border-left: 1px solid #333333; - } - &:hover, &:focus { - text-decoration: none; - } - } - } - } - +.o_theme_customization_menu { .dropdown-menu { position: static !important; transform: none !important; @@ -73,7 +32,6 @@ width: 100%; min-height: 24px; margin: 0; - padding: 8px; text-align: center; font-size: 14px; line-height: 1; @@ -90,14 +48,9 @@ justify-content: flex-start; } } - &:not(.dropdown-item) label { - border: 2px solid transparent; - background-color: $o-brand-lightsecondary; - - &.checked { - border-color: $o-brand-odoo; - color: $o-brand-odoo; - } + &:not(.dropdown-item) label.checked { + border: 2px solid $o-brand-odoo; + color: $o-brand-odoo; } &.o_theme_customize_with_widget { @@ -115,10 +68,9 @@ .o_theme_customize_color { position: relative; - width: 30px; - height: 20px; + width: 1rem; + height: 1rem; margin-left: auto; - border: 1px solid black; &::before, &::after { content: ""; @@ -171,10 +123,6 @@ pointer-events: none; } - .o_theme_customize_option_list { - box-shadow: 0 0 40px 0 rgba(0, 0, 0, 0.05); - } - .o_theme_customize_dropdown_btn { background-color: $o-brand-lightsecondary; } diff --git a/addons/website/static/src/xml/website.editor.xml b/addons/website/static/src/xml/website.editor.xml index 31ffb28704d..eb19b27e7af 100644 --- a/addons/website/static/src/xml/website.editor.xml +++ b/addons/website/static/src/xml/website.editor.xml @@ -28,15 +28,11 @@ -
    -