mirror of
https://github.com/Combodo/iTop.git
synced 2026-02-12 23:14:18 +01:00
246 lines
8.8 KiB
JavaScript
246 lines
8.8 KiB
JavaScript
/*
|
|
* Copyright (C) 2013-2024 Combodo SAS
|
|
*
|
|
* This file is part of iTop.
|
|
*
|
|
* iTop is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* iTop is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
*/
|
|
|
|
;
|
|
$(function () {
|
|
// the widget definition, where 'itop' is the namespace,
|
|
// 'panel' the widget name
|
|
$.widget('itop.panel', $.itop.ui_block,
|
|
{
|
|
// default options
|
|
options:
|
|
{
|
|
// The viewport element (jQuery object) to consider for the panel
|
|
viewport_elem: null,
|
|
// Whether the header should stay visible or not during the scroll in the "viewport_elem"
|
|
is_header_visible_on_scroll: false,
|
|
},
|
|
css_classes:
|
|
{
|
|
has_sticky_header: 'ibo-has-sticky-header',
|
|
sticky_sentinel: 'ibo-sticky-sentinel',
|
|
sticky_sentinel_top: 'ibo-sticky-sentinel-top',
|
|
},
|
|
js_selectors: {
|
|
global: {
|
|
fullscreen_elements: '.ibo-is-fullscreen',
|
|
},
|
|
block: {
|
|
panel_header: '[data-role="ibo-panel--header"]',
|
|
panel_header_sticky_sentinel_top: '[data-role="ibo-panel--header--sticky-sentinel-top"]',
|
|
panel_body: '[data-role="ibo-panel--body"]',
|
|
tab_container: '[data-role="ibo-tab-container"]',
|
|
tab_container_tabs_list: '[data-role="ibo-tab-container--tabs-list"]',
|
|
}
|
|
},
|
|
// {ScrollMagic.Controller} SM controller for the sticky header
|
|
sticky_header_controller: null,
|
|
|
|
// the constructor
|
|
_create: function () {
|
|
this._initializeMarkup();
|
|
this._bindEvents();
|
|
},
|
|
// events bound via _bind are removed automatically
|
|
// revert other modifications here
|
|
_destroy: function () {
|
|
},
|
|
_initializeMarkup: function () {
|
|
const me = this;
|
|
|
|
if (this._isHeaderVisibleOnScroll()) {
|
|
this.element.addClass(this.css_classes.has_sticky_header);
|
|
|
|
// Add sentinel to the markup to detect when the element changes between scrolling & sticked states
|
|
$('<div></div>')
|
|
.addClass(this.css_classes.sticky_sentinel)
|
|
.addClass(this.css_classes.sticky_sentinel_top)
|
|
.attr('data-role', 'ibo-panel--header--sticky-sentinel-top')
|
|
.prependTo(this.element);
|
|
|
|
this._updateStickyHeaderHandler();
|
|
}
|
|
},
|
|
_bindEvents: function () {
|
|
const me = this;
|
|
const oBodyElem = $('body');
|
|
|
|
if (this._isHeaderVisibleOnScroll()) {
|
|
// When a modal opens, it could have been for this panel. As the panel is moved into the modal's markup after this JS widget is instantiated
|
|
// the viewport is not the right one and we need to update it.
|
|
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]);
|
|
}
|
|
}
|
|
},
|
|
|
|
// Stickey header helpers
|
|
/**
|
|
* Create or update an handler on the header to toggle its "sticky" state.
|
|
* Update is needed when the panel was moved to other DOM node.
|
|
* @private
|
|
*/
|
|
_updateStickyHeaderHandler: function () {
|
|
const me = this;
|
|
|
|
// 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;
|
|
}
|
|
|
|
// Update the reference viewport
|
|
this.options.viewport_elem = oNewViewportElem;
|
|
|
|
// Clean SM controller if there was already one
|
|
if (null !== this.sticky_header_controller) {
|
|
this.sticky_header_controller.destroy(true);
|
|
}
|
|
|
|
// Prepare SM controller
|
|
this.sticky_header_controller = new ScrollMagic.Controller({
|
|
container: this.options.viewport_elem,
|
|
});
|
|
|
|
let oSMScene = new ScrollMagic.Scene({
|
|
// Traduction: As soon as the header's top sentinel...
|
|
triggerElement: this.element.find(this.js_selectors.block.panel_header_sticky_sentinel_top).first(),
|
|
// ... leaves the viewport...
|
|
triggerHook: 0,
|
|
offset: this.element.find(this.js_selectors.block.panel_header_sticky_sentinel_top).first().outerHeight()
|
|
})
|
|
// ... we consider the header as sticking...
|
|
.on('enter', function () {
|
|
// N°4631 - If a non-intersecting element is fullscreen, we do nothing
|
|
if ($(me.js_selectors.global.fullscreen_elements).length > 0) {
|
|
return;
|
|
}
|
|
me._onHeaderBecomesSticky();
|
|
})
|
|
// ... and when it comes back in the viewport, it stops.
|
|
.on('leave', function () {
|
|
// N°4631 - If a non-intersecting element is fullscreen, we do nothing
|
|
if ($(me.js_selectors.global.fullscreen_elements).length > 0) {
|
|
return;
|
|
}
|
|
me._onHeaderStopsBeingSticky();
|
|
})
|
|
.addTo(this.sticky_header_controller);
|
|
},
|
|
_onHeaderBecomesSticky: function () {
|
|
this.element.find(this.js_selectors.block.panel_header).first().addClass(this.css_classes.is_sticking);
|
|
if (this._hasTabContainer()) {
|
|
this._updateTabsListPosition(false /* Need to wait for the header transition to end */);
|
|
}
|
|
},
|
|
_onHeaderStopsBeingSticky: function () {
|
|
const fPanelBottomPosition = this.element.position().top + this.element.find(this.js_selectors.block.panel_header_sticky_sentinel_top).first().outerHeight();
|
|
const fViewportVerticalScrollPosition = this.options.viewport_elem.scrollHeight - this.options.viewport_elem.clientHeight;
|
|
|
|
// Test to prevent the screen from flashing (cf bug 4124)
|
|
if (fPanelBottomPosition < fViewportVerticalScrollPosition) {
|
|
this.element.find(this.js_selectors.block.panel_header).first().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).first();
|
|
|
|
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).first();
|
|
const oHeaderOffset = oHeaderElem.offset();
|
|
const iHeaderWidth = oHeaderElem.outerWidth();
|
|
const iHeaderHeight = oHeaderElem.outerHeight();
|
|
const iPanelBorderWidth = parseInt(me.element.find(me.js_selectors.block.panel_body).first().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
|
|
/**
|
|
* @return {boolean} True if the panel should have its header visible during scroll
|
|
* @private
|
|
*/
|
|
_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).first().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).first().hasClass(this.css_classes.is_vertical);
|
|
},
|
|
});
|
|
});
|