* Copyright jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
//>>description: Transforms a set of container elements into a tab structure.
//>>docs: http://api.jqueryui.com/tabs/
//>>demos: http://jqueryui.com/tabs/
//>>css.structure: ../../themes/base/core.css
//>>css.structure: ../../themes/base/tabs.css
//>>css.theme: ../../themes/base/theme.css
if ( typeof define === "function" && define.amd ) {
// AMD. Register as an anonymous module.
"ui-tabs": "ui-corner-all",
"ui-tabs-nav": "ui-corner-all",
"ui-tabs-panel": "ui-corner-bottom",
"ui-tabs-tab": "ui-corner-top"
return function( anchor ) {
var anchorUrl, locationUrl;
anchorUrl = anchor.href.replace( rhash, "" );
locationUrl = location.href.replace( rhash, "" );
// Decoding may throw an error if the URL isn't UTF-8 (#9518)
anchorUrl = decodeURIComponent( anchorUrl );
locationUrl = decodeURIComponent( locationUrl );
return anchor.hash.length > 1 && anchorUrl === locationUrl;
this._addClass( "ui-tabs", "ui-widget ui-widget-content" );
this._toggleClass( "ui-tabs-collapsible", null, options.collapsible );
options.active = this._initialActive();
// Take disabling tabs via class attribute from HTML
// into account and update option properly.
if ( $.isArray( options.disabled ) ) {
options.disabled = $.unique( options.disabled.concat(
$.map( this.tabs.filter( ".ui-state-disabled" ), function( li ) {
return that.tabs.index( li );
// Check for length avoids error when initializing empty list
if ( this.options.active !== false && this.anchors.length ) {
this.active = this._findActive( options.active );
if ( this.active.length ) {
this.load( options.active );
_initialActive: function() {
var active = this.options.active,
collapsible = this.options.collapsible,
locationHash = location.hash.substring( 1 );
// check the fragment identifier in the URL
this.tabs.each( function( i, tab ) {
if ( $( tab ).attr( "aria-controls" ) === locationHash ) {
// Check for a tab marked active via a class
active = this.tabs.index( this.tabs.filter( ".ui-tabs-active" ) );
// No active tab, set to false
if ( active === null || active === -1 ) {
active = this.tabs.length ? 0 : false;
// Handle numbers: negative, out of range
if ( active !== false ) {
active = this.tabs.index( this.tabs.eq( active ) );
active = collapsible ? false : 0;
// Don't allow collapsible: false and active: false
if ( !collapsible && active === false && this.anchors.length ) {
_getCreateEventData: function() {
panel: !this.active.length ? $() : this._getPanelForTab( this.active )
_tabKeydown: function( event ) {
var focusedTab = $( $.ui.safeActiveElement( this.document[ 0 ] ) ).closest( "li" ),
selectedIndex = this.tabs.index( focusedTab ),
if ( this._handlePageNav( event ) ) {
switch ( event.keyCode ) {
selectedIndex = this.anchors.length - 1;
// Activate only, no collapsing
clearTimeout( this.activating );
this._activate( selectedIndex );
// Toggle (cancel delayed activation, allow collapsing)
clearTimeout( this.activating );
// Determine if we should collapse or activate
this._activate( selectedIndex === this.options.active ? false : selectedIndex );
// Focus the appropriate tab, based on which key was pressed
clearTimeout( this.activating );
selectedIndex = this._focusNextTab( selectedIndex, goingForward );
// Navigating with control/command key will prevent automatic activation
if ( !event.ctrlKey && !event.metaKey ) {
// Update aria-selected immediately so that AT think the tab is already selected.
// Otherwise AT may confuse the user by stating that they need to activate the tab,
// but the tab will already be activated by the time the announcement finishes.
focusedTab.attr( "aria-selected", "false" );
this.tabs.eq( selectedIndex ).attr( "aria-selected", "true" );
this.activating = this._delay( function() {
this.option( "active", selectedIndex );
_panelKeydown: function( event ) {
if ( this._handlePageNav( event ) ) {
// Ctrl+up moves focus to the current tab
if ( event.ctrlKey && event.keyCode === $.ui.keyCode.UP ) {
this.active.trigger( "focus" );
// Alt+page up/down moves focus to the previous/next tab (and activates)
_handlePageNav: function( event ) {
if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_UP ) {
this._activate( this._focusNextTab( this.options.active - 1, false ) );
if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_DOWN ) {
this._activate( this._focusNextTab( this.options.active + 1, true ) );
_findNextTab: function( index, goingForward ) {
var lastTabIndex = this.tabs.length - 1;
if ( index > lastTabIndex ) {
while ( $.inArray( constrain(), this.options.disabled ) !== -1 ) {
index = goingForward ? index + 1 : index - 1;
_focusNextTab: function( index, goingForward ) {
index = this._findNextTab( index, goingForward );
this.tabs.eq( index ).trigger( "focus" );
_setOption: function( key, value ) {
if ( key === "active" ) {
// _activate() will handle invalid values and update this.options
this._super( key, value );
if ( key === "collapsible" ) {
this._toggleClass( "ui-tabs-collapsible", null, value );
// Setting collapsible: false while collapsed; open first panel
if ( !value && this.options.active === false ) {
this._setupEvents( value );
if ( key === "heightStyle" ) {
this._setupHeightStyle( value );
_sanitizeSelector: function( hash ) {
return hash ? hash.replace( /[!"$%&'()*+,.\/:;<=>?@\[\]\^`{|}~]/g, "\\$&" ) : "";
var options = this.options,
lis = this.tablist.children( ":has(a[href])" );
// Get disabled tabs from class attribute from HTML
// this will get converted to a boolean if needed in _refresh()
options.disabled = $.map( lis.filter( ".ui-state-disabled" ), function( tab ) {
// Was collapsed or no tabs
if ( options.active === false || !this.anchors.length ) {
// was active, but active tab is gone
} else if ( this.active.length && !$.contains( this.tablist[ 0 ], this.active[ 0 ] ) ) {
// all remaining tabs are disabled
if ( this.tabs.length === options.disabled.length ) {
this._activate( this._findNextTab( Math.max( 0, options.active - 1 ), false ) );
// was active, active tab still exists
// make sure active index is correct
options.active = this.tabs.index( this.active );
this._setOptionDisabled( this.options.disabled );
this._setupEvents( this.options.event );
this._setupHeightStyle( this.options.heightStyle );
this.tabs.not( this.active ).attr( {
"aria-selected": "false",
"aria-expanded": "false",
this.panels.not( this._getPanelForTab( this.active ) )
// Make sure one tab is in the tab order
if ( !this.active.length ) {
this.tabs.eq( 0 ).attr( "tabIndex", 0 );
this._addClass( this.active, "ui-tabs-active", "ui-state-active" );
this._getPanelForTab( this.active )
_processTabs: function() {
prevAnchors = this.anchors,
prevPanels = this.panels;
this.tablist = this._getList().attr( "role", "tablist" );
this._addClass( this.tablist, "ui-tabs-nav",
"ui-helper-reset ui-helper-clearfix ui-widget-header" );
// Prevent users from focusing disabled tabs via click
.on( "mousedown" + this.eventNamespace, "> li", function( event ) {
if ( $( this ).is( ".ui-state-disabled" ) ) {
// Preventing the default action in mousedown doesn't prevent IE
// from focusing the element, so if the anchor gets focused, blur.
// We don't have to worry about focusing the previously focused
// element since clicking on a non-focusable element should focus
.on( "focus" + this.eventNamespace, ".ui-tabs-anchor", function() {
if ( $( this ).closest( "li" ).is( ".ui-state-disabled" ) ) {
this.tabs = this.tablist.find( "> li:has(a[href])" )
this._addClass( this.tabs, "ui-tabs-tab", "ui-state-default" );
this.anchors = this.tabs.map( function() {
return $( "a", this )[ 0 ];
this._addClass( this.anchors, "ui-tabs-anchor" );
this.anchors.each( function( i, anchor ) {
var selector, panel, panelId,
anchorId = $( anchor ).uniqueId().attr( "id" ),
tab = $( anchor ).closest( "li" ),
originalAriaControls = tab.attr( "aria-controls" );
if ( that._isLocal( anchor ) ) {
panelId = selector.substring( 1 );
panel = that.element.find( that._sanitizeSelector( selector ) );
// If the tab doesn't already have aria-controls,
// generate an id by using a throw-away element
panelId = tab.attr( "aria-controls" ) || $( {} ).uniqueId()[ 0 ].id;
selector = "#" + panelId;
panel = that.element.find( selector );
panel = that._createPanel( panelId );
panel.insertAfter( that.panels[ i - 1 ] || that.tablist );
panel.attr( "aria-live", "polite" );
that.panels = that.panels.add( panel );
if ( originalAriaControls ) {
tab.data( "ui-tabs-aria-controls", originalAriaControls );
"aria-controls": panelId,
"aria-labelledby": anchorId
panel.attr( "aria-labelledby", anchorId );
this.panels.attr( "role", "tabpanel" );
this._addClass( this.panels, "ui-tabs-panel", "ui-widget-content" );
// Avoid memory leaks (#10056)
this._off( prevTabs.not( this.tabs ) );
this._off( prevAnchors.not( this.anchors ) );
this._off( prevPanels.not( this.panels ) );
// Allow overriding how to find the list for rare usage scenarios (#7715)
return this.tablist || this.element.find( "ol, ul" ).eq( 0 );
_createPanel: function( id ) {
.data( "ui-tabs-destroy", true );
_setOptionDisabled: function( disabled ) {