[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>
This commit is contained in:
Benjamin Vray
2020-03-31 18:16:08 +00:00
committed by qsm-odoo
co-authored by qsm-odoo
parent 5ba817c453
commit 3defe35a0e
10 changed files with 423 additions and 106 deletions
+1 -1
View File
@@ -5,6 +5,7 @@
// ====== Variables =========
$o-theme-navbar-logo-height: $nav-link-height !default;
$o-theme-navbar-fixed-logo-height: $nav-link-height !default;
// Portal toolbar (filters, search bar)
$o-portal-mobile-toolbar: true; // Enable/Disable custom design
@@ -47,7 +48,6 @@ header {
height: $o-theme-navbar-logo-height;
@include media-breakpoint-down(sm) {
height: auto;
max-height: min($o-theme-navbar-logo-height, 5rem);
}
}
+286 -60
View File
@@ -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);
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');
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();
});
// 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 () {
_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;
}
// Reset opened menus
if (this.$navbarCollapses.hasClass('show')) {
return;
}
var wOffset = $(window).scrollTop();
var hOffset = this.$target.scrollTop();
this.$headerClone.toggleClass('affixed', wOffset > (hOffset + 300));
// Reset opened menus
this.$dropdowns.add(this.$dropdownMenus).removeClass('show');
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');
}
@@ -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;
@@ -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;
}
}
+60 -18
View File
@@ -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;
+45 -3
View File
@@ -389,13 +389,23 @@
data-no-check="true"
groups="website.group_website_designer">
<we-checkbox string="Show Sign In" data-customize-website-views="portal.portal_show_sign_in" data-reload="/"/>
<we-checkbox string="Fixed Top Menu" data-customize-website-views="website.affix_top_menu" data-reload="/"/>
<we-colorpicker string="Background Color" data-customize-website-color="" data-color="menu"/>
<we-select string="Logo" data-reload="/">
<we-button data-customize-website-views="website.website_name">Text</we-button>
<we-button data-customize-website-views="website.layout_logo_show">Image</we-button>
</we-select>
<we-input string="Logo Height" data-customize-website-variable="null" data-variable="logo-height" data-unit="px" data-save-unit="rem"/>
<we-input string="Logo Height"
data-customize-website-variable="null"
data-variable="logo-height"
data-unit="px"
data-save-unit="rem"/>
<we-input id="option_logo_height_scrolled"
string="Logo Height (Scrolled)"
data-customize-website-variable="null"
data-variable="fixed-logo-height"
data-unit="px"
data-save-unit="rem"
data-dependencies="!header_visibility_scroll_opt"/>
<we-row string="Font">
<we-fontfamilypicker data-variable="navbar-font-number"/>
<we-input data-customize-website-variable="null" data-variable="header-font-size" data-unit="px" data-save-unit="rem"/>
@@ -427,18 +437,50 @@
data-customize-website-variable="logo_hamburger"
string="Logo - Hamburger (cover)"/>
</we-select>
<we-select id="header_visibilities_list" string="Header Visibility">
<we-button id="option_header_visibility_standard"
data-name="header_visibility_standard_opt"
data-select-class="o_header_standard"
data-customize-website-views="website.header_visibility_standard"
string="Standard"/>
<we-button id="option_header_visibility_scroll"
data-name="header_visibility_scroll_opt"
data-select-class=""
data-customize-website-views=""
string="Scroll"/>
<we-button id="option_header_visibility_fixed"
data-name="header_visibility_fixed_opt"
data-select-class="o_header_fixed"
data-customize-website-views="website.header_visibility_fixed"
string="Fixed"/>
<we-button id="option_header_visibility_disappears"
data-name="header_visibility_disappears_opt"
data-select-class="o_header_disappears"
data-customize-website-views="website.header_visibility_disappears"
string="Disappears"/>
<we-button id="option_header_visibility_fade_out"
data-name="header_visibility_fade_out_opt"
data-select-class="o_header_fade_out"
data-customize-website-views="website.header_visibility_fade_out"
string="Fade Out"/>
</we-select>
</div>
<div data-js="topMenuTransparency"
data-selector="[data-main-object^='website.page('] #wrapwrap > header"
data-no-check="true">
<we-checkbox string="Transparent on this page" data-transparent="true" data-no-preview="true"/>
<we-checkbox id="option_header_transparent"
string="Transparent on this page"
data-name="header_transparent_opt"
data-transparent="true"
data-no-preview="true"/>
</div>
<div data-js="topMenuColor"
data-selector="[data-main-object^='website.page('] #wrapwrap > header"
data-no-check="true">
<we-colorpicker string="Transparent color"
id="option_header_transparent_color"
data-select-style="true"
data-css-property="background-color"
data-color-prefix="bg-"
+20 -2
View File
@@ -244,9 +244,27 @@
</xpath>
</template>
<template id="affix_top_menu" inherit_id="website.layout" name="Fixed Top Menu">
<template id="header_visibility_standard" inherit_id="website.layout" name="Header Visibility Standard">
<xpath expr="//header" position="attributes">
<attribute name="t-attf-class" add="#{'o_affix_enabled'}" separator=" "/>
<attribute name="t-attf-class" add="o_header_standard o_top_fixed_element" separator=" "/>
</xpath>
</template>
<template id="header_visibility_fixed" inherit_id="website.layout" name="Header Visibility Fixed" active="False">
<xpath expr="//header" position="attributes">
<attribute name="t-attf-class" add="o_header_fixed o_top_fixed_element" separator=" "/>
</xpath>
</template>
<template id="header_visibility_disappears" inherit_id="website.layout" name="Header Visibility Disappears" active="False">
<xpath expr="//header" position="attributes">
<attribute name="t-attf-class" add="o_header_disappears o_top_fixed_element" separator=" "/>
</xpath>
</template>
<template id="header_visibility_fade_out" inherit_id="website.layout" name="Header Visibility Fade Out" active="False">
<xpath expr="//header" position="attributes">
<attribute name="t-attf-class" add="o_header_fade_out o_top_fixed_element" separator=" "/>
</xpath>
</template>
@@ -150,7 +150,7 @@ publicWidget.registry.productsRecentlyViewedSnippet = publicWidget.Widget.extend
},
}).then(function (data) {
wSaleUtils.updateCartNavBar(data);
var $navButton = wSaleUtils.getNavBarButton('.o_wsale_my_cart');
var $navButton = $('header .o_wsale_my_cart').first();
var fetch = self._fetch();
var animation = wSaleUtils.animateClone($navButton, $(ev.currentTarget).parents('.o_carousel_product_card'), 25, 40);
Promise.all([fetch, animation]).then(function (values) {
@@ -1,24 +1,6 @@
odoo.define('website_sale.utils', function (require) {
'use strict';
/**
* Gets the element in the navbar currently displayed.
* Depending on the scroll position, it could either be the one in the main
* top bar or the one in the affixed navbar.
*
* @private
* @param {string} selector
* @returns {jQuery}
*/
function getNavBarButton(selector) {
var $affixedHeaderButton = $('header.affixed ' + selector);
if ($affixedHeaderButton.length) {
return $affixedHeaderButton;
} else {
return $('header ' + selector).first();
}
}
function animateClone($cart, $elem, offsetTop, offsetLeft) {
$cart.find('.o_animate_blink').addClass('o_red_highlight o_shadow_animation').delay(500).queue(function () {
$(this).removeClass("o_shadow_animation").dequeue();
@@ -72,7 +54,6 @@ function updateCartNavBar(data) {
return {
animateClone: animateClone,
getNavBarButton: getNavBarButton,
updateCartNavBar: updateCartNavBar,
};
});
@@ -105,7 +105,7 @@ publicWidget.registry.ProductWishlist = publicWidget.Widget.extend(VariantMixin,
product_id: productId,
},
}).then(function () {
var $navButton = wSaleUtils.getNavBarButton('.o_wsale_my_wish');
var $navButton = $('header .o_wsale_my_wish').first();
self.wishlistProductIDs.push(productId);
self._updateWishlistView();
wSaleUtils.animateClone($navButton, $el.closest('form'), 25, 40);
@@ -157,7 +157,7 @@ publicWidget.registry.ProductWishlist = publicWidget.Widget.extend(VariantMixin,
* @private
*/
_addOrMoveWish: function (e) {
var $navButton = wSaleUtils.getNavBarButton('.o_wsale_my_cart');
var $navButton = $('header .o_wsale_my_cart').first();
var tr = $(e.currentTarget).parents('tr');
var product = tr.data('product-id');
$('.o_wsale_my_cart').removeClass('d-none');