Files
iTop/js/layouts/tab-container/tab-container.js
2024-05-06 18:35:31 +02:00

401 lines
19 KiB
JavaScript

/*
* @copyright Copyright (C) 2010-2024 Combodo SAS
* @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_disabled: 'ibo-is-disabled',
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"]',
global: {
fullscreen_elements: '.ibo-is-fullscreen',
},
},
// 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){
// N°4631 - If a non-intersecting element is fullscreen, we do nothing
if ($(me.js_selectors.global.fullscreen_elements).length > 0) {
return;
}
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();
if (oExtraTabTogglerElem.attr('aria-disabled') === 'true') {
// Corresponding tab is disabled, do nothing
oEvent.stopPropagation();
return;
}
// 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();
// Compute list position
// Note: Arbitrary +6px for the position as we don't want it to be exactly against the toggler
let fTopOffset = this.element.find(this.js_selectors.extra_tabs_list_toggler).offset().top + this.element.find(this.js_selectors.extra_tabs_list_toggler).outerHeight() + 6;
// We need to compute position from the right side of the screen because at this time the list isn't visible and we can't know its width, so we can't position it regarding the left side of the screen
// Note: We use window.innerWidth instead of outerWidth as we need the width of the actual viewport, not the OS browser window
let fRightOffset = window.innerWidth - this.element.find(this.js_selectors.extra_tabs_list_toggler).offset().left - this.element.find(this.js_selectors.extra_tabs_list_toggler).outerWidth();
this.element.find(this.js_selectors.extra_tabs_list)
.css('top', fTopOffset + 'px')
.css('right', fRightOffset + 'px');
// 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+'"]');
// Disabled tabs should be disabled in the ExtraTabs list as well
let bIsDisabled = false;
if (oTabHeaderElem.attr('aria-disabled') === 'true') {
bIsDisabled = true;
}
// 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);
}
// Enable/disable the corresponding extra tab element
if (bIsDisabled) {
oMatchingExtraTabElem.attr('aria-disabled', 'true');
oMatchingExtraTabElem.addClass(this.css_classes.is_disabled);
} else {
oMatchingExtraTabElem.attr('aria-disabled', 'false');
oMatchingExtraTabElem.removeClass(this.css_classes.is_disabled);
}
},
// - 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: function(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: function(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;
},
/**
* @param sId {string} The [data-tab-id] of the tab
* @return {Object} The jQuery object representing the tab element
*
* @private
*/
_getTabElementFromTabId: function(sId) {
return this.element.children(this.js_selectors.tabs_list).children(this.js_selectors.tab_header+'[data-tab-id="'+sId+'"]');
},
/**
* @param sId {string} The [data-tab-id] of the tab
* @return {Object} The jQuery object representing the tab element
*/
disableTab: function(sId){
const tabsWidget = this.GetTabsWidget();
const iIdx = this._getTabIndexFromTabId(sId);
tabsWidget.disable(iIdx);
const tabElement = this._getTabElementFromTabId(sId);
this._updateTabHeaderDisplay(tabElement);
},
/**
* @param sId {string} The [data-tab-id] of the tab
* @return {Object} The jQuery object representing the tab element
*/
enableTab: function(sId){
const tabsWidget = this.GetTabsWidget();
const iIdx = this._getTabIndexFromTabId(sId);
tabsWidget.enable(iIdx);
const tabElement = this._getTabElementFromTabId(sId);
this._updateTabHeaderDisplay(tabElement);
}
});
});