odoo.define('website.content.menu', function (require) { 'use strict'; var config = require('web.config'); var sAnimation = require('website.content.snippets.animation'); sAnimation.registry.affixMenu = sAnimation.Class.extend({ selector: 'header.o_affix_enabled', /** * @override */ start: function () { var def = this._super.apply(this, arguments); if (this.editableMode) { return def; } var self = this; this.$headerClone = this.$target.clone().addClass('o_header_affix affix').removeClass('o_affix_enabled'); this.$headerClone.insertAfter(this.$target); this.$headers = this.$target.add(this.$headerClone); this.$dropdowns = this.$headers.find('.dropdown'); this.$navbarCollapses = this.$headers.find('.navbar-collapse'); // Handle events for the collapse menus _.each(this.$headerClone.find('[data-toggle="collapse"]'), function (el) { var $source = $(el); var targetClass = $source.attr('data-target'); var $target = self.$headerClone.find(targetClass); var className = targetClass.substring(1); $source.attr('data-target', targetClass + '_clone'); $target.removeClass(className).addClass(className + '_clone'); }); // Window Handlers $(window).on('resize.affixMenu scroll.affixMenu', _.throttle(this._onWindowUpdate.bind(this), 200)); setTimeout(this._onWindowUpdate.bind(this), 0); // setTimeout to allow override with advanced stuff... see themes return def; }, /** * @override */ destroy: function () { if (this.$headerClone) { this.$headerClone.remove(); $(window).off('.affixMenu'); } this._super.apply(this, arguments); }, //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- /** * Called when the window is resized or scrolled -> updates affix status and * automatically closes submenus. * * @private */ _onWindowUpdate: function () { var wOffset = $(window).scrollTop(); var hOffset = this.$target.scrollTop(); this.$headerClone.toggleClass('affixed', wOffset > (hOffset + 300)); // Reset opened menus this.$dropdowns.removeClass('open'); this.$navbarCollapses.removeClass('in').attr('aria-expanded', false); }, }); /** * Auto adapt the header layout so that elements are not wrapped on a new line. * * Note: this works well with the affixMenu... by chance (autohideMenu is called * after alphabetically). * * @todo We may want to avoid some code duplication by sharing what is done in * the backend in enterprise... */ sAnimation.registry.autohideMenu = sAnimation.Class.extend({ selector: 'header:not(.o_no_autohide_menu) > .navbar > *', /** * @override */ start: function () { var $allItems = this.$('#top_menu').children(); this.$unfoldableItems = $allItems.filter('.divider, .divider ~ li'); this.$items = $allItems.not(this.$unfoldableItems); $(window).on('resize', _.debounce(this._adapt.bind(this), 500)); this._adapt(); return this._super.apply(this, arguments); }, /** * @override */ destroy: function () { this._super.apply(this, arguments); this._restore(); }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- /** * @private */ _adapt: function () { this._restore(); if (config.device.isMobile) { return; } this.maxWidth = this.$el.width(); var $unfoldable = this.$unfoldableItems.add(this.$el.children().not('.navbar-collapse')); this.maxWidth -= _.reduce($unfoldable, function (sum, el) { return sum + computeFloatOuterWidthWithMargins(el); }, 0); var nbItems = this.$items.length; var menuItemsWidth = _.reduce(this.$items, function (sum, el) { return sum + computeFloatOuterWidthWithMargins(el); }, 0); if (menuItemsWidth > this.maxWidth) { this.$extraItemsToggle = $('
  • ', {class: 'o_extra_menu_items'}); this.$extraItemsToggle.append($('', {href: '#', class: 'dropdown-toggle', 'data-toggle': 'dropdown'}) .append($('', {class: 'fa fa-plus'}))); this.$extraItemsToggle.append($('
      ', {class: 'dropdown-menu'})); this.$extraItemsToggle.insertAfter(this.$items.last()); menuItemsWidth += computeFloatOuterWidthWithMargins(this.$extraItemsToggle[0]); do { menuItemsWidth -= computeFloatOuterWidthWithMargins(this.$items.eq(--nbItems)[0]); } while (menuItemsWidth > this.maxWidth); var $extraItems = this.$items.slice(nbItems).detach(); this.$extraItemsToggle.children('ul').append($extraItems); this.$extraItemsToggle.toggleClass('active', $extraItems.hasClass('active')); } function computeFloatOuterWidthWithMargins(el) { var rect = el.getBoundingClientRect(); var style = window.getComputedStyle(el); return rect.right - rect.left + parseFloat(style.marginLeft) + parseFloat(style.marginRight); } }, /** * @private */ _restore: function () { if (this.$extraItemsToggle) { this.$extraItemsToggle.find("> ul > *").insertBefore(this.$extraItemsToggle); this.$extraItemsToggle.remove(); delete this.$extraItemsToggle; } }, }); /** * Note: this works well with the affixMenu... by chance (menuDirection is * called after alphabetically). * * @todo check bootstrap v4: maybe handled automatically now ? */ sAnimation.registry.menuDirection = sAnimation.Class.extend({ selector: 'header .navbar .nav', events: { 'show.bs.dropdown': '_onDropdownShow', }, /** * @override */ start: function () { this.defaultAlignment = this.$el.is('.navbar-right') ? 'right' : 'left'; return this._super.apply(this, arguments); }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- /** * @private * @param {string} alignment - either 'left' or 'right' * @param {integer} liOffset * @param {integer} liWidth * @param {integer} menuWidth * @returns {boolean} */ _checkOpening: function (alignment, liOffset, liWidth, menuWidth, windowWidth) { if (alignment === 'left') { // Check if ok to open the dropdown to the right (no window overflow) return (liOffset + menuWidth <= windowWidth); } else { // Check if ok to open the dropdown to the left (no window overflow) return (liOffset + liWidth - menuWidth >= 0); } }, //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- /** * @private */ _onDropdownShow: function (ev) { var $li = $(ev.target); var $menu = $li.children('.dropdown-menu'); var liOffset = $li.offset().left; var liWidth = $li.outerWidth(); var menuWidth = $menu.outerWidth(); var windowWidth = $(window).outerWidth(); $menu.removeClass('dropdown-menu-left dropdown-menu-right'); var alignment = this.defaultAlignment; if ($li.nextAll(':visible').length === 0) { // The dropdown is the last menu item, open to the left alignment = 'right'; } // If can't open in the current direction because it would overflow the // window, change the direction. But if the other direction would do the // same, change back the direction. for (var i = 0 ; i < 2 ; i++) { if (!this._checkOpening(alignment, liOffset, liWidth, menuWidth, windowWidth)) { alignment = (alignment === 'left' ? 'right' : 'left'); } } $menu.addClass('dropdown-menu-' + alignment); }, }); });