diff options
| author | Benjamin Vray <bvr@odoo.com> | 2019-11-06 15:01:07 +0000 |
|---|---|---|
| committer | qsm-odoo <qsm@odoo.com> | 2020-03-31 18:16:08 +0000 |
| commit | 3defe35a0e4e2097115633797df6898ac2724c66 (patch) | |
| tree | f20fb822b258fde0fefa59045889944d6f5dba6d /addons/website/static/src | |
| parent | 5ba817c453270fa8366397f9767d79072054df20 (diff) | |
[IMP] website, *: add and review header visibility options
*: portal
Adding a choice of several header behaviors (fixed, fade out with
scroll, etc.). Review the way those animations works in mobile/desktop
and now perform them without duplicating the header DOM element.
Also allow to hide of adapt the size of the logo when the page is
scrolled.
Part of https://github.com/odoo/odoo/pull/38950
task-2087641
Co-authored-by: qsm-odoo <qsm@odoo.com>
Diffstat (limited to 'addons/website/static/src')
| -rw-r--r-- | addons/website/static/src/js/content/menu.js | 344 | ||||
| -rw-r--r-- | addons/website/static/src/scss/secondary_variables.scss | 1 | ||||
| -rw-r--r-- | addons/website/static/src/scss/website.edit_mode.scss | 7 | ||||
| -rw-r--r-- | addons/website/static/src/scss/website.scss | 78 |
4 files changed, 353 insertions, 77 deletions
diff --git a/addons/website/static/src/js/content/menu.js b/addons/website/static/src/js/content/menu.js index 611ed40b554..36caa0accbb 100644 --- a/addons/website/static/src/js/content/menu.js +++ b/addons/website/static/src/js/content/menu.js @@ -1,66 +1,66 @@ 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'); -publicWidget.registry.affixMenu = publicWidget.Widget.extend({ - selector: 'header.o_affix_enabled', +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 () { - var def = this._super.apply(this, arguments); - - var self = this; - this.$headerClone = this.$target.clone().addClass('o_header_affix affix o_top_fixed_element').removeClass('o_affix_enabled').removeAttr('id'); - this.$headerClone.insertAfter(this.$target); - this.$headers = this.$target.add(this.$headerClone); - this.$dropdowns = this.$headers.find('.dropdown'); - this.$dropdownMenus = this.$headers.find('.dropdown-menu'); - this.$navbarCollapses = this.$headers.find('.navbar-collapse'); - - this._adaptDefaultOffset(); - wUtils.onceAllImagesLoaded(this.$headerClone).then(function () { - self._adaptDefaultOffset(); - }); + 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'); - // Handle events for the collapse menus - _.each(this.$headerClone.find('[data-toggle="collapse"]'), function (el) { - var $source = $(el); - var targetIDSelector = $source.attr('data-target'); - var $target = self.$headerClone.find(targetIDSelector); - $source.attr('data-target', targetIDSelector + '_clone'); - $target.attr('id', targetIDSelector.substr(1) + '_clone'); - }); - // While scrolling through navbar menus, body should not be scrolled with it - this.$headerClone.find('div.navbar-collapse').on('show.bs.collapse', function () { - $(document.body).addClass('overflow-hidden'); - }).on('hide.bs.collapse', function () { + // 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'); }); - // Window Handlers - $(window).on('resize.affixMenu scroll.affixMenu', _.throttle(this._onWindowUpdate.bind(this), 200)); - setTimeout(this._onWindowUpdate.bind(this), 0); // setTimeout to allow override with advanced stuff... see themes + // 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._updateMainPaddingTopLoop(1)); + this.$el.on('transitionend.BaseAnimatedHeader', () => this._updateMainPaddingTopLoop(-1)); - return def.then(function () { - self.trigger_up('widgets_start_request', { - $target: self.$headerClone, - }); - }); + return this._super(...arguments); }, /** * @override */ destroy: function () { - if (this.$headerClone) { - this.$headerClone.remove(); - $(window).off('.affixMenu'); - } - this._super.apply(this, arguments); + 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); }, //-------------------------------------------------------------------------- @@ -69,16 +69,41 @@ publicWidget.registry.affixMenu = publicWidget.Widget.extend({ /** * @private + * @param {boolean} [useFixed=true] */ - _adaptDefaultOffset: function () { - var bottom = this.$target.offset().top + this._getHeaderHeight(); - this.$headerClone.css('margin-top', Math.min(-200, -bottom) + 'px'); + _toggleFixedHeader: function (useFixed = true) { + this.fixedHeader = useFixed; + this.el.classList.toggle('o_header_affixed', useFixed); + this._updateMainPaddingTop(); }, /** * @private */ - _getHeaderHeight: function () { - return this.$headerClone.outerHeight(); + _updateMainPaddingTop: function () { + this.headerHeight = this.$el.outerHeight(); + if (this.isOverlayHeader) { + return; + } + const headerSize = this.el.classList.contains('o_header_affixed'); + this.$main.css('padding-top', headerSize ? this.headerHeight : ''); + }, + /** + * @private + * @param {integer} [addCount=0] + */ + _updateMainPaddingTopLoop: function (addCount = 0) { + this._updateMainPaddingTop(); + + this._transitionCount += addCount; + this._transitionCount = Math.max(0, this._transitionCount); + + // 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. + clearTimeout(this._paddingLoopTimer); + if (this._transitionCount > 0) { + window.requestAnimationFrame(() => this._updateMainPaddingTopLoop()); + this._paddingLoopTimer = setTimeout(() => this._updateMainPaddingTopLoop(-1), 500); + } }, //-------------------------------------------------------------------------- @@ -86,31 +111,232 @@ publicWidget.registry.affixMenu = publicWidget.Widget.extend({ //-------------------------------------------------------------------------- /** - * Called when the window is resized or scrolled -> updates affix status and - * automatically closes submenus. + * Called when the window is scrolled * * @private + * @param {integer} scroll */ - _onWindowUpdate: function () { - if (this.$navbarCollapses.hasClass('show')) { - return; + _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'); } - var wOffset = $(window).scrollTop(); - var hOffset = this.$target.scrollTop(); - this.$headerClone.toggleClass('affixed', wOffset > (hOffset + 300)); + // 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; + } // Reset opened menus - this.$dropdowns.add(this.$dropdownMenus).removeClass('show'); + if (this.$navbarCollapses.hasClass('show')) { + return; + } + this.$dropdowns.removeClass('show'); this.$navbarCollapses.removeClass('show').attr('aria-expanded', false); }, + /** + * Called when the window is resized + * + * @private + */ + _updateHeaderOnResize: function () { + 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 + //-------------------------------------------------------------------------- + + /** + * Called when the window is scrolled + * + * @private + * @param {integer} scroll + */ + _updateHeaderOnScroll: function (scroll) { + this._super(...arguments); + + const mainPosScrolled = (scroll > this.headerHeight); + const reachPosScrolled = (scroll > this.scrolledPoint); + + // 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, -100%)'); + this.fixedHeaderShow = reachPosScrolled; + } + }, +}); + +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) { + if (!this.$el.hasClass('o_header_affixed')) { + this._toggleFixedHeader(true); + } + } else { + this._toggleFixedHeader(false); + } + }, +}); + +const BaseDisappearingHeader = publicWidget.registry.FixedHeader.extend({ + /** + * @override + */ + init: function () { + this._super(...arguments); + this.scrollingDownwards = true; + this.hiddenHeader = false; + this.position = 0; + this.checkPoint = 0; + }, + /** + * @override + */ + destroy: function () { + this._showHeader(); + this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @abstract + */ + _hideHeader: function () {}, + /** + * @private + */ + _showHeader: function () {}, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @override + */ + _updateHeaderOnScroll: function (scroll) { + this._super(...arguments); + + const scrollingDownwards = (scroll > this.position); + if (scrollingDownwards !== this.scrollingDownwards) { + this.checkPoint = scroll; + } + + if (scrollingDownwards) { + if (!this.hiddenHeader && scroll - this.checkPoint > 200) { + this.hiddenHeader = true; + this._hideHeader(); + } + } else { + if (this.hiddenHeader && scroll - this.checkPoint < -100) { + this.hiddenHeader = false; + this._showHeader(); + } + } + + this.scrollingDownwards = scrollingDownwards; + this.position = scroll; + }, +}); + +publicWidget.registry.DisappearingHeader = BaseDisappearingHeader.extend({ + selector: 'header.o_header_disappears', + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _hideHeader: function () { + this.$el.css('transform', 'translate(0, -100%)'); + }, + /** + * @override + */ + _showHeader: function () { + this.$el.css('transform', ''); + }, +}); + +publicWidget.registry.FadeOutHeader = BaseDisappearingHeader.extend({ + selector: 'header.o_header_fade_out', + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _hideHeader: function () { + this.$el.stop(false, true).fadeOut(); + }, + /** + * @override + */ + _showHeader: function () { + this.$el.stop(false, true).fadeIn(); + }, }); /** * Auto adapt the header layout so that elements are not wrapped on a new line. - * - * Note: this works well with the affixMenu... by chance (autohideMenu is called - * after alphabetically). */ publicWidget.registry.autohideMenu = publicWidget.Widget.extend({ selector: 'header #top_menu', @@ -225,7 +451,7 @@ publicWidget.registry.menuDirection = publicWidget.Widget.extend({ // If can't open in the current direction because it would overflow the // window, change the direction. But if the other direction would do the // same, change back the direction. - for (var i = 0 ; i < 2 ; i++) { + for (var i = 0; i < 2; i++) { if (!this._checkOpening(alignment, liOffset, liWidth, menuWidth, windowWidth)) { alignment = (alignment === 'left' ? 'right' : 'left'); } diff --git a/addons/website/static/src/scss/secondary_variables.scss b/addons/website/static/src/scss/secondary_variables.scss index 5f3d6704c85..7eb722f34e8 100644 --- a/addons/website/static/src/scss/secondary_variables.scss +++ b/addons/website/static/src/scss/secondary_variables.scss @@ -77,6 +77,7 @@ $-with-support-theme-fonts: (); $o-theme-fonts: $-with-support-theme-fonts; $o-theme-navbar-logo-height: o-website-value('logo-height') !default; +$o-theme-navbar-fixed-logo-height: o-website-value('fixed-logo-height') !default; $o-theme-font: nth($o-theme-fonts, o-website-value('font-number')) !default; $o-theme-headings-font: nth($o-theme-fonts, o-website-value('headings-font-number')) !default; $o-theme-navbar-font: nth($o-theme-fonts, o-website-value('navbar-font-number')) !default; diff --git a/addons/website/static/src/scss/website.edit_mode.scss b/addons/website/static/src/scss/website.edit_mode.scss index 495873955fd..c076b050fd0 100644 --- a/addons/website/static/src/scss/website.edit_mode.scss +++ b/addons/website/static/src/scss/website.edit_mode.scss @@ -161,3 +161,10 @@ table.editorbar-panel { } } } + +.editor_has_snippets { + .o_header_affixed { + left: $o-we-sidebar-width !important; + top: var(--o-we-toolbar-height) !important; + } +} diff --git a/addons/website/static/src/scss/website.scss b/addons/website/static/src/scss/website.scss index 6c76dac6179..cc7af6543bd 100644 --- a/addons/website/static/src/scss/website.scss +++ b/addons/website/static/src/scss/website.scss @@ -55,6 +55,7 @@ $-font-numbers: ( --h6: #{$-h6-color}; --logo-height: #{$o-theme-navbar-logo-height}; + --fixed-logo-height: #{$o-theme-navbar-fixed-logo-height}; --number-of-fonts: #{length($o-theme-fonts)}; } @@ -643,28 +644,50 @@ table.table_desc tr td { // Layout // -// Header -.o_header_affix { +$-transition-duration: 200ms; + +// Affixed Header +.o_header_affixed { display: block; @include o-position-absolute(0, 0, auto, 0); position: fixed; z-index: $zindex-modal - 20; background: $light; - margin-top: -999px; - transition: margin-top 500ms ease 0s; - @include media-breakpoint-down(sm) { - .navbar-collapse { - @if o-website-value('header-template') != 'logo_hamburger' { - max-height: 70vh; - } + + &:not(.o_header_no_transition) { + transition: transform $-transition-duration; + } + + .navbar-collapse { + @if o-website-value('header-template') != 'logo_hamburger' { + max-height: 70vh; + } + &.show { overflow-y: auto; } } - &.affixed { - margin-top: 0px !important; // the default margin-top is adapted in JS + &.o_header_is_scrolled { + .navbar-brand { + font-size: $o-theme-navbar-fixed-logo-height / $line-height-base; + + img { + height: $o-theme-navbar-fixed-logo-height; + } + } + @include media-breakpoint-up(md) { + @if (index(('menu_logo_below', 'logo_menu_below'), o-website-value('header-template'))) != null { + .navbar-brand { + font-size: 0px; + opacity: 0; + img { + height: 0px; + } + } + } + } } } -#oe_main_menu_navbar + #wrapwrap .o_header_affix { +#oe_main_menu_navbar + #wrapwrap .o_header_affixed { top: $o-navbar-height; } @@ -694,6 +717,17 @@ table.table_desc tr td { // Header templates header { + &:not(.o_header_no_transition) { + .navbar-brand { + transition: font-size $-transition-duration, opacity $-transition-duration ease-out; + + img { + transition: height $-transition-duration; + } + } + } + + z-index: 1; @include media-breakpoint-up(md) { // Allow to center the logo, ignoring the toggler space .o_navbar_toggler_container { @@ -715,14 +749,20 @@ header { .o_navbar_toggler_container { direction: rtl; } - .container { + &:not(.o_header_is_scrolled) .container { padding-top: $spacer; padding-bottom: $spacer; + } + .container { + transition: padding 200ms; #top_menu.navbar-nav { height: 90vh; padding-top : 10vh; } + .dropdown-menu { + text-align: center; + } } } } @else if index(('logo_menu_below', 'menu_logo_below'), o-website-value('header-template')) { @@ -741,10 +781,12 @@ header { margin: auto; } .navbar-brand { - @if o-website-value('header-template') == 'menu_logo_below' { - margin-top: $navbar-padding-y; - } @else { - margin-bottom: $navbar-padding-y; + @include media-breakpoint-up(md) { + @if o-website-value('header-template') == 'menu_logo_below' { + margin-top: $navbar-padding-y; + } @else { + margin-bottom: $navbar-padding-y; + } } } @if o-website-value('header-template') == 'menu_logo_below' { @@ -788,7 +830,7 @@ header { } #wrapwrap.o_header_overlay { - > header:not(.o_header_affix) { + > header:not(.o_header_affixed) { @include o-position-absolute(0, 0, auto, 0); z-index: 1000; |
