diff --git a/addons/web/__manifest__.py b/addons/web/__manifest__.py
index 0cc3535cd75..1740ee7a72a 100644
--- a/addons/web/__manifest__.py
+++ b/addons/web/__manifest__.py
@@ -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'),
diff --git a/addons/web/static/src/legacy/js/core/dom.js b/addons/web/static/src/legacy/js/core/dom.js
index 9412fdc1cde..38c1a0c3729 100644
--- a/addons/web/static/src/legacy/js/core/dom.js
+++ b/addons/web/static/src/legacy/js/core/dom.js
@@ -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 = $('
', {class: 'dropdown-menu'});
- $extraItemsToggle = $('', {class: 'nav-item dropdown o_extra_menu_items'})
- .append($('', {role: 'button', href: '#', class: 'nav-link dropdown-toggle o-no-caret', 'data-toggle': 'dropdown', 'aria-expanded': false})
- .append($('', {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;
});
diff --git a/addons/web/static/src/legacy/js/core/menu.js b/addons/web/static/src/legacy/js/core/menu.js
new file mode 100644
index 00000000000..5c2a996e7f3
--- /dev/null
+++ b/addons/web/static/src/legacy/js/core/menu.js
@@ -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'));
+}
diff --git a/addons/website/__manifest__.py b/addons/website/__manifest__.py
index 7badf1e3660..edc42c81698 100644
--- a/addons/website/__manifest__.py
+++ b/addons/website/__manifest__.py
@@ -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',
diff --git a/addons/website/static/src/js/content/auto_hide_menu.js b/addons/website/static/src/js/content/auto_hide_menu.js
new file mode 100644
index 00000000000..f592b9213e7
--- /dev/null
+++ b/addons/website/static/src/js/content/auto_hide_menu.js
@@ -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']
+ });
+ }
+});
diff --git a/addons/website/static/src/js/content/menu.js b/addons/website/static/src/js/content/menu.js
index 3bd8765d6af..1c40c94c4d1 100644
--- a/addons/website/static/src/js/content/menu.js
+++ b/addons/website/static/src/js/content/menu.js
@@ -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).
diff --git a/addons/website/static/src/js/menu/navbar.js b/addons/website/static/src/js/menu/navbar.js
index e316988a76a..b7e6a87b5a7 100644
--- a/addons/website/static/src/js/menu/navbar.js
+++ b/addons/website/static/src/js/menu/navbar.js
@@ -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
diff --git a/addons/website/static/src/scss/website.scss b/addons/website/static/src/scss/website.scss
index 73a5ff9a5ae..af7f2f1f52c 100644
--- a/addons/website/static/src/scss/website.scss
+++ b/addons/website/static/src/scss/website.scss
@@ -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;
}
}
diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml
index 0718e764bc3..9e786e682d9 100644
--- a/addons/website_sale/views/templates.xml
+++ b/addons/website_sale/views/templates.xml
@@ -4,7 +4,7 @@
-
+
My Cart
@@ -124,7 +124,7 @@
-
+