[REF] web, *: share and adapt code for auto "more" buttons for navbars
* portal, web_tour, website, base Before this commit, there were 3 different implementations for the same feature: the "more" button in the community navbar, the "+" button for the website menu and the "+" button in the enterprise navbar. As those implementation were now broken with BS4, it was a good time to share the code of those 3 features.
This commit is contained in:
@@ -32,8 +32,13 @@ header {
|
||||
|
||||
img {
|
||||
@include o-w-fill-navbar;
|
||||
width: auto;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
.nav-link {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
.navbar {
|
||||
margin-bottom: 0;
|
||||
|
||||
@@ -2,6 +2,7 @@ odoo.define('web.Menu', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var dom = require('web.dom');
|
||||
var session = require('web.session');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
@@ -37,66 +38,10 @@ var Menu = Widget.extend({
|
||||
}
|
||||
this.trigger('menu_bound');
|
||||
|
||||
var lazyreflow = _.debounce(this.reflow.bind(this), 200);
|
||||
core.bus.on('resize', this, function() {
|
||||
if ($(window).width() < 768 ) {
|
||||
lazyreflow('all_outside');
|
||||
} else {
|
||||
lazyreflow();
|
||||
}
|
||||
});
|
||||
core.bus.trigger('resize');
|
||||
dom.initAutoMoreMenu(this.$el);
|
||||
|
||||
this.is_bound.resolve();
|
||||
},
|
||||
|
||||
/**
|
||||
* Reflow the menu items and dock overflowing items into a "More" menu item.
|
||||
* Automatically called when 'menu_bound' event is triggered and on window resizing.
|
||||
*
|
||||
* @param {string} behavior If set to 'all_outside', all the items are displayed.
|
||||
* If not set, only the overflowing items are hidden.
|
||||
*/
|
||||
reflow: function(behavior) {
|
||||
var self = this;
|
||||
var $more_container = this.$('#menu_more_container').hide();
|
||||
var $more = this.$('#menu_more');
|
||||
var $systray = this.$el.parents().find('.oe_systray');
|
||||
|
||||
$more.children('li').insertBefore($more_container); // Pull all the items out of the more menu
|
||||
|
||||
// 'all_outside' beahavior should display all the items, so hide the more menu and exit
|
||||
if (behavior === 'all_outside') {
|
||||
// Show list of menu items
|
||||
self.$el.show();
|
||||
this.$el.find('li').show();
|
||||
$more_container.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
// Hide all menu items
|
||||
var $toplevel_items = this.$el.find('li').not($more_container).not($systray.find('li')).hide();
|
||||
// Show list of menu items (which is empty for now since all menu items are hidden)
|
||||
self.$el.show();
|
||||
$toplevel_items.each(function() {
|
||||
var remaining_space = self.$el.parent().width() - $more_container.outerWidth();
|
||||
self.$el.parent().children(':visible').each(function() {
|
||||
remaining_space -= $(this).outerWidth();
|
||||
});
|
||||
|
||||
if ($(this).width() >= remaining_space) {
|
||||
return false; // the current item will be appended in more_container
|
||||
}
|
||||
$(this).show(); // show the current item in menu bar
|
||||
});
|
||||
$more.append($toplevel_items.filter(':hidden').show());
|
||||
$more_container.toggle(!!$more.children().length);
|
||||
// Hide toplevel item if there is only one
|
||||
var $toplevel = self.$el.children("li:visible");
|
||||
if ($toplevel.length === 1) {
|
||||
$toplevel.hide();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Opens a given menu by id, as if a user had browsed to that menu by hand
|
||||
* except does not trigger any event on the way
|
||||
|
||||
@@ -3,6 +3,7 @@ odoo.define('web.WebClient', function (require) {
|
||||
|
||||
var AbstractWebClient = require('web.AbstractWebClient');
|
||||
var config = require('web.config');
|
||||
var core = require('web.core');
|
||||
var data_manager = require('web.data_manager');
|
||||
var dom = require('web.dom');
|
||||
var framework = require('web.framework');
|
||||
@@ -48,6 +49,9 @@ return AbstractWebClient.extend({
|
||||
return $.when(systray_menu_loaded, user_menu_loaded).then(function() {
|
||||
self.menu.start();
|
||||
self.bind_hashchange();
|
||||
setTimeout(function () {
|
||||
self.menu.$el.removeClass('d-none');
|
||||
}, 200)
|
||||
});
|
||||
|
||||
},
|
||||
@@ -165,7 +169,7 @@ return AbstractWebClient.extend({
|
||||
toggle_fullscreen: function(fullscreen) {
|
||||
this._super(fullscreen);
|
||||
if (!fullscreen) {
|
||||
this.menu.reflow();
|
||||
core.bus.trigger('resize');
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ odoo.define('web.dom', function (require) {
|
||||
* something happens in the DOM.
|
||||
*/
|
||||
|
||||
var config = require('web.config');
|
||||
var core = require('web.core');
|
||||
|
||||
/**
|
||||
@@ -315,5 +316,117 @@ return {
|
||||
.select();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Creates an automatic 'more' dropdown-menu for a set of navbar items.
|
||||
*
|
||||
* @param {jQuery} $el
|
||||
* @param {Object} [options]
|
||||
* @param {string} [options.unfoldable='none']
|
||||
* @param {function} [options.maxWidth]
|
||||
* @param {string} [options.sizeClass='SM']
|
||||
*/
|
||||
initAutoMoreMenu: function ($el, options) {
|
||||
options = _.extend({
|
||||
unfoldable: 'none',
|
||||
maxWidth: false,
|
||||
sizeClass: 'SM',
|
||||
}, options || {});
|
||||
|
||||
var $extraItemsToggle = null;
|
||||
|
||||
var debouncedAdapt = _.debounce(_adapt, 250);
|
||||
core.bus.on('resize', null, debouncedAdapt);
|
||||
_adapt();
|
||||
|
||||
$el.data('dom:autoMoreMenu:destroy', function () {
|
||||
_restore();
|
||||
core.bus.off('resize', null, debouncedAdapt);
|
||||
$el.removeData('dom:autoMoreMenu:destroy');
|
||||
});
|
||||
|
||||
function _restore() {
|
||||
if ($extraItemsToggle === null) {
|
||||
return;
|
||||
}
|
||||
var $items = $extraItemsToggle.children('.dropdown-menu').children();
|
||||
$items.addClass('nav-item');
|
||||
$items.children().removeClass('dropdown-item').addClass('nav-link');
|
||||
$items.insertBefore($extraItemsToggle);
|
||||
$extraItemsToggle.remove();
|
||||
$extraItemsToggle = null;
|
||||
}
|
||||
|
||||
function _adapt() {
|
||||
if (!$el.is(':visible')) {
|
||||
return;
|
||||
}
|
||||
|
||||
_restore();
|
||||
if (config.device.size_class <= config.device.SIZES[options.sizeClass]) {
|
||||
return;
|
||||
}
|
||||
|
||||
var $allItems = $el.children();
|
||||
var $unfoldableItems = $allItems.filter(options.unfoldable);
|
||||
var $items = $allItems.not($unfoldableItems);
|
||||
|
||||
var maxWidth = 0;
|
||||
if (options.maxWidth) {
|
||||
maxWidth = options.maxWidth();
|
||||
} else {
|
||||
var rect = $el[0].getBoundingClientRect();
|
||||
var style = window.getComputedStyle($el[0]);
|
||||
maxWidth = (rect.right - rect.left);
|
||||
maxWidth -= (parseFloat(style.paddingLeft) + parseFloat(style.paddingRight) + parseFloat(style.borderLeftWidth) + parseFloat(style.borderRightWidth));
|
||||
maxWidth -= _.reduce($unfoldableItems, function (sum, el) {
|
||||
return sum + computeFloatOuterWidthWithMargins(el);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
var nbItems = $items.length;
|
||||
var menuItemsWidth = _.reduce($items, function (sum, el) {
|
||||
return sum + computeFloatOuterWidthWithMargins(el);
|
||||
}, 0);
|
||||
|
||||
if (maxWidth - menuItemsWidth >= -0.001) {
|
||||
return
|
||||
}
|
||||
|
||||
var $dropdownMenu = $('<ul/>', {class: 'dropdown-menu'});
|
||||
$extraItemsToggle = $('<li/>', {class: 'nav-item dropdown o_extra_menu_items'})
|
||||
.append($('<a/>', {href: '#', class: 'nav-link dropdown-toggle o-no-caret', 'data-toggle': 'dropdown'})
|
||||
.append($('<i/>', {class: 'fa fa-plus'})))
|
||||
.append($dropdownMenu);
|
||||
$extraItemsToggle.insertAfter($items.last());
|
||||
|
||||
menuItemsWidth += computeFloatOuterWidthWithMargins($extraItemsToggle[0]);
|
||||
do {
|
||||
menuItemsWidth -= computeFloatOuterWidthWithMargins($items.eq(--nbItems)[0]);
|
||||
} while (menuItemsWidth > maxWidth);
|
||||
|
||||
var $extraItems = $items.slice(nbItems).detach();
|
||||
$extraItems.removeClass('nav-item');
|
||||
$extraItems.children().removeClass('nav-link').addClass('dropdown-item');
|
||||
$dropdownMenu.append($extraItems);
|
||||
$extraItemsToggle.find('.nav-link').toggleClass('active', $extraItems.children().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);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Cleans what has been done by `initAutoMoreMenu'.
|
||||
*
|
||||
* @param {jQuery} $el
|
||||
*/
|
||||
destroyAutoMoreMenu: function ($el) {
|
||||
var destroyFunc = $el.data('dom:autoMoreMenu:destroy');
|
||||
if (destroyFunc) {
|
||||
destroyFunc.call(null);
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
pointer-events: none; // hack to prevent clicking on it because dropdown always opened
|
||||
}
|
||||
}
|
||||
> ul { // remove dropdown-menu default style as it is nested in another one
|
||||
> .dropdown-menu { // remove dropdown-menu default style as it is nested in another one
|
||||
position: static;
|
||||
float: none;
|
||||
display: block;
|
||||
|
||||
@@ -14,8 +14,12 @@
|
||||
display: none!important;
|
||||
}
|
||||
|
||||
.dropdown-toggle.o-no-caret {
|
||||
&::before, &::after {
|
||||
content: normal;
|
||||
.dropdown-toggle {
|
||||
white-space: nowrap;
|
||||
|
||||
&.o-no-caret {
|
||||
&::before, &::after {
|
||||
content: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -400,18 +400,14 @@
|
||||
</template>
|
||||
|
||||
<template id="web.menu">
|
||||
<ul class="nav navbar-nav navbar-left oe_application_menu_placeholder" style="display: none;" role="menu">
|
||||
<ul class="nav navbar-nav navbar-left d-none oe_application_menu_placeholder" role="menu">
|
||||
<li t-foreach="menu_data['children']" t-as="menu" class="nav-item">
|
||||
<t t-call="web.menu_link"/>
|
||||
</li>
|
||||
<li id="menu_more_container" class="nav-item dropdown" style="display: none;">
|
||||
<a role="menuitem" href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" id="more_menu_button">More</a>
|
||||
<div id="menu_more" class="dropdown-menu" role="menu"></div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="nav navbar-nav ml-auto oe_systray" style="display: none;"/>
|
||||
<ul class="nav navbar-nav oe_user_menu_placeholder" style="display: none;"/>
|
||||
<ul class="nav navbar-nav flex-nowrap ml-auto oe_systray" style="display: none;"/>
|
||||
<ul class="nav navbar-nav flex-nowrap oe_user_menu_placeholder" style="display: none;"/>
|
||||
</template>
|
||||
<template id="web.menu_secondary">
|
||||
<a class="o_sub_menu_logo" t-att-href="'/web/?debug' if debug else '/web'">
|
||||
|
||||
@@ -361,7 +361,10 @@ return core.Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
|
||||
position: "bottom",
|
||||
auto: true,
|
||||
run: function (actions) {
|
||||
actions.auto("#more_menu_button");
|
||||
var $more = $('.o_extra_menu_items > .dropdown-toggle');
|
||||
if ($more.length) {
|
||||
actions.auto($more);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ odoo.define('website.content.menu', function (require) {
|
||||
'use strict';
|
||||
|
||||
var config = require('web.config');
|
||||
var dom = require('web.dom');
|
||||
var sAnimation = require('website.content.snippets.animation');
|
||||
|
||||
sAnimation.registry.affixMenu = sAnimation.Class.extend({
|
||||
@@ -81,19 +82,13 @@ sAnimation.registry.affixMenu = sAnimation.Class.extend({
|
||||
* the backend in enterprise...
|
||||
*/
|
||||
sAnimation.registry.autohideMenu = sAnimation.Class.extend({
|
||||
selector: 'header:not(.o_no_autohide_menu) > .navbar > *',
|
||||
selector: 'header:not(.o_no_autohide_menu) #top_menu',
|
||||
|
||||
/**
|
||||
* @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();
|
||||
|
||||
dom.initAutoMoreMenu(this.$el, {unfoldable: '.divider, .divider ~ li'});
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
@@ -101,65 +96,7 @@ sAnimation.registry.autohideMenu = sAnimation.Class.extend({
|
||||
*/
|
||||
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 = $('<li/>', {class: 'o_extra_menu_items'});
|
||||
this.$extraItemsToggle.append($('<a/>', {href: '#', class: 'dropdown-toggle', 'data-toggle': 'dropdown'})
|
||||
.append($('<i/>', {class: 'fa fa-plus'})));
|
||||
this.$extraItemsToggle.append($('<ul/>', {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;
|
||||
}
|
||||
dom.destroyAutoMoreMenu(this.$el);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -163,16 +163,12 @@ class TranslationToolsTestCase(unittest.TestCase):
|
||||
<span class="oe_menu_text">Blah</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown" id="menu_more_container" style="display: none;">
|
||||
<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#">More <b class="caret"/></a>
|
||||
<div class="dropdown-menu" id="menu_more"/>
|
||||
</li>
|
||||
</ul>
|
||||
</t>"""
|
||||
result = xml_translate(terms.append, source)
|
||||
self.assertEquals(result, source)
|
||||
self.assertItemsEqual(terms,
|
||||
['<span class="oe_menu_text">Blah</span>', 'More <b class="caret"/>'])
|
||||
['<span class="oe_menu_text">Blah</span>'])
|
||||
|
||||
def test_translate_xml_with_namespace(self):
|
||||
""" Test xml_translate() on elements with namespaces. """
|
||||
|
||||
Reference in New Issue
Block a user