mirror of
https://github.com/Combodo/iTop.git
synced 2026-02-13 07:24:13 +01:00
N°4021 - Add support for tab container for the sticky header
This commit is contained in:
@@ -13,7 +13,9 @@ $ibo-panel-with-tab-container--padding-top: -1 * ($ibo-panel--body--padding-top
|
||||
$ibo-panel-with-tab-container--margin-x: -1 * $ibo-panel--body--padding-x !default;
|
||||
$ibo-panel-with-tab-container--margin-bottom: -1 * $ibo-panel--body--padding-bottom !default;
|
||||
|
||||
// Note: We use the child ">" selector to ensure this applies only the child tab container, not another one that would be nested
|
||||
$ibo-panel-with-tab-container--tab-toggler--font-size--is-sticking: $ibo-font-size-100 !default;
|
||||
|
||||
// Note: We use the child ">" selector to ensure this applies only to the child tab container, not another one that would be nested
|
||||
.ibo-panel {
|
||||
> .ibo-panel--body {
|
||||
> .ibo-tab-container {
|
||||
@@ -59,4 +61,27 @@ $ibo-panel-with-tab-container--margin-bottom: -1 * $ibo-panel--body--padding-bot
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Sticky header rules */
|
||||
&.ibo-has-sticky-header {
|
||||
> .ibo-panel--body {
|
||||
> .ibo-tab-container {
|
||||
> .ibo-tab-container--tabs-list.ibo-is-sticking {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
&:not(.ibo-is-vertical){
|
||||
> .ibo-tab-container--tabs-list.ibo-is-sticking {
|
||||
padding-left: 0;
|
||||
|
||||
.ibo-tab-container--tab-toggler,
|
||||
.ibo-tab-container--extra-tabs-list-toggler {
|
||||
font-size: $ibo-panel-with-tab-container--tab-toggler--font-size--is-sticking;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,6 @@
|
||||
*/
|
||||
|
||||
/* SCSS variables */
|
||||
$ibo-panel--spacing-top: 24px !default;
|
||||
|
||||
/* - Base variables for the block */
|
||||
$ibo-panel--base-border-size: 1px !default;
|
||||
$ibo-panel--base-border-style: solid !default;
|
||||
@@ -35,18 +33,14 @@ $ibo-panel--base-transition: $ibo-panel--base-transition-property $ibo-panel--ba
|
||||
}
|
||||
|
||||
/* - Specific variables for the block */
|
||||
$ibo-panel--header--margin-bottom: 4px !default;
|
||||
|
||||
$ibo-panel--header--background-color--is-sticking: $ibo-color-grey-100 !default;
|
||||
$ibo-panel--header--border--is-sticking: $ibo-panel--base-border !default;
|
||||
|
||||
$ibo-panel--header--padding-y--is-sticking: 4px !default;
|
||||
$ibo-panel--spacing-top: 24px !default;
|
||||
|
||||
$ibo-panel--highlight--width: 100% !default;
|
||||
$ibo-panel--highlight--height: 8px !default;
|
||||
$ibo-panel--highlight--border-radius: $ibo-border-radius-400 $ibo-border-radius-400 0 0 !default;
|
||||
$ibo-panel--highlight--padding-bottom: $ibo-panel--highlight--height !default;
|
||||
|
||||
$ibo-panel--body--z-index: 1 !default;
|
||||
$ibo-panel--body--background-color: $ibo-color-white-100 !default;
|
||||
$ibo-panel--body--padding-bottom: 24px !default;
|
||||
$ibo-panel--body--padding-top: $ibo-panel--body--padding-bottom + $ibo-panel--highlight--height !default;
|
||||
@@ -54,6 +48,12 @@ $ibo-panel--body--padding-x: 16px !default;
|
||||
$ibo-panel--body--border-radius: $ibo-border-radius-500 !default;
|
||||
$ibo-panel--body--border: $ibo-panel--base-border !default;
|
||||
|
||||
$ibo-panel--header--z-index: $ibo-panel--body--z-index + 1 !default; /* Should always be above the body */
|
||||
$ibo-panel--header--margin-bottom: 4px !default;
|
||||
$ibo-panel--header--background-color--is-sticking: $ibo-color-grey-100 !default;
|
||||
$ibo-panel--header--border--is-sticking: $ibo-panel--base-border !default;
|
||||
$ibo-panel--header--padding-y--is-sticking: 4px !default;
|
||||
|
||||
$ibo-panel--icon--size: 48px !default;
|
||||
$ibo-panel--icon--spacing: 16px !default;
|
||||
$ibo-panel--icon--size-as-medallion: 72px !default;
|
||||
@@ -163,6 +163,8 @@ $ibo-panel-colors: (
|
||||
}
|
||||
|
||||
.ibo-panel--header {
|
||||
position: relative;
|
||||
z-index: $ibo-panel--header--z-index;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
@@ -217,6 +219,7 @@ $ibo-panel-colors: (
|
||||
|
||||
.ibo-panel--body {
|
||||
position: relative;
|
||||
z-index: $ibo-panel--body--z-index;
|
||||
padding: $ibo-panel--body--padding-top $ibo-panel--body--padding-x $ibo-panel--body--padding-bottom $ibo-panel--body--padding-x;
|
||||
background-color: $ibo-panel--body--background-color;
|
||||
border: $ibo-panel--body--border;
|
||||
@@ -262,8 +265,8 @@ $ibo-panel-colors: (
|
||||
// Note: Direct child selector is mandatory, otherwise a panel within a panel could be affected too when it shouldn't (eg. dashboard in an object, n:n panel)
|
||||
> .ibo-panel--header {
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
top: 0; /* Default, stick on the top of the panel. Custom integrations should be done in the "blocks-integrations" partials */
|
||||
border: transparent; /* To avoid visual glitch during transition, otherwise the border looks black first */
|
||||
|
||||
/* All transitions should have a specific duration except for the header's "top" property otherwise it feels laggy */
|
||||
/* - The header itself */
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
$(function () {
|
||||
// the widget definition, where 'itop' is the namespace,
|
||||
// 'panel' the widget name
|
||||
$.widget('itop.panel',
|
||||
$.widget('itop.panel', $.itop.ui_block,
|
||||
{
|
||||
// default options
|
||||
options:
|
||||
@@ -33,17 +33,19 @@ $(function () {
|
||||
css_classes:
|
||||
{
|
||||
has_sticky_header: 'ibo-has-sticky-header',
|
||||
is_sticking: 'ibo-is-sticking',
|
||||
sticky_sentinel: 'ibo-sticky-sentinel',
|
||||
sticky_sentinel_top: 'ibo-sticky-sentinel-top',
|
||||
},
|
||||
js_selectors:
|
||||
{
|
||||
modal: '.ui-dialog',
|
||||
modal_content: '.ui-dialog-content',
|
||||
js_selectors: {
|
||||
global: {},
|
||||
block: {
|
||||
panel_header: '[data-role="ibo-panel--header"]:first',
|
||||
panel_header_sticky_sentinel_top: '[data-role="ibo-panel--header--sticky-sentinel-top"]',
|
||||
},
|
||||
panel_header_sticky_sentinel_top: '[data-role="ibo-panel--header--sticky-sentinel-top"]:first',
|
||||
panel_body: '[data-role="ibo-panel--body"]:first',
|
||||
tab_container: '[data-role="ibo-tab-container"]:first',
|
||||
tab_container_tabs_list: '[data-role="ibo-tab-container--tabs-list"]:first',
|
||||
}
|
||||
},
|
||||
// {ScrollMagic.Controller} SM controller for the sticky header
|
||||
sticky_header_controller: null,
|
||||
|
||||
@@ -82,6 +84,14 @@ $(function () {
|
||||
oBodyElem.on('dialogopen', function(){
|
||||
me._updateStickyHeaderHandler();
|
||||
});
|
||||
|
||||
// Observe the panel resizes in order to adjust the tabs list; only necessary when header is sticky for now
|
||||
if(window.ResizeObserver) {
|
||||
const oPanelRO = new ResizeObserver(function(){
|
||||
me._updateTabsListPosition();
|
||||
});
|
||||
oPanelRO.observe(this.element[0]);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -94,10 +104,8 @@ $(function () {
|
||||
_updateStickyHeaderHandler: function () {
|
||||
const me = this;
|
||||
|
||||
// Determine in which kind of container the panel is
|
||||
let oNewViewportElem = this.element.scrollParent()[0];
|
||||
|
||||
// If viewport hasn't changed, there is no need to refresh the SM controller
|
||||
let oNewViewportElem = this.element.scrollParent()[0];
|
||||
if (oNewViewportElem === this.options.viewport_elem) {
|
||||
return;
|
||||
}
|
||||
@@ -116,24 +124,74 @@ $(function () {
|
||||
});
|
||||
|
||||
let oSMScene = new ScrollMagic.Scene({
|
||||
triggerElement: this.element.find(this.js_selectors.panel_header_sticky_sentinel_top)[0],
|
||||
// Traduction: As soon as the header's top sentinel...
|
||||
triggerElement: this.element.find(this.js_selectors.block.panel_header_sticky_sentinel_top)[0],
|
||||
// ... leaves the viewport...
|
||||
triggerHook: 0,
|
||||
duration: this.element.outerHeight(),
|
||||
offset: this.element.find(this.js_selectors.panel_header_sticky_sentinel_top).outerHeight()
|
||||
offset: this.element.find(this.js_selectors.block.panel_header_sticky_sentinel_top).outerHeight()
|
||||
})
|
||||
.on('enter', function(){
|
||||
// ... we consider the header as sticking...
|
||||
.on('enter', function () {
|
||||
me._onHeaderBecomesSticky();
|
||||
})
|
||||
.on('leave', function(){
|
||||
// ... and when it comes back in the viewport, it stops.
|
||||
.on('leave', function () {
|
||||
me._onHeaderStopsBeingSticky();
|
||||
})
|
||||
.addTo(this.sticky_header_controller);
|
||||
},
|
||||
_onHeaderBecomesSticky: function () {
|
||||
this.element.find(this.js_selectors.panel_header).addClass(this.css_classes.is_sticking);
|
||||
this.element.find(this.js_selectors.block.panel_header).addClass(this.css_classes.is_sticking);
|
||||
if (this._hasTabContainer()) {
|
||||
this._updateTabsListPosition(false /* Need to wait for the header transition to end */);
|
||||
}
|
||||
},
|
||||
_onHeaderStopsBeingSticky: function () {
|
||||
this.element.find(this.js_selectors.panel_header).removeClass(this.css_classes.is_sticking);
|
||||
this.element.find(this.js_selectors.block.panel_header).removeClass(this.css_classes.is_sticking);
|
||||
if (this._hasTabContainer()) {
|
||||
this._updateTabsListPosition(false /* Need to wait for the header transition to end */);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Update the position of the tabs list so it is consistent with the header, which is important when the header is sticky
|
||||
*
|
||||
* @param bImmediate {boolean} Should the position be updated immediatly or delayed (typically if we have to wait for a transition to end)
|
||||
* @private
|
||||
*/
|
||||
_updateTabsListPosition: function(bImmediate = true) {
|
||||
// Vertical tab container is not supported yet
|
||||
if(this._isTabContainerVertical()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const me = this;
|
||||
const oTabsListElem = this.element.find(this.js_selectors.block.tab_container_tabs_list);
|
||||
|
||||
if(this._isHeaderSticking()){
|
||||
// Unfortunately for now the timeout is hardcoded as we don't know how to get notified when the *CSS* transition is done.
|
||||
const iTimeout = bImmediate ? 0 : 300;
|
||||
setTimeout(function(){
|
||||
const oHeaderElem = me.element.find(me.js_selectors.block.panel_header);
|
||||
const oHeaderOffset = oHeaderElem.offset();
|
||||
const iHeaderWidth = oHeaderElem.outerWidth();
|
||||
const iHeaderHeight = oHeaderElem.outerHeight();
|
||||
const iPanelBorderWidth = parseInt(me.element.find(me.js_selectors.block.panel_body).css('border-left-width'));
|
||||
|
||||
oTabsListElem
|
||||
.css('top', oHeaderOffset.top + iHeaderHeight)
|
||||
.css('left', oHeaderOffset.left + iPanelBorderWidth)
|
||||
.css('width', iHeaderWidth - (2 * iPanelBorderWidth))
|
||||
.addClass(me.css_classes.is_sticking);
|
||||
}, iTimeout);
|
||||
} else {
|
||||
// Reset to default style
|
||||
oTabsListElem
|
||||
.css('top', '')
|
||||
.css('left', '')
|
||||
.css('width', '')
|
||||
.removeClass(me.css_classes.is_sticking);
|
||||
}
|
||||
},
|
||||
|
||||
// Helpers
|
||||
@@ -144,5 +202,29 @@ $(function () {
|
||||
_isHeaderVisibleOnScroll: function () {
|
||||
return this.options.is_header_visible_on_scroll;
|
||||
},
|
||||
/**
|
||||
* @return {boolean} True if the header is currently sticking
|
||||
* @private
|
||||
*/
|
||||
_isHeaderSticking: function () {
|
||||
return this.element.find(this.js_selectors.block.panel_header).hasClass(this.css_classes.is_sticking);
|
||||
},
|
||||
/**
|
||||
* @return {boolean} True if the panel has a tab container
|
||||
* @private
|
||||
*/
|
||||
_hasTabContainer: function () {
|
||||
return this.element.find(this.js_selectors.block.tab_container).length > 0;
|
||||
},
|
||||
/**
|
||||
* @return {boolean} True if the panel has a tab container and it is vertical, false otherwise
|
||||
* @private
|
||||
*/
|
||||
_isTabContainerVertical: function () {
|
||||
if(!this._hasTabContainer()) {
|
||||
return false;
|
||||
}
|
||||
return this.element.find(this.js_selectors.block.tab_container).hasClass(this.css_classes.is_vertical);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
$(function()
|
||||
{
|
||||
// the widget definition, where 'itop' is the namespace,
|
||||
// 'panel' the widget name
|
||||
// 'object_details' the widget name
|
||||
$.widget( 'itop.object_details', $.itop.panel,
|
||||
{
|
||||
// default options
|
||||
@@ -32,5 +32,21 @@ $(function()
|
||||
{
|
||||
this._super();
|
||||
},
|
||||
|
||||
_bindEvents: function ()
|
||||
{
|
||||
this._super();
|
||||
|
||||
// Keep URL's hash parameters when clicking on a link of the header
|
||||
this.element.on('click', '[data-role="ibo-panel--header-right"] a', function() {
|
||||
aMatches = /#(.*)$/.exec(window.location.href);
|
||||
if (aMatches != null) {
|
||||
currentHash = aMatches[1];
|
||||
if (/#(.*)$/.test(this.href)) {
|
||||
this.href = this.href.replace(/#(.*)$/, '#'+currentHash);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,6 +20,8 @@ $.widget( "itop.scrollabletabs", $.ui.tabs, {
|
||||
}
|
||||
},
|
||||
},
|
||||
// Used keep the beginning of the panel visible when scrolling to it
|
||||
scroll_offset_y: null,
|
||||
controller: null,
|
||||
_create: function() {
|
||||
var me = this;
|
||||
@@ -35,6 +37,8 @@ $.widget( "itop.scrollabletabs", $.ui.tabs, {
|
||||
|
||||
this._super(this.options);
|
||||
|
||||
// Initialize the vertical scroll offset
|
||||
this.scroll_offset_y = this.element.find('#' + this.tabs.eq(0).attr('data-tab-id')).offset().top;
|
||||
|
||||
// Add every other tab to the controller
|
||||
$(this.js_selectors.tab_toggler).each(function(){
|
||||
@@ -47,18 +51,27 @@ $.widget( "itop.scrollabletabs", $.ui.tabs, {
|
||||
|
||||
// Set active tab, tab-container gives us a tab based on url hash or 0
|
||||
this.setTab(this._findActive(this.options.active));
|
||||
this.controller.scrollTo('#' + this.tabs.eq(this.options.active).attr('data-tab-id'));
|
||||
// If not on the first tab, we scroll directly to it
|
||||
// Note: We don't want to scroll if we are on the first one, otherwise it will looks buggy because the page will be a bit scrolled and it doesn't feel right
|
||||
if(this.options.active > 0) {
|
||||
const oActiveTab = this.tabs.eq(this.options.active);
|
||||
const oActivePanel = this.element.find('#' + oActiveTab.attr('data-tab-id'));
|
||||
|
||||
// Remove from scroll length the initial space between the top of the first panel and the top of the screen; this is to avoid scrolling too far
|
||||
// That being said, as lists are fetched / updated asynchroniously, once they got their responses, the layout will change/shift and the current tab won't be the good one anymore 😕
|
||||
this.controller.scrollTo(oActivePanel.offset().top - this.scroll_offset_y);
|
||||
}
|
||||
},
|
||||
// Create a new scene to be added to the controller
|
||||
_newScene: function(tab, panel)
|
||||
{
|
||||
var me = this;
|
||||
var iPanelId = panel.attr('id');
|
||||
var sPanelId = panel.attr('id');
|
||||
return new ScrollMagic.Scene({
|
||||
triggerElement: '#' + iPanelId,
|
||||
triggerHook: 0.03, // show, when scrolled 10% into view
|
||||
triggerElement: '#' + sPanelId,
|
||||
triggerHook: 0.2, // show, when scrolled 20% into view
|
||||
duration: function () {
|
||||
return $('#' + iPanelId).outerHeight();
|
||||
return $('#' + sPanelId).outerHeight();
|
||||
}
|
||||
})
|
||||
.on("enter", function (event) {
|
||||
|
||||
@@ -15,6 +15,8 @@ $(function()
|
||||
css_classes:
|
||||
{
|
||||
is_hidden: 'ibo-is-hidden',
|
||||
is_transparent: 'ibo-is-transparent',
|
||||
is_opaque: 'ibo-is-opaque',
|
||||
is_scrollable: 'ibo-is-scrollable',
|
||||
tab_container: 'ibo-tab-container',
|
||||
},
|
||||
@@ -106,17 +108,30 @@ $(function()
|
||||
me._onTabTogglerClick($(this));
|
||||
});
|
||||
// Resize of the tab container
|
||||
if(window.ResizeObserver)
|
||||
{
|
||||
const oTabsListRO = new ResizeObserver(function(){
|
||||
// Note: For a reason I don't understand, when called instantly the sub function CombodoGlobalToolbox.IsElementVisibleToTheUser() won't be able to retrieve an element using the document.elementFromPoint() function
|
||||
// As it won't return anything, the function always thinks it's invisible...
|
||||
setTimeout(function(){
|
||||
me._onTabContainerResize();
|
||||
}, 200);
|
||||
|
||||
});
|
||||
oTabsListRO.observe($('.ibo-tab-container--tabs-list')[0]);
|
||||
if(window.IntersectionObserver) {
|
||||
const oTabsListIntersectObs = new IntersectionObserver(function(aEntries, oTabsListIntersectObs){
|
||||
aEntries.forEach(oEntry => {
|
||||
let oTabHeaderElem = $(oEntry.target);
|
||||
let bIsVisible = oEntry.isIntersecting;
|
||||
if(bIsVisible) {
|
||||
oTabHeaderElem.removeClass(me.css_classes.is_transparent);
|
||||
oTabHeaderElem.css('visibility', '');
|
||||
}
|
||||
else {
|
||||
oTabHeaderElem.removeClass(me.css_classes.is_transparent);
|
||||
// This is necessary, otherwise link will still be clickable
|
||||
oTabHeaderElem.css('visibility', 'hidden');
|
||||
}
|
||||
me._updateTabHeaderDisplay(oTabHeaderElem, bIsVisible);
|
||||
});
|
||||
me._updateExtraTabsList();
|
||||
}, {
|
||||
root: $('.ibo-tab-container--tabs-list')[0],
|
||||
threshold: [1] // Must be completely visible
|
||||
});
|
||||
this.element.find(this.js_selectors.tab_header).each(function(){
|
||||
oTabsListIntersectObs.observe(this);
|
||||
});
|
||||
}
|
||||
// Click on extra tabs list toggler
|
||||
this.element.find(this.js_selectors.extra_tabs_list_toggler).on('click', function(oEvent){
|
||||
@@ -233,18 +248,25 @@ $(function()
|
||||
/**
|
||||
* Update tab header display based on its visibility to the user
|
||||
*
|
||||
* @param oTabHeaderElem jQuery element
|
||||
* @param oTabHeaderElem {Object} jQuery element
|
||||
* @param bIsVisible {boolean|null} If null, visibility will be computed automatically. Not that performance might not be great so it's preferable to pass the value when known
|
||||
* @private
|
||||
*/
|
||||
_updateTabHeaderDisplay(oTabHeaderElem)
|
||||
_updateTabHeaderDisplay(oTabHeaderElem, bIsVisible = null)
|
||||
{
|
||||
const sTabId = oTabHeaderElem.attr('data-tab-id');
|
||||
const oMatchingExtraTabElem = this.element.find(this.js_selectors.extra_tab_toggler+'[href="#'+sTabId+'"]');
|
||||
|
||||
if (!CombodoGlobalToolbox.IsElementVisibleToTheUser(oTabHeaderElem[0], true, 2)) {
|
||||
oMatchingExtraTabElem.removeClass(this.css_classes.is_hidden);
|
||||
} else {
|
||||
// Manually check if the tab header is visible if the info isn't passed
|
||||
if (bIsVisible === null) {
|
||||
bIsVisible = CombodoGlobalToolbox.IsElementVisibleToTheUser(oTabHeaderElem[0], true, 2);
|
||||
}
|
||||
|
||||
// Hide/show the corresponding extra tab element
|
||||
if (bIsVisible) {
|
||||
oMatchingExtraTabElem.addClass(this.css_classes.is_hidden);
|
||||
} else {
|
||||
oMatchingExtraTabElem.removeClass(this.css_classes.is_hidden);
|
||||
}
|
||||
},
|
||||
// - Update extra tabs list
|
||||
|
||||
@@ -26,6 +26,7 @@ $(function () {
|
||||
options: {},
|
||||
css_classes: {
|
||||
is_sticking: 'ibo-is-sticking',
|
||||
is_vertical: 'ibo-is-vertical',
|
||||
},
|
||||
js_selectors: {
|
||||
// Selectors that target any elements in the DOM
|
||||
|
||||
@@ -25,19 +25,6 @@
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block iboPanelHeaderRightActionsHandlers %}
|
||||
// Keep URL's hash parameters when clicking on a link of the header
|
||||
$('#{{ oUIBlock.GetId() }}').on('click', '[data-role="ibo-panel--header-right"] a', function() {
|
||||
aMatches = /#(.*)$/.exec(window.location.href);
|
||||
if (aMatches != null) {
|
||||
currentHash = aMatches[1];
|
||||
if (/#(.*)$/.test(this.href)) {
|
||||
this.href = this.href.replace(/#(.*)$/, '#'+currentHash);
|
||||
}
|
||||
}
|
||||
});
|
||||
{% endblock %}
|
||||
|
||||
{% block iboWidgetInstantiation %}
|
||||
$('#{{ oUIBlock.GetId() }}').object_details({
|
||||
is_header_visible_on_scroll: {{ oUIBlock.IsHeaderVisibleOnScroll|var_export }}
|
||||
|
||||
Reference in New Issue
Block a user