mirror of
https://github.com/Combodo/iTop.git
synced 2026-02-13 07:24:13 +01:00
334 lines
15 KiB
JavaScript
334 lines
15 KiB
JavaScript
/*
|
|
* @copyright Copyright (C) 2010-2021 Combodo SARL
|
|
* @license http://opensource.org/licenses/AGPL-3.0
|
|
*/
|
|
|
|
$(function()
|
|
{
|
|
$.widget( 'itop.tab_container',
|
|
{
|
|
// default options
|
|
options:
|
|
{
|
|
remote_tab_load_dict: 'Click to load',
|
|
},
|
|
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',
|
|
},
|
|
js_selectors:
|
|
{
|
|
tabs_list: '[data-role="ibo-tab-container--tabs-list"]',
|
|
tab_header: '[data-role="ibo-tab-container--tab-header"]',
|
|
tab_ajax_type: '[data-tab-type="ajax"]',
|
|
tab_html_type: '[data-tab-type="html"]',
|
|
tab_toggler: '[data-role="ibo-tab-container--tab-toggler"]',
|
|
extra_tabs_container: '[data-role="ibo-tab-container--extra-tabs-container"]',
|
|
extra_tabs_list_toggler: '[data-role="ibo-tab-container--extra-tabs-list-toggler"]',
|
|
extra_tabs_list: '[data-role="ibo-tab-container--extra-tabs-list"]',
|
|
extra_tab_toggler: '[data-role="ibo-tab-container--extra-tab-toggler"]',
|
|
},
|
|
|
|
// the constructor
|
|
_create: function()
|
|
{
|
|
var me = this;
|
|
this.element.addClass(this.css_classes.tab_container);
|
|
|
|
// Ugly patch for a change in the behavior of jQuery UI:
|
|
// Before jQuery UI 1.9, tabs were always considered as "local" (opposed to Ajax)
|
|
// when their href was beginning by #. Starting with 1.9, a <base> tag in the page
|
|
// is taken into account and causes "local" tabs to be considered as Ajax
|
|
// unless their URL is equal to the URL of the page...
|
|
if ($('base').length > 0) {
|
|
this.element.find(this.js_selectors.tab_toggler).each(function () {
|
|
const sHash = location.hash;
|
|
const sCleanLocation = location.href.toString().replace(sHash, '').replace(/#$/, '');
|
|
$(this).attr('href', sCleanLocation+$(this).attr('href'));
|
|
});
|
|
}
|
|
|
|
let oTabsParams = {
|
|
classes: {
|
|
'ui-tabs-panel': 'ibo-tab-container--tab-container', // For ajax tabs, so their containers have the right CSS classes
|
|
},
|
|
// There we want a number as it is for the jQuery Tabs API
|
|
active: this._getTabIndexFromTabId($.bbq.getState(this.element.attr('id'), true)) || 0,
|
|
remote_tab_load_dict: this.options.remote_tab_load_dict
|
|
};
|
|
if ($.bbq) {
|
|
// Enable tabs on all tab widgets. The `event` property must be overridden so
|
|
// that the tabs aren't changed on click, and any custom event name can be
|
|
// specified. Note that if you define a callback for the 'select' event, it
|
|
// will be executed for the selected tab whenever the hash changes.
|
|
oTabsParams['event'] = 'change';
|
|
}
|
|
|
|
// While our tab widget is loading, protect tab toggler from being triggered
|
|
this.element.find(this.js_selectors.tab_toggler).on('click', function(e){
|
|
if(me.element.attr('data-status') === 'loading') {
|
|
e.preventDefault();
|
|
e.stopImmediatePropagation();
|
|
}
|
|
});
|
|
// Now that we are protected from toggler being triggered without tab container being loaded, we can put back
|
|
// data-target attribute value back into href attribute
|
|
$.each(this.element.find(this.js_selectors.tab_header + this.js_selectors.tab_ajax_type), function (a){
|
|
let oLink = $(this).find(me.js_selectors.tab_toggler);
|
|
oLink.attr('href', oLink.attr('data-target'));
|
|
})
|
|
|
|
this._addTabsWidget(oTabsParams);
|
|
|
|
this._bindEvents()
|
|
|
|
// We're done, set our status as loaded
|
|
this.element.attr('data-status', 'loaded');
|
|
},
|
|
/**
|
|
* @param oParams {Object} Structured object representing the options for the jQuery UI Tabs widget
|
|
* @private
|
|
*/
|
|
_addTabsWidget: function (oParams) {
|
|
if (this.element.hasClass(this.css_classes.is_scrollable)) {
|
|
this.element.scrollabletabs(oParams);
|
|
} else {
|
|
this.element.regulartabs(oParams);
|
|
}
|
|
},
|
|
/**
|
|
* Return tabs widget instance
|
|
* @public
|
|
*/
|
|
GetTabsWidget: function () {
|
|
if (this.element.hasClass(this.css_classes.is_scrollable)) {
|
|
return this.element.scrollabletabs('instance');
|
|
} else {
|
|
return this.element.regulartabs('instance');
|
|
}
|
|
},
|
|
// events bound via _bind are removed automatically
|
|
// revert other modifications here
|
|
_destroy: function()
|
|
{
|
|
this.element.removeClass(this.css_classes.tab_container);
|
|
},
|
|
_bindEvents: function()
|
|
{
|
|
const me = this;
|
|
|
|
// Bind an event on tab activation
|
|
this.element.on('tabsactivate', function(oEvent, oUI){
|
|
me._onTabActivated(oUI);
|
|
});
|
|
this.element.on('tabscrolled', function(oEvent, oUI){
|
|
me._onTabActivated(oUI);
|
|
});
|
|
// Bind an event to window.onhashchange that, when the history state changes,
|
|
// iterates over all tab widgets, changing the current tab as necessary.
|
|
$(window).on('hashchange', function(){
|
|
me._onHashChange();
|
|
});
|
|
// Click on tab togglers
|
|
this.element.find(this.js_selectors.tab_toggler).on('click', function(){
|
|
me._onTabTogglerClick($(this));
|
|
});
|
|
// Resize of the tab container
|
|
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: this.element.find(this.js_selectors.tabs_list)[0],
|
|
threshold: [0.9] // N°4783 Should be completely visible, but lowering the threshold prevents a bug in the JS Observer API when the window is zoomed in/out, in which case all items respond as being hidden even when they are not.
|
|
});
|
|
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){
|
|
me._onExtraTabsListTogglerClick($(this), oEvent);
|
|
});
|
|
// Click on "extra tab togglers"
|
|
this.element.find(this.js_selectors.extra_tab_toggler).on('click', function(oEvent){
|
|
me._onExtraTabTogglerClick($(this), oEvent);
|
|
});
|
|
// Mostly for outside clicks that should close elements
|
|
$('body').on('click', function(oEvent){
|
|
me._onBodyClick(oEvent);
|
|
});
|
|
},
|
|
|
|
// Events callbacks
|
|
// - Update tab headers display on container resize
|
|
_onTabContainerResize: function()
|
|
{
|
|
const me = this;
|
|
this.element.find(this.js_selectors.tab_header).each(function(){
|
|
me._updateTabHeaderDisplay($(this));
|
|
});
|
|
this._updateExtraTabsList();
|
|
},
|
|
// - Update URL hash when tab is activated
|
|
_onTabActivated: function(oUI)
|
|
{
|
|
let oState = {};
|
|
|
|
// Get the id of this tab widget.
|
|
const sId = this.element.attr('id');
|
|
|
|
//Datatable are not displayed correctly when hidden
|
|
$(oUI.newPanel).find('.dataTables_scrollBody > .ibo-datatable').each(function () {
|
|
$('#'+this.id).DataTable().columns.adjust().draw();
|
|
});
|
|
|
|
// Get the ID of this tab.
|
|
const sTabId = $(oUI.newTab).attr('data-tab-id');
|
|
|
|
// Set the state!
|
|
oState[sId] = sTabId;
|
|
$.bbq.pushState(oState);
|
|
},
|
|
// - Change current tab as necessary when URL hash changes
|
|
_onHashChange: function () {
|
|
// Get the index for this tab widget from the hash, based on the
|
|
// appropriate id property. In jQuery 1.4, you should use e.getState()
|
|
// instead of $.bbq.getState(). The second, 'true' argument coerces the
|
|
// string value to a number.
|
|
const sTabId = $.bbq.getState(this.element.attr('id'), true) || this._getTabIdFromTabIndex(0);
|
|
|
|
// Select the appropriate tab for this tab widget by triggering the custom
|
|
// event specified in the .tabs() init above (you could keep track of what
|
|
// tab each widget is on using .data, and only select a tab if it has
|
|
// changed).
|
|
this.element.children(this.js_selectors.tabs_list).children(this.js_selectors.tab_header+'[data-tab-id="'+sTabId+'"]').find(this.js_selectors.tab_toggler).triggerHandler('change');
|
|
|
|
// Iterate over all truncated lists to find whether they are expanded or not
|
|
$('a.truncated').each(function () {
|
|
const sState = $.bbq.getState(this.id, true) || 'close';
|
|
if (sState === 'open') {
|
|
$(this).trigger('open');
|
|
} else {
|
|
$(this).trigger('close');
|
|
}
|
|
});
|
|
},
|
|
// - Define our own click handler for the tabs, overriding the default.
|
|
_onTabTogglerClick: function (oTabHeaderElem) {
|
|
if ($.bbq) {
|
|
let oState = {};
|
|
|
|
// Get the id of this tab widget.
|
|
const sId = this.element.attr('id');
|
|
|
|
// Get the index of this tab.
|
|
const sTabId = oTabHeaderElem.closest(this.js_selectors.tab_header).attr('data-tab-id');
|
|
|
|
// Set the state!
|
|
oState[sId] = sTabId;
|
|
$.bbq.pushState(oState);
|
|
}
|
|
},
|
|
// - Forward click event to real tab toggler
|
|
_onExtraTabTogglerClick: function (oExtraTabTogglerElem, oEvent) {
|
|
// Prevent anchor default behaviour
|
|
oEvent.preventDefault();
|
|
|
|
// Trigger click event on real tab toggler (the hidden one)
|
|
const sTargetTabId = oExtraTabTogglerElem.attr('href').replace(/#/, '');
|
|
this.element.find(this.js_selectors.tab_header+'[data-tab-id="'+sTargetTabId+'"] '+this.js_selectors.tab_toggler).trigger('click');
|
|
},
|
|
// - Toggle extra tabs list
|
|
_onExtraTabsListTogglerClick: function(oElem, oEvent)
|
|
{
|
|
// Prevent anchor default behaviour
|
|
oEvent.preventDefault();
|
|
|
|
// TODO 3.0.0: Should/could we use a popover menu instead here?
|
|
this.element.find(this.js_selectors.extra_tabs_list).toggleClass(this.css_classes.is_hidden);
|
|
},
|
|
_onBodyClick: function(oEvent)
|
|
{
|
|
// Close extra tabs list if opened
|
|
if($(oEvent.target.closest(this.js_selectors.extra_tabs_container)).length === 0)
|
|
{
|
|
this.element.find(this.js_selectors.extra_tabs_list).addClass(this.css_classes.is_hidden);
|
|
}
|
|
},
|
|
|
|
// Helpers
|
|
/**
|
|
* Update tab header display based on its visibility to the user
|
|
*
|
|
* @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, bIsVisible = null)
|
|
{
|
|
const sTabId = oTabHeaderElem.attr('data-tab-id');
|
|
const oMatchingExtraTabElem = this.element.find(this.js_selectors.extra_tab_toggler+'[href="#'+sTabId+'"]');
|
|
|
|
// 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
|
|
_updateExtraTabsList: function () {
|
|
const iVisibleExtraTabsCount = this.element.find(this.js_selectors.extra_tab_toggler+':not(.'+this.css_classes.is_hidden+')').length;
|
|
const oExtraTabsContainerElem = this.element.find(this.js_selectors.extra_tabs_container);
|
|
|
|
if (iVisibleExtraTabsCount > 0) {
|
|
oExtraTabsContainerElem.removeClass(this.css_classes.is_hidden);
|
|
} else {
|
|
oExtraTabsContainerElem.addClass(this.css_classes.is_hidden);
|
|
}
|
|
},
|
|
// - Get tab's "data-tab-id" from tab's index
|
|
/**
|
|
* @param iIdx {number} Index (starting from 0) of the tab in the container
|
|
* @return {string} The [data-tab-id] of the iIdx-th tab (zero based). Can return undefined if it has not [data-tab-id] attribute
|
|
* @private
|
|
*/
|
|
_getTabIdFromTabIndex(iIdx) {
|
|
return this.element.children(this.js_selectors.tabs_list).children(this.js_selectors.tab_header).eq(iIdx).attr('data-tab-id');
|
|
},
|
|
/**
|
|
* @param sId {string} The [data-tab-id] of the tab
|
|
* @return {number} The index (zero based) of the tab. If no matching tab, 0 will be returned.
|
|
* @private
|
|
*/
|
|
_getTabIndexFromTabId(sId) {
|
|
const oTabElem = this.element.children(this.js_selectors.tabs_list).children(this.js_selectors.tab_header+'[data-tab-id="'+sId+'"]');
|
|
|
|
return oTabElem.length === 0 ? 0 : oTabElem.prevAll().length;
|
|
}
|
|
});
|
|
});
|