diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/website/static/src/js/content/menu.js | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/website/static/src/js/content/menu.js')
| -rw-r--r-- | addons/website/static/src/js/content/menu.js | 642 |
1 files changed, 642 insertions, 0 deletions
diff --git a/addons/website/static/src/js/content/menu.js b/addons/website/static/src/js/content/menu.js new file mode 100644 index 00000000..71f74ab5 --- /dev/null +++ b/addons/website/static/src/js/content/menu.js @@ -0,0 +1,642 @@ +odoo.define('website.content.menu', function (require) { +'use strict'; + +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({ + disabledInEditableMode: false, + effects: [{ + startEvents: 'scroll', + update: '_updateHeaderOnScroll', + }, { + startEvents: 'resize', + update: '_updateHeaderOnResize', + }], + + /** + * @constructor + */ + init: function () { + this._super(...arguments); + this.fixedHeader = false; + this.scrolledPoint = 0; + this.hasScrolled = false; + }, + /** + * @override + */ + start: function () { + this.$main = this.$el.next('main'); + this.isOverlayHeader = !!this.$el.closest('.o_header_overlay, .o_header_overlay_theme').length; + this.$dropdowns = this.$el.find('.dropdown, .dropdown-menu'); + this.$navbarCollapses = this.$el.find('.navbar-collapse'); + + // While scrolling through navbar menus on medium devices, body should not be scrolled with it + this.$navbarCollapses.on('show.bs.collapse.BaseAnimatedHeader', function () { + if (config.device.size_class <= config.device.SIZES.SM) { + $(document.body).addClass('overflow-hidden'); + } + }).on('hide.bs.collapse.BaseAnimatedHeader', function () { + $(document.body).removeClass('overflow-hidden'); + }); + + // We can rely on transitionend which is well supported but not on + // transitionstart, so we listen to a custom odoo event. + this._transitionCount = 0; + this.$el.on('odoo-transitionstart.BaseAnimatedHeader', () => this._adaptToHeaderChangeLoop(1)); + this.$el.on('transitionend.BaseAnimatedHeader', () => this._adaptToHeaderChangeLoop(-1)); + + return this._super(...arguments); + }, + /** + * @override + */ + destroy: function () { + this._toggleFixedHeader(false); + this.$el.removeClass('o_header_affixed o_header_is_scrolled o_header_no_transition'); + this.$navbarCollapses.off('.BaseAnimatedHeader'); + this.$el.off('.BaseAnimatedHeader'); + this._super(...arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + */ + _adaptFixedHeaderPosition() { + dom.compensateScrollbar(this.el, this.fixedHeader, false, 'right'); + }, + /** + * @private + */ + _adaptToHeaderChange: function () { + this._updateMainPaddingTop(); + this.el.classList.toggle('o_top_fixed_element', this.fixedHeader && this._isShown()); + + for (const callback of extraMenuUpdateCallbacks) { + callback(); + } + }, + /** + * @private + * @param {integer} [addCount=0] + */ + _adaptToHeaderChangeLoop: function (addCount = 0) { + this._adaptToHeaderChange(); + + this._transitionCount += addCount; + this._transitionCount = Math.max(0, this._transitionCount); + + // As long as we detected a transition start without its related + // transition end, keep updating the main padding top. + if (this._transitionCount > 0) { + window.requestAnimationFrame(() => this._adaptToHeaderChangeLoop()); + + // The normal case would be to have the transitionend event to be + // fired but we cannot rely on it, so we use a timeout as fallback. + if (addCount !== 0) { + clearTimeout(this._changeLoopTimer); + this._changeLoopTimer = setTimeout(() => { + this._adaptToHeaderChangeLoop(-this._transitionCount); + }, 500); + } + } else { + // When we detected all transitionend events, we need to stop the + // setTimeout fallback. + clearTimeout(this._changeLoopTimer); + } + }, + /** + * @private + */ + _computeTopGap() { + return 0; + }, + /** + * @private + */ + _isShown() { + return true; + }, + /** + * @private + * @param {boolean} [useFixed=true] + */ + _toggleFixedHeader: function (useFixed = true) { + this.fixedHeader = useFixed; + this._adaptToHeaderChange(); + this.el.classList.toggle('o_header_affixed', useFixed); + this._adaptFixedHeaderPosition(); + }, + /** + * @private + */ + _updateMainPaddingTop: function () { + this.headerHeight = this.$el.outerHeight(); + this.topGap = this._computeTopGap(); + + if (this.isOverlayHeader) { + return; + } + this.$main.css('padding-top', this.fixedHeader ? this.headerHeight : ''); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Called when the window is scrolled + * + * @private + * @param {integer} scroll + */ + _updateHeaderOnScroll: function (scroll) { + // Disable css transition if refresh with scrollTop > 0 + if (!this.hasScrolled) { + this.hasScrolled = true; + if (scroll > 0) { + this.$el.addClass('o_header_no_transition'); + } + } else { + this.$el.removeClass('o_header_no_transition'); + } + + // Indicates the page is scrolled, the logo size is changed. + const headerIsScrolled = (scroll > this.scrolledPoint); + if (this.headerIsScrolled !== headerIsScrolled) { + this.el.classList.toggle('o_header_is_scrolled', headerIsScrolled); + this.$el.trigger('odoo-transitionstart'); + this.headerIsScrolled = headerIsScrolled; + } + + // Close opened menus + this.$dropdowns.removeClass('show'); + this.$navbarCollapses.removeClass('show').attr('aria-expanded', false); + }, + /** + * Called when the window is resized + * + * @private + */ + _updateHeaderOnResize: function () { + this._adaptFixedHeaderPosition(); + if (document.body.classList.contains('overflow-hidden') + && config.device.size_class > config.device.SIZES.SM) { + document.body.classList.remove('overflow-hidden'); + this.$el.find('.navbar-collapse').removeClass('show'); + } + }, +}); + +publicWidget.registry.StandardAffixedHeader = BaseAnimatedHeader.extend({ + selector: 'header.o_header_standard:not(.o_header_sidebar)', + + /** + * @constructor + */ + init: function () { + this._super(...arguments); + this.fixedHeaderShow = false; + this.scrolledPoint = 300; + }, + /** + * @override + */ + start: function () { + this.headerHeight = this.$el.outerHeight(); + return this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @override + */ + _isShown() { + return !this.fixedHeader || this.fixedHeaderShow; + }, + /** + * Called when the window is scrolled + * + * @private + * @param {integer} scroll + */ + _updateHeaderOnScroll: function (scroll) { + this._super(...arguments); + + const mainPosScrolled = (scroll > this.headerHeight + this.topGap); + const reachPosScrolled = (scroll > this.scrolledPoint + this.topGap); + + // Switch between static/fixed position of the header + if (this.fixedHeader !== mainPosScrolled) { + this.$el.css('transform', mainPosScrolled ? 'translate(0, -100%)' : ''); + void this.$el[0].offsetWidth; // Force a paint refresh + this._toggleFixedHeader(mainPosScrolled); + } + // Show/hide header + if (this.fixedHeaderShow !== reachPosScrolled) { + this.$el.css('transform', reachPosScrolled ? `translate(0, -${this.topGap}px)` : 'translate(0, -100%)'); + this.fixedHeaderShow = reachPosScrolled; + this._adaptToHeaderChange(); + } + }, +}); + +publicWidget.registry.FixedHeader = BaseAnimatedHeader.extend({ + selector: 'header.o_header_fixed', + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @override + */ + _updateHeaderOnScroll: function (scroll) { + this._super(...arguments); + // Need to be 'unfixed' when the window is not scrolled so that the + // transparent menu option still works. + if (scroll > (this.scrolledPoint + this.topGap)) { + if (!this.$el.hasClass('o_header_affixed')) { + this.$el.css('transform', `translate(0, -${this.topGap}px)`); + void this.$el[0].offsetWidth; // Force a paint refresh + this._toggleFixedHeader(true); + } + } else { + this._toggleFixedHeader(false); + void this.$el[0].offsetWidth; // Force a paint refresh + this.$el.css('transform', ''); + } + }, +}); + +const BaseDisappearingHeader = publicWidget.registry.FixedHeader.extend({ + /** + * @override + */ + init: function () { + this._super(...arguments); + this.scrollingDownwards = true; + this.hiddenHeader = false; + this.position = 0; + this.atTop = true; + this.checkPoint = 0; + this.scrollOffsetLimit = 200; + }, + /** + * @override + */ + destroy: function () { + this._showHeader(); + this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + */ + _hideHeader: function () { + this.$el.trigger('odoo-transitionstart'); + }, + /** + * @override + */ + _isShown() { + return !this.fixedHeader || !this.hiddenHeader; + }, + /** + * @private + */ + _showHeader: function () { + this.$el.trigger('odoo-transitionstart'); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @override + */ + _updateHeaderOnScroll: function (scroll) { + this._super(...arguments); + + const scrollingDownwards = (scroll > this.position); + const atTop = (scroll <= 0); + if (scrollingDownwards !== this.scrollingDownwards) { + this.checkPoint = scroll; + } + + this.scrollingDownwards = scrollingDownwards; + this.position = scroll; + this.atTop = atTop; + + if (scrollingDownwards) { + if (!this.hiddenHeader && scroll - this.checkPoint > (this.scrollOffsetLimit + this.topGap)) { + this.hiddenHeader = true; + this._hideHeader(); + } + } else { + if (this.hiddenHeader && scroll - this.checkPoint < -(this.scrollOffsetLimit + this.topGap) / 2) { + this.hiddenHeader = false; + this._showHeader(); + } + } + + if (atTop && !this.atTop) { + // Force reshowing the invisible-on-scroll sections when reaching + // the top again + this._showHeader(); + } + }, +}); + +publicWidget.registry.DisappearingHeader = BaseDisappearingHeader.extend({ + selector: 'header.o_header_disappears', + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _hideHeader: function () { + this._super(...arguments); + this.$el.css('transform', 'translate(0, -100%)'); + }, + /** + * @override + */ + _showHeader: function () { + this._super(...arguments); + this.$el.css('transform', this.atTop ? '' : `translate(0, -${this.topGap}px)`); + }, +}); + +publicWidget.registry.FadeOutHeader = BaseDisappearingHeader.extend({ + selector: 'header.o_header_fade_out', + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _hideHeader: function () { + this._super(...arguments); + this.$el.stop(false, true).fadeOut(); + }, + /** + * @override + */ + _showHeader: function () { + this._super(...arguments); + this.$el.css('transform', this.atTop ? '' : `translate(0, -${this.topGap}px)`); + this.$el.stop(false, true).fadeIn(); + }, +}); + +/** + * 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). + * + * @todo check bootstrap v4: maybe handled automatically now ? + */ +publicWidget.registry.menuDirection = publicWidget.Widget.extend({ + selector: 'header .navbar .nav', + disabledInEditableMode: false, + events: { + 'show.bs.dropdown': '_onDropdownShow', + }, + + /** + * @override + */ + start: function () { + this.defaultAlignment = this.$el.is('.ml-auto, .ml-auto ~ *') ? '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 + * @param {integer} pageWidth + * @returns {boolean} + */ + _checkOpening: function (alignment, liOffset, liWidth, menuWidth, pageWidth) { + if (alignment === 'left') { + // Check if ok to open the dropdown to the right (no window overflow) + return (liOffset + menuWidth <= pageWidth); + } 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 pageWidth = $('#wrapwrap').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 + // page, 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, pageWidth)) { + alignment = (alignment === 'left' ? 'right' : 'left'); + } + } + + $menu.addClass('dropdown-menu-' + alignment); + }, +}); + +publicWidget.registry.hoverableDropdown = animations.Animation.extend({ + selector: 'header.o_hoverable_dropdown', + disabledInEditableMode: false, + effects: [{ + startEvents: 'resize', + update: '_dropdownHover', + }], + events: { + 'mouseenter .dropdown': '_onMouseEnter', + 'mouseleave .dropdown': '_onMouseLeave', + }, + + /** + * @override + */ + start: function () { + this.$dropdownMenus = this.$el.find('.dropdown-menu'); + this.$dropdownToggles = this.$el.find('.dropdown-toggle'); + this._dropdownHover(); + return this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + */ + _dropdownHover: function () { + if (config.device.size_class > config.device.SIZES.SM) { + this.$dropdownMenus.css('margin-top', '0'); + this.$dropdownMenus.css('top', 'unset'); + } else { + this.$dropdownMenus.css('margin-top', ''); + this.$dropdownMenus.css('top', ''); + } + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {Event} ev + */ + _onMouseEnter: function (ev) { + if (config.device.size_class <= config.device.SIZES.SM) { + return; + } + + const $dropdown = $(ev.currentTarget); + $dropdown.addClass('show'); + $dropdown.find(this.$dropdownToggles).attr('aria-expanded', 'true'); + $dropdown.find(this.$dropdownMenus).addClass('show'); + }, + /** + * @private + * @param {Event} ev + */ + _onMouseLeave: function (ev) { + if (config.device.size_class <= config.device.SIZES.SM) { + return; + } + + const $dropdown = $(ev.currentTarget); + $dropdown.removeClass('show'); + $dropdown.find(this.$dropdownToggles).attr('aria-expanded', 'false'); + $dropdown.find(this.$dropdownMenus).removeClass('show'); + }, +}); + +publicWidget.registry.HeaderMainCollapse = publicWidget.Widget.extend({ + selector: 'header#top', + events: { + 'show.bs.collapse #top_menu_collapse': '_onCollapseShow', + 'hidden.bs.collapse #top_menu_collapse': '_onCollapseHidden', + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onCollapseShow() { + this.el.classList.add('o_top_menu_collapse_shown'); + }, + /** + * @private + */ + _onCollapseHidden() { + this.el.classList.remove('o_top_menu_collapse_shown'); + }, +}); + +return { + extraMenuUpdateCallbacks: extraMenuUpdateCallbacks, +}; +}); |
