summaryrefslogtreecommitdiff
path: root/addons/website/static/src/js/content/menu.js
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/website/static/src/js/content/menu.js
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (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.js642
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,
+};
+});