odoo.define('website.navbar', function (require) { 'use strict'; var core = require('web.core'); var dom = require('web.dom'); var publicWidget = require('web.public.widget'); var concurrency = require('web.concurrency'); var Widget = require('web.Widget'); var websiteRootData = require('website.root'); var websiteNavbarRegistry = new publicWidget.RootWidgetRegistry(); var WebsiteNavbar = publicWidget.RootWidget.extend({ xmlDependencies: ['/website/static/src/xml/website.xml'], events: _.extend({}, publicWidget.RootWidget.prototype.events || {}, { 'click [data-action]': '_onActionMenuClick', 'mouseover > ul > li.dropdown:not(.show)': '_onMenuHovered', 'click .o_mobile_menu_toggle': '_onMobileMenuToggleClick', 'mouseenter #oe_applications:not(:has(.dropdown-item))': '_onOeApplicationsHovered', 'show.bs.dropdown #oe_applications:not(:has(.dropdown-item))': '_onOeApplicationsShow', }), custom_events: _.extend({}, publicWidget.RootWidget.prototype.custom_events || {}, { 'action_demand': '_onActionDemand', 'edit_mode': '_onEditMode', 'readonly_mode': '_onReadonlyMode', 'ready_to_save': '_onSave', }), /** * @constructor */ init: function () { this._super.apply(this, arguments); var self = this; var initPromise = new Promise(function (resolve) { self.resolveInit = resolve; }); this._widgetDefs = [initPromise]; }, /** * @override */ start: function () { var self = this; dom.initAutoMoreMenu(this.$('ul.o_menu_sections'), { maxWidth: function () { // The navbar contains different elements in community and // enterprise, so we check for both of them here only return self.$el.width() - (self.$('.o_menu_systray').outerWidth(true) || 0) - (self.$('ul#oe_applications').outerWidth(true) || 0) - (self.$('.o_menu_toggle').outerWidth(true) || 0) - (self.$('.o_menu_brand').outerWidth(true) || 0); }, }); return this._super.apply(this, arguments).then(function () { self.resolveInit(); }); }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- /** * @override */ _attachComponent: function () { var def = this._super.apply(this, arguments); this._widgetDefs.push(def); return def; }, /** * As the WebsiteNavbar instance is designed to be unique, the associated * registry has been instantiated outside of the class and is simply * returned here. * * @override */ _getRegistry: function () { return websiteNavbarRegistry; }, /** * Searches for the automatic widget {@see RootWidget} which can handle that * action. * * @private * @param {string} actionName * @param {Array} params * @returns {Promise} */ _handleAction: function (actionName, params, _i) { var self = this; return this._whenReadyForActions().then(function () { var defs = []; _.each(self._widgets, function (w) { if (!w.handleAction) { return; } var def = w.handleAction(actionName, params); if (def !== null) { defs.push(def); } }); if (!defs.length) { // Handle the case where all action-capable components are not // instantiated yet (rare) -> retry some times to eventually abort if (_i > 50) { console.warn(_.str.sprintf("Action '%s' was not able to be handled.", actionName)); return Promise.reject(); } return concurrency.delay(100).then(function () { return self._handleAction(actionName, params, (_i || 0) + 1); }); } return Promise.all(defs).then(function (values) { if (values.length === 1) { return values[0]; } return values; }); }); }, /** * @private * @returns {Promise} */ async _loadAppMenus() { if (!this._loadAppMenusProm) { this._loadAppMenusProm = this._rpc({ model: 'ir.ui.menu', method: 'load_menus_root', args: [], }); const result = await this._loadAppMenusProm; const menus = core.qweb.render('website.oe_applications_menu', { 'menu_data': result, }); this.$('#oe_applications .dropdown-menu').html(menus); } return this._loadAppMenusProm; }, /** * @private */ _whenReadyForActions: function () { return Promise.all(this._widgetDefs); }, //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- /** * Called when the backend applications menu is hovered -> fetch the * available menus and insert it in DOM. * * @private */ _onOeApplicationsHovered: function () { this._loadAppMenus(); }, /** * Called when the backend applications menu is opening -> fetch the * available menus and insert it in DOM. Needed on top of hovering as the * dropdown could be opened via keyboard (or the user could just already * be over the dropdown when the JS is fully loaded). * * @private */ _onOeApplicationsShow: function () { this._loadAppMenus(); }, /** * Called when an action menu is clicked -> searches for the automatic * widget {@see RootWidget} which can handle that action. * * @private * @param {Event} ev */ _onActionMenuClick: function (ev) { const restore = dom.addButtonLoadingEffect(ev.currentTarget); this._handleAction($(ev.currentTarget).data('action')).then(restore).guardedCatch(restore); }, /** * Called when an action is asked to be executed from a child widget -> * searches for the automatic widget {@see RootWidget} which can handle * that action. */ _onActionDemand: function (ev) { var def = this._handleAction(ev.data.actionName, ev.data.params); if (ev.data.onSuccess) { def.then(ev.data.onSuccess); } if (ev.data.onFailure) { def.guardedCatch(ev.data.onFailure); } }, /** * Called in response to edit mode activation -> hides the navbar. * * @private */ _onEditMode: function () { this.$el.addClass('editing_mode'); this.do_hide(); }, /** * Called when a submenu is hovered -> automatically opens it if another * menu was already opened. * * @private * @param {Event} ev */ _onMenuHovered: function (ev) { var $opened = this.$('> ul > li.dropdown.show'); if ($opened.length) { $opened.find('.dropdown-toggle').dropdown('toggle'); $(ev.currentTarget).find('.dropdown-toggle').dropdown('toggle'); } }, /** * Called when the mobile menu toggle button is click -> modifies the DOM * to open the mobile menu. * * @private */ _onMobileMenuToggleClick: function () { this.$el.parent().toggleClass('o_mobile_menu_opened'); }, /** * Called in response to edit mode activation -> hides the navbar. * * @private */ _onReadonlyMode: function () { this.$el.removeClass('editing_mode'); this.do_show(); }, /** * Called in response to edit mode saving -> checks if action-capable * children have something to save. * * @private * @param {OdooEvent} ev */ _onSave: function (ev) { ev.data.defs.push(this._handleAction('on_save')); }, }); var WebsiteNavbarActionWidget = Widget.extend({ /** * 'Action name' -> 'Handler name' object * * Any [data-action="x"] element inside the website navbar will * automatically trigger an action "x". This action can then be handled by * any `WebsiteNavbarActionWidget` instance if the action name "x" is * registered in this `actions` object. */ actions: {}, //-------------------------------------------------------------------------- // Public //-------------------------------------------------------------------------- /** * Checks if the widget can execute an action whose name is given, with the * given parameters. If it is the case, execute that action. * * @param {string} actionName * @param {Array} params * @returns {Promise|null} action's promise or null if no action was found */ handleAction: function (actionName, params) { var action = this[this.actions[actionName]]; if (action) { return Promise.resolve(action.apply(this, params || [])); } return null; }, }); websiteRootData.websiteRootRegistry.add(WebsiteNavbar, '#oe_main_menu_navbar'); return { WebsiteNavbar: WebsiteNavbar, websiteNavbarRegistry: websiteNavbarRegistry, WebsiteNavbarActionWidget: WebsiteNavbarActionWidget, }; });