[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:
qsm-odoo
2018-07-27 12:36:54 +02:00
parent 76f714dca8
commit 6dc37ee461
10 changed files with 145 additions and 142 deletions
@@ -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 -57
View File
@@ -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');
}
},
+113
View File
@@ -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);
}
},
};
});
+1 -1
View File
@@ -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;
+7 -3
View File
@@ -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;
}
}
}
+3 -7
View File
@@ -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);
}
},
},
+4 -67
View File
@@ -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);
},
});
+1 -5
View File
@@ -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. """