[MERGE] website, *: remove menu loading effect

*: web, website_sale

See sub-commits for details.

In short: the menu was loading with an effect partly for technical
reasons: potentially folding menu items in a "+" dropdown instead of
making it go over multiple lines. Since the JS lazy loading, the menu
made even more time to appear. All of that is now removed: the menu is
there as soon as the page appears to the user, without effect or delay.

task-2618929

closes odoo/odoo#77843

X-original-commit: 0681678c4d028f9f2bda419996bf5b14e771c132
Signed-off-by: Quentin Smetz (qsm) <qsm@odoo.com>
This commit is contained in:
Odoo's Mergebot
2021-10-05 16:17:14 +02:00
committed by GitHub
9 changed files with 265 additions and 183 deletions
+2
View File
@@ -61,6 +61,7 @@ This module provides the core of the Odoo Web Client.
'web/static/src/boot.js',
'web/static/src/session.js',
'web/static/src/legacy/js/core/cookie_utils.js',
'web/static/src/legacy/js/core/menu.js',
],
'web.assets_common': [
('include', 'web._assets_helpers'),
@@ -79,6 +80,7 @@ This module provides the core of the Odoo Web Client.
('remove', 'web/static/src/boot.js'),
('remove', 'web/static/src/session.js'),
('remove', 'web/static/src/legacy/js/core/cookie_utils.js'),
('remove', 'web/static/src/legacy/js/core/menu.js'),
],
'web.assets_backend': [
('include', 'web._assets_helpers'),
-125
View File
@@ -616,131 +616,6 @@ var dom = {
$scrollable.animate({scrollTop: originalScrollTop}, clonedOptions);
});
},
/**
* 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 autoMarginLeftRegex = /\bm[lx]?(?:-(?:sm|md|lg|xl))?-auto\b/;
var autoMarginRightRegex = /\bm[rx]?(?:-(?:sm|md|lg|xl))?-auto\b/;
var $extraItemsToggle = null;
var debouncedAdapt = _.debounce(_adapt, 250);
core.bus.on('resize', null, debouncedAdapt);
_adapt();
$el.data('dom:autoMoreMenu:adapt', _adapt);
$el.data('dom:autoMoreMenu:destroy', function () {
_restore();
core.bus.off('resize', null, debouncedAdapt);
$el.removeData(['dom:autoMoreMenu:destroy', 'dom:autoMoreMenu:adapt']);
});
function _restore() {
if ($extraItemsToggle === null) {
return;
}
var $items = $extraItemsToggle.children('.dropdown-menu').children();
$items.addClass('nav-item');
$items.children('.dropdown-item, a').removeClass('dropdown-item').addClass('nav-link');
$items.insertBefore($extraItemsToggle);
$extraItemsToggle.remove();
$extraItemsToggle = null;
}
function _adapt() {
_restore();
if (!$el.is(':visible') || $el.closest('.show').length) {
// Never transform the menu when it is not visible yet or if
// it is a toggleable one.
return;
}
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 {
maxWidth = computeFloatOuterWidthWithMargins($el[0], true, true, true);
var style = window.getComputedStyle($el[0]);
maxWidth -= (parseFloat(style.paddingLeft) + parseFloat(style.paddingRight) + parseFloat(style.borderLeftWidth) + parseFloat(style.borderRightWidth));
maxWidth -= _.reduce($unfoldableItems, function (sum, el) {
return sum + computeFloatOuterWidthWithMargins(el, true, true, false);
}, 0);
}
var nbItems = $items.length;
var menuItemsWidth = _.reduce($items, function (sum, el) {
return sum + computeFloatOuterWidthWithMargins(el, true, true, false);
}, 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/>', {role: 'button', href: '#', class: 'nav-link dropdown-toggle o-no-caret', 'data-toggle': 'dropdown', 'aria-expanded': false})
.append($('<i/>', {class: 'fa fa-plus'})))
.append($dropdownMenu);
$extraItemsToggle.insertAfter($items.last());
menuItemsWidth += computeFloatOuterWidthWithMargins($extraItemsToggle[0], true, true, false);
do {
menuItemsWidth -= computeFloatOuterWidthWithMargins($items.eq(--nbItems)[0], true, true, false);
} while (!(maxWidth - menuItemsWidth >= -0.001) && (nbItems > 0));
var $extraItems = $items.slice(nbItems).detach();
$extraItems.removeClass('nav-item');
$extraItems.children('.nav-link, a').removeClass('nav-link').addClass('dropdown-item');
$dropdownMenu.append($extraItems);
$extraItemsToggle.find('.nav-link').toggleClass('active', $extraItems.children().hasClass('active'));
}
function computeFloatOuterWidthWithMargins(el, mLeft, mRight, considerAutoMargins) {
var rect = el.getBoundingClientRect();
var style = window.getComputedStyle(el);
var outerWidth = rect.right - rect.left;
if (mLeft !== false && (considerAutoMargins || !autoMarginLeftRegex.test(el.getAttribute('class')))) {
outerWidth += parseFloat(style.marginLeft);
}
if (mRight !== false && (considerAutoMargins || !autoMarginRightRegex.test(el.getAttribute('class')))) {
outerWidth += parseFloat(style.marginRight);
}
// Would be NaN for invisible elements for example
return isNaN(outerWidth) ? 0 : outerWidth;
}
},
/**
* 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);
}
},
};
return dom;
});
@@ -0,0 +1,218 @@
/** @odoo-module **/
/**
* Creates an automatic 'more' dropdown-menu for a set of navbar items.
*
* @param {HTMLElement} el
* @param {Object} [options]
* @param {string} [options.unfoldable='none'] selector for items that do not
* need to be added to dropdown-menu.
* @param {float} [options.maxWidth] The max width value that menu content
* can take => the overflowing (foldable) items are added in the dropdown-menu.
* @param {string} [options.minSize] the menu auto-hide option will be disabled
* if viewport is smaller than minSize.
* @param {Array} [options.images=[]] images to wait for before menu update.
* @param {Array} [options.loadingStyleClasses=[]] list of CSS classes to add while
* updating the menu.
*/
export async function initAutoMoreMenu(el, options) {
if (!el) {
return;
}
options = Object.assign({
unfoldable: 'none',
maxWidth: false,
minSize: '767',
images: [],
loadingStyleClasses: [],
}, options || {});
const isUserNavbar = el.parentElement.classList.contains('o_main_navbar');
const dropdownSubMenuClasses = ['show', 'border', 'position-static'];
var autoMarginLeftRegex = /\bm[lx]?(?:-(?:sm|md|lg|xl))?-auto\b/;
var autoMarginRightRegex = /\bm[rx]?(?:-(?:sm|md|lg|xl))?-auto\b/;
var extraItemsToggle = null;
let debounce;
if (options.images.length) {
await _afterImagesLoading(options.images);
_adapt();
}
const debouncedAdapt = () => {
clearTimeout(debounce);
debounce = setTimeout(_adapt, 250);
};
window.addEventListener('resize', debouncedAdapt);
_adapt();
el.addEventListener('dom:autoMoreMenu:adapt', _adapt);
el.addEventListener('dom:autoMoreMenu:destroy', destroy, {once: true});
function _restore() {
if (!extraItemsToggle) {
return;
}
// Move extra menu items from dropdown-menu to menu element in the same order.
[...extraItemsToggle.querySelector('.dropdown-menu').children].forEach((item) => {
if (!isUserNavbar) {
item.classList.add('nav-item');
const itemLink = item.querySelector('.dropdown-item');
itemLink.classList.remove('dropdown-item');
itemLink.classList.add('nav-link');
} else {
item.classList.remove('dropdown-item');
const dropdownSubMenu = item.querySelector('.dropdown-menu');
if (dropdownSubMenu) {
dropdownSubMenu.classList.remove(...dropdownSubMenuClasses);
}
}
el.insertBefore(item, extraItemsToggle);
});
extraItemsToggle.remove();
extraItemsToggle = null;
}
function _adapt() {
if (options.loadingStyleClasses.length) {
el.classList.add(...options.loadingStyleClasses);
}
_restore();
// Ignore invisible/toggleable top menu element & small viewports.
if (!el.getClientRects().length || el.closest('.show')
|| window.matchMedia(`(max-width: ${options.minSize}px)`).matches) {
return _endAutoMoreMenu();
}
let unfoldableItems = [];
const items = [...el.children].filter((node) => {
if (node.matches && !node.matches(options.unfoldable)) {
return true;
}
unfoldableItems.push(node);
return false;
});
var nbItems = items.length;
var menuItemsWidth = items.reduce((sum, el) => sum + computeFloatOuterWidthWithMargins(el, true, true, false), 0);
let maxWidth = 0;
if (options.maxWidth) {
maxWidth = options.maxWidth();
}
if (!maxWidth) {
maxWidth = computeFloatOuterWidthWithMargins(el, true, true, true);
var style = window.getComputedStyle(el);
maxWidth -= (parseFloat(style.paddingLeft) + parseFloat(style.paddingRight) + parseFloat(style.borderLeftWidth) + parseFloat(style.borderRightWidth));
maxWidth -= unfoldableItems.reduce((sum, el) => sum + computeFloatOuterWidthWithMargins(el, true, true, false), 0);
}
// Ignore if there is no overflow.
if (maxWidth - menuItemsWidth >= -0.001) {
return _endAutoMoreMenu();
}
const dropdownMenu = _addExtraItemsButton(items[nbItems - 1].nextElementSibling);
menuItemsWidth += computeFloatOuterWidthWithMargins(extraItemsToggle, true, true, false);
do {
menuItemsWidth -= computeFloatOuterWidthWithMargins(items[--nbItems], true, true, false);
} while (!(maxWidth - menuItemsWidth >= -0.001) && (nbItems > 0));
const extraItems = items.slice(nbItems);
extraItems.forEach((el) => {
if (!isUserNavbar) {
const navLink = el.querySelector('.nav-link, a');
el.classList.remove('nav-item');
navLink.classList.remove('nav-link');
navLink.classList.add('dropdown-item');
navLink.classList.toggle('active', el.classList.contains('active'));
} else {
const dropdownSubMenu = el.querySelector('.dropdown-menu');
el.classList.add('dropdown-item');
if (dropdownSubMenu) {
dropdownSubMenu.classList.add(...dropdownSubMenuClasses);
}
}
dropdownMenu.appendChild(el);
});
_endAutoMoreMenu();
}
function computeFloatOuterWidthWithMargins(el, mLeft, mRight, considerAutoMargins) {
var rect = el.getBoundingClientRect();
var style = window.getComputedStyle(el);
var outerWidth = rect.right - rect.left;
if (mLeft !== false && (considerAutoMargins || !autoMarginLeftRegex.test(el.getAttribute('class')))) {
outerWidth += parseFloat(style.marginLeft);
}
if (mRight !== false && (considerAutoMargins || !autoMarginRightRegex.test(el.getAttribute('class')))) {
outerWidth += parseFloat(style.marginRight);
}
// Would be NaN for invisible elements for example
return isNaN(outerWidth) ? 0 : outerWidth;
}
function _addExtraItemsButton(target) {
let dropdownMenu = document.createElement('ul');
extraItemsToggle = document.createElement('li');
const extraItemsToggleIcon = document.createElement('i');
const extraItemsToggleLink = document.createElement('a');
dropdownMenu.className = 'dropdown-menu';
extraItemsToggle.className = 'nav-item dropdown o_extra_menu_items';
extraItemsToggleIcon.className = 'fa fa-plus';
Object.entries({
role: 'button',
href: '#',
class: 'nav-link dropdown-toggle o-no-caret',
'data-toggle': 'dropdown',
'aria-expanded': false,
}).forEach(([key, value]) => {
extraItemsToggleLink.setAttribute(key, value);
});
extraItemsToggleLink.appendChild(extraItemsToggleIcon);
extraItemsToggle.appendChild(extraItemsToggleLink);
extraItemsToggle.appendChild(dropdownMenu);
el.insertBefore(extraItemsToggle, target);
return dropdownMenu;
}
function destroy() {
_restore();
window.removeEventListener('resize', debouncedAdapt);
el.removeEventListener('dom:autoMoreMenu:adapt', _adapt);
}
function _afterImagesLoading(images) {
const defs = images.map((image) => {
if (image.complete || !image.getClientRects().length) {
return null;
}
return new Promise(function (resolve, reject) {
if (!image.width) {
// The purpose of the 'o_menu_image_placeholder' class is to add a default
// size to non loaded images (on the first update) to prevent flickering.
image.classList.add('o_menu_image_placeholder');
}
image.addEventListener('load', () => {
image.classList.remove('o_menu_image_placeholder');
resolve();
});
});
});
return Promise.all(defs);
}
function _endAutoMoreMenu() {
el.classList.remove(...options.loadingStyleClasses);
}
}
/**
* Cleans what has been done by ``initAutoMoreMenu``.
*
* @param {HTMLElement} el
*/
export function destroyAutoMoreMenu(el) {
el.dispatchEvent(new Event('dom:autoMoreMenu:destroy'));
}
+2
View File
@@ -133,10 +133,12 @@
],
'web.assets_frontend_minimal': [
'website/static/src/js/content/inject_dom.js',
'website/static/src/js/content/auto_hide_menu.js',
],
'web.assets_frontend_lazy': [
# Remove assets_frontend_minimal
('remove', 'website/static/src/js/content/inject_dom.js'),
('remove', 'website/static/src/js/content/auto_hide_menu.js'),
],
'web._assets_primary_variables': [
'website/static/src/scss/primary_variables.scss',
@@ -0,0 +1,33 @@
/** @odoo-module **/
import { initAutoMoreMenu } from '@web/legacy/js/core/menu';
/**
* Auto adapt the header layout so that elements are not wrapped on a new line.
*/
document.addEventListener('DOMContentLoaded', async () => {
const header = document.querySelector('header#top');
if (header) {
const topMenu = header.querySelector('#top_menu');
if (header.classList.contains('o_no_autohide_menu')) {
topMenu.classList.remove('o_menu_loading');
return;
}
const unfoldable = '.divider, .divider ~ li, .o_no_autohide_item, .js_language_selector';
const excludedImagesSelector = '.o_mega_menu, .o_offcanvas_logo_container, .o_lang_flag';
const excludedImages = [...header.querySelectorAll(excludedImagesSelector)];
const images = [...header.querySelectorAll('img')].filter((img) => {
excludedImages.forEach(node => {
if (node.contains(img)) {
return false;
}
});
return img.matches && !img.matches(excludedImagesSelector);
});
initAutoMoreMenu(topMenu, {
unfoldable: unfoldable,
images: images,
loadingStyleClasses: ['o_menu_loading']
});
}
});
@@ -4,9 +4,7 @@ odoo.define('website.content.menu', function (require) {
const config = require('web.config');
var dom = require('web.dom');
var publicWidget = require('web.public.widget');
var wUtils = require('website.utils');
var animations = require('website.content.snippets.animation');
const extraMenuUpdateCallbacks = [];
const BaseAnimatedHeader = animations.Animation.extend({
@@ -413,51 +411,6 @@ publicWidget.registry.FadeOutHeader = BaseDisappearingHeader.extend({
},
});
/**
* Auto adapt the header layout so that elements are not wrapped on a new line.
*/
publicWidget.registry.autohideMenu = publicWidget.Widget.extend({
selector: 'header#top',
disabledInEditableMode: false,
/**
* @override
*/
async start() {
await this._super(...arguments);
this.$topMenu = this.$('#top_menu');
this.noAutohide = this.$el.is('.o_no_autohide_menu');
if (!this.noAutohide) {
await wUtils.onceAllImagesLoaded(this.$('.navbar'), this.$('.o_mega_menu, .o_offcanvas_logo_container, .dropdown-menu .o_lang_flag'));
// The previous code will make sure we wait for images to be fully
// loaded before initializing the auto more menu. But in some cases,
// it is not enough, we also have to wait for fonts or even extra
// scripts. Those will have no impact on the feature in most cases
// though, so we will only update the auto more menu at that time,
// no wait for it to initialize the feature.
var $window = $(window);
$window.on('load.autohideMenu', function () {
$window.trigger('resize');
});
dom.initAutoMoreMenu(this.$topMenu, {unfoldable: '.divider, .divider ~ li, .o_no_autohide_item'});
}
this.$topMenu.removeClass('o_menu_loading');
this.$topMenu.trigger('menu_loaded');
},
/**
* @override
*/
destroy() {
this._super(...arguments);
if (!this.noAutohide && this.$topMenu) {
$(window).off('.autohideMenu');
dom.destroyAutoMoreMenu(this.$topMenu);
}
},
});
/**
* Note: this works well with the affixMenu... by chance (menuDirection is
* called after alphabetically).
+2 -2
View File
@@ -5,7 +5,7 @@ import dom from 'web.dom';
import publicWidget from 'web.public.widget';
import concurrency from 'web.concurrency';
import Widget from 'web.Widget';
import { initAutoMoreMenu } from '@web/legacy/js/core/menu';
import { registry } from "@web/core/registry";
var WebsiteNavbar = publicWidget.RootWidget.extend({
@@ -40,7 +40,7 @@ var WebsiteNavbar = publicWidget.RootWidget.extend({
*/
start: function () {
var self = this;
dom.initAutoMoreMenu(this.$('ul.o_menu_sections'), {
initAutoMoreMenu(this.el.querySelector('ul.o_menu_sections'), {
maxWidth: function () {
// The navbar contains different elements in community and
// enterprise, so we check for both of them here only
+6 -7
View File
@@ -207,20 +207,19 @@ body:not(.o_fullscreen) {
.navbar-collapse {
min-width: 0; // Allows it to shrink during loading
}
.nav-item {
transition: opacity 1000ms ease 0s;
}
.btn {
// This was a default bootstrap style before but it was removed from
// the library at some point. It seems important in the header so that
// the header does not flicker during loading.
white-space: nowrap;
}
.o_menu_loading {
.o_menu_image_placeholder {
width: 80px !important;
}
#top_menu {
flex-wrap: nowrap !important;
overflow: hidden !important;
.nav-item {
&.o_menu_loading {
overflow: hidden !important;
opacity: 0 !important;
}
}
+2 -2
View File
@@ -4,7 +4,7 @@
<t t-set="website_sale_order" t-value="website.sale_get_order()" />
<t t-set="show_cart" t-value="true"/>
<li t-attf-class="#{_item_class} divider d-none"/> <!-- Make sure the cart and related menus are not folded (see autohideMenu) -->
<li t-attf-class="o_wsale_my_cart #{not show_cart and 'd-none'} #{_item_class}">
<li t-attf-class="o_wsale_my_cart align-self-md-start #{not show_cart and 'd-none'} #{_item_class}">
<a href="/shop/cart" t-attf-class="#{_link_class}">
<i t-if="_icon" class="fa fa-shopping-cart"/>
<span t-if="_text">My Cart</span>
@@ -124,7 +124,7 @@
<xpath expr="//t[@t-foreach='website.menu_id.child_id']" position="after">
<t t-call="website_sale.header_cart_link">
<t t-set="_text" t-value="True"/>
<t t-set="_item_class" t-value="'nav-item'"/>
<t t-set="_item_class" t-value="'nav-item ml-md-auto'"/>
<t t-set="_link_class" t-value="'nav-link'"/>
</t>
</xpath>