2 * jQuery UI Tabs 1.11.4
5 * Copyright jQuery Foundation and other contributors
6 * Released under the MIT license.
7 * http://jquery.org/license
9 * http://api.jqueryui.com/tabs/
11 (function( factory
) {
12 if ( typeof define
=== "function" && define
.amd
) {
14 // AMD. Register as an anonymous module.
27 return $.widget( "ui.tabs", {
34 heightStyle
: "content",
45 _isLocal
: (function() {
48 return function( anchor
) {
49 var anchorUrl
, locationUrl
;
52 // IE7 doesn't normalize the href property when set via script (#9317)
53 anchor
= anchor
.cloneNode( false );
55 anchorUrl
= anchor
.href
.replace( rhash
, "" );
56 locationUrl
= location
.href
.replace( rhash
, "" );
58 // decoding may throw an error if the URL isn't UTF-8 (#9518)
60 anchorUrl
= decodeURIComponent( anchorUrl
);
63 locationUrl
= decodeURIComponent( locationUrl
);
66 return anchor
.hash
.length
> 1 && anchorUrl
=== locationUrl
;
72 options
= this.options
;
77 .addClass( "ui-tabs ui-widget ui-widget-content ui-corner-all" )
78 .toggleClass( "ui-tabs-collapsible", options
.collapsible
);
81 options
.active
= this._initialActive();
83 // Take disabling tabs via class attribute from HTML
84 // into account and update option properly.
85 if ( $.isArray( options
.disabled
) ) {
86 options
.disabled
= $.unique( options
.disabled
.concat(
87 $.map( this.tabs
.filter( ".ui-state-disabled" ), function( li
) {
88 return that
.tabs
.index( li
);
93 // check for length avoids error when initializing empty list
94 if ( this.options
.active
!== false && this.anchors
.length
) {
95 this.active
= this._findActive( options
.active
);
102 if ( this.active
.length
) {
103 this.load( options
.active
);
107 _initialActive: function() {
108 var active
= this.options
.active
,
109 collapsible
= this.options
.collapsible
,
110 locationHash
= location
.hash
.substring( 1 );
112 if ( active
=== null ) {
113 // check the fragment identifier in the URL
114 if ( locationHash
) {
115 this.tabs
.each(function( i
, tab
) {
116 if ( $( tab
).attr( "aria-controls" ) === locationHash
) {
123 // check for a tab marked active via a class
124 if ( active
=== null ) {
125 active
= this.tabs
.index( this.tabs
.filter( ".ui-tabs-active" ) );
128 // no active tab, set to false
129 if ( active
=== null || active
=== -1 ) {
130 active
= this.tabs
.length
? 0 : false;
134 // handle numbers: negative, out of range
135 if ( active
!== false ) {
136 active
= this.tabs
.index( this.tabs
.eq( active
) );
137 if ( active
=== -1 ) {
138 active
= collapsible
? false : 0;
142 // don't allow collapsible: false and active: false
143 if ( !collapsible
&& active
=== false && this.anchors
.length
) {
150 _getCreateEventData: function() {
153 panel
: !this.active
.length
? $() : this._getPanelForTab( this.active
)
157 _tabKeydown: function( event
) {
158 var focusedTab
= $( this.document
[0].activeElement
).closest( "li" ),
159 selectedIndex
= this.tabs
.index( focusedTab
),
162 if ( this._handlePageNav( event
) ) {
166 switch ( event
.keyCode
) {
167 case $.ui
.keyCode
.RIGHT
:
168 case $.ui
.keyCode
.DOWN
:
171 case $.ui
.keyCode
.UP
:
172 case $.ui
.keyCode
.LEFT
:
173 goingForward
= false;
176 case $.ui
.keyCode
.END
:
177 selectedIndex
= this.anchors
.length
- 1;
179 case $.ui
.keyCode
.HOME
:
182 case $.ui
.keyCode
.SPACE
:
183 // Activate only, no collapsing
184 event
.preventDefault();
185 clearTimeout( this.activating
);
186 this._activate( selectedIndex
);
188 case $.ui
.keyCode
.ENTER
:
189 // Toggle (cancel delayed activation, allow collapsing)
190 event
.preventDefault();
191 clearTimeout( this.activating
);
192 // Determine if we should collapse or activate
193 this._activate( selectedIndex
=== this.options
.active
? false : selectedIndex
);
199 // Focus the appropriate tab, based on which key was pressed
200 event
.preventDefault();
201 clearTimeout( this.activating
);
202 selectedIndex
= this._focusNextTab( selectedIndex
, goingForward
);
204 // Navigating with control/command key will prevent automatic activation
205 if ( !event
.ctrlKey
&& !event
.metaKey
) {
207 // Update aria-selected immediately so that AT think the tab is already selected.
208 // Otherwise AT may confuse the user by stating that they need to activate the tab,
209 // but the tab will already be activated by the time the announcement finishes.
210 focusedTab
.attr( "aria-selected", "false" );
211 this.tabs
.eq( selectedIndex
).attr( "aria-selected", "true" );
213 this.activating
= this._delay(function() {
214 this.option( "active", selectedIndex
);
219 _panelKeydown: function( event
) {
220 if ( this._handlePageNav( event
) ) {
224 // Ctrl+up moves focus to the current tab
225 if ( event
.ctrlKey
&& event
.keyCode
=== $.ui
.keyCode
.UP
) {
226 event
.preventDefault();
231 // Alt+page up/down moves focus to the previous/next tab (and activates)
232 _handlePageNav: function( event
) {
233 if ( event
.altKey
&& event
.keyCode
=== $.ui
.keyCode
.PAGE_UP
) {
234 this._activate( this._focusNextTab( this.options
.active
- 1, false ) );
237 if ( event
.altKey
&& event
.keyCode
=== $.ui
.keyCode
.PAGE_DOWN
) {
238 this._activate( this._focusNextTab( this.options
.active
+ 1, true ) );
243 _findNextTab: function( index
, goingForward
) {
244 var lastTabIndex
= this.tabs
.length
- 1;
246 function constrain() {
247 if ( index
> lastTabIndex
) {
251 index
= lastTabIndex
;
256 while ( $.inArray( constrain(), this.options
.disabled
) !== -1 ) {
257 index
= goingForward
? index
+ 1 : index
- 1;
263 _focusNextTab: function( index
, goingForward
) {
264 index
= this._findNextTab( index
, goingForward
);
265 this.tabs
.eq( index
).focus();
269 _setOption: function( key
, value
) {
270 if ( key
=== "active" ) {
271 // _activate() will handle invalid values and update this.options
272 this._activate( value
);
276 if ( key
=== "disabled" ) {
277 // don't use the widget factory's disabled handling
278 this._setupDisabled( value
);
282 this._super( key
, value
);
284 if ( key
=== "collapsible" ) {
285 this.element
.toggleClass( "ui-tabs-collapsible", value
);
286 // Setting collapsible: false while collapsed; open first panel
287 if ( !value
&& this.options
.active
=== false ) {
292 if ( key
=== "event" ) {
293 this._setupEvents( value
);
296 if ( key
=== "heightStyle" ) {
297 this._setupHeightStyle( value
);
301 _sanitizeSelector: function( hash
) {
302 return hash
? hash
.replace( /[!"$%&'()*+,.\/:;<=>?@\[\]\^`{|}~]/g, "\\$&" ) : "";
305 refresh: function() {
306 var options
= this.options
,
307 lis
= this.tablist
.children( ":has(a[href])" );
309 // get disabled tabs from class attribute from HTML
310 // this will get converted to a boolean if needed in _refresh()
311 options
.disabled
= $.map( lis
.filter( ".ui-state-disabled" ), function( tab
) {
312 return lis
.index( tab
);
317 // was collapsed or no tabs
318 if ( options
.active
=== false || !this.anchors
.length
) {
319 options
.active
= false;
321 // was active, but active tab is gone
322 } else if ( this.active
.length
&& !$.contains( this.tablist
[ 0 ], this.active
[ 0 ] ) ) {
323 // all remaining tabs are disabled
324 if ( this.tabs
.length
=== options
.disabled
.length
) {
325 options
.active
= false;
327 // activate previous tab
329 this._activate( this._findNextTab( Math
.max( 0, options
.active
- 1 ), false ) );
331 // was active, active tab still exists
333 // make sure active index is correct
334 options
.active
= this.tabs
.index( this.active
);
340 _refresh: function() {
341 this._setupDisabled( this.options
.disabled
);
342 this._setupEvents( this.options
.event
);
343 this._setupHeightStyle( this.options
.heightStyle
);
345 this.tabs
.not( this.active
).attr({
346 "aria-selected": "false",
347 "aria-expanded": "false",
350 this.panels
.not( this._getPanelForTab( this.active
) )
353 "aria-hidden": "true"
356 // Make sure one tab is in the tab order
357 if ( !this.active
.length
) {
358 this.tabs
.eq( 0 ).attr( "tabIndex", 0 );
361 .addClass( "ui-tabs-active ui-state-active" )
363 "aria-selected": "true",
364 "aria-expanded": "true",
367 this._getPanelForTab( this.active
)
370 "aria-hidden": "false"
375 _processTabs: function() {
377 prevTabs
= this.tabs
,
378 prevAnchors
= this.anchors
,
379 prevPanels
= this.panels
;
381 this.tablist
= this._getList()
382 .addClass( "ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all" )
383 .attr( "role", "tablist" )
385 // Prevent users from focusing disabled tabs via click
386 .delegate( "> li", "mousedown" + this.eventNamespace
, function( event
) {
387 if ( $( this ).is( ".ui-state-disabled" ) ) {
388 event
.preventDefault();
393 // Preventing the default action in mousedown doesn't prevent IE
394 // from focusing the element, so if the anchor gets focused, blur.
395 // We don't have to worry about focusing the previously focused
396 // element since clicking on a non-focusable element should focus
398 .delegate( ".ui-tabs-anchor", "focus" + this.eventNamespace
, function() {
399 if ( $( this ).closest( "li" ).is( ".ui-state-disabled" ) ) {
404 this.tabs
= this.tablist
.find( "> li:has(a[href])" )
405 .addClass( "ui-state-default ui-corner-top" )
411 this.anchors
= this.tabs
.map(function() {
412 return $( "a", this )[ 0 ];
414 .addClass( "ui-tabs-anchor" )
416 role
: "presentation",
422 this.anchors
.each(function( i
, anchor
) {
423 var selector
, panel
, panelId
,
424 anchorId
= $( anchor
).uniqueId().attr( "id" ),
425 tab
= $( anchor
).closest( "li" ),
426 originalAriaControls
= tab
.attr( "aria-controls" );
429 if ( that
._isLocal( anchor
) ) {
430 selector
= anchor
.hash
;
431 panelId
= selector
.substring( 1 );
432 panel
= that
.element
.find( that
._sanitizeSelector( selector
) );
435 // If the tab doesn't already have aria-controls,
436 // generate an id by using a throw-away element
437 panelId
= tab
.attr( "aria-controls" ) || $( {} ).uniqueId()[ 0 ].id
;
438 selector
= "#" + panelId
;
439 panel
= that
.element
.find( selector
);
440 if ( !panel
.length
) {
441 panel
= that
._createPanel( panelId
);
442 panel
.insertAfter( that
.panels
[ i
- 1 ] || that
.tablist
);
444 panel
.attr( "aria-live", "polite" );
448 that
.panels
= that
.panels
.add( panel
);
450 if ( originalAriaControls
) {
451 tab
.data( "ui-tabs-aria-controls", originalAriaControls
);
454 "aria-controls": panelId
,
455 "aria-labelledby": anchorId
457 panel
.attr( "aria-labelledby", anchorId
);
461 .addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" )
462 .attr( "role", "tabpanel" );
464 // Avoid memory leaks (#10056)
466 this._off( prevTabs
.not( this.tabs
) );
467 this._off( prevAnchors
.not( this.anchors
) );
468 this._off( prevPanels
.not( this.panels
) );
472 // allow overriding how to find the list for rare usage scenarios (#7715)
473 _getList: function() {
474 return this.tablist
|| this.element
.find( "ol,ul" ).eq( 0 );
477 _createPanel: function( id
) {
480 .addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" )
481 .data( "ui-tabs-destroy", true );
484 _setupDisabled: function( disabled
) {
485 if ( $.isArray( disabled
) ) {
486 if ( !disabled
.length
) {
488 } else if ( disabled
.length
=== this.anchors
.length
) {
494 for ( var i
= 0, li
; ( li
= this.tabs
[ i
] ); i
++ ) {
495 if ( disabled
=== true || $.inArray( i
, disabled
) !== -1 ) {
497 .addClass( "ui-state-disabled" )
498 .attr( "aria-disabled", "true" );
501 .removeClass( "ui-state-disabled" )
502 .removeAttr( "aria-disabled" );
506 this.options
.disabled
= disabled
;
509 _setupEvents: function( event
) {
512 $.each( event
.split(" "), function( index
, eventName
) {
513 events
[ eventName
] = "_eventHandler";
517 this._off( this.anchors
.add( this.tabs
).add( this.panels
) );
518 // Always prevent the default action, even when disabled
519 this._on( true, this.anchors
, {
520 click: function( event
) {
521 event
.preventDefault();
524 this._on( this.anchors
, events
);
525 this._on( this.tabs
, { keydown
: "_tabKeydown" } );
526 this._on( this.panels
, { keydown
: "_panelKeydown" } );
528 this._focusable( this.tabs
);
529 this._hoverable( this.tabs
);
532 _setupHeightStyle: function( heightStyle
) {
534 parent
= this.element
.parent();
536 if ( heightStyle
=== "fill" ) {
537 maxHeight
= parent
.height();
538 maxHeight
-= this.element
.outerHeight() - this.element
.height();
540 this.element
.siblings( ":visible" ).each(function() {
541 var elem
= $( this ),
542 position
= elem
.css( "position" );
544 if ( position
=== "absolute" || position
=== "fixed" ) {
547 maxHeight
-= elem
.outerHeight( true );
550 this.element
.children().not( this.panels
).each(function() {
551 maxHeight
-= $( this ).outerHeight( true );
554 this.panels
.each(function() {
555 $( this ).height( Math
.max( 0, maxHeight
-
556 $( this ).innerHeight() + $( this ).height() ) );
558 .css( "overflow", "auto" );
559 } else if ( heightStyle
=== "auto" ) {
561 this.panels
.each(function() {
562 maxHeight
= Math
.max( maxHeight
, $( this ).height( "" ).height() );
563 }).height( maxHeight
);
567 _eventHandler: function( event
) {
568 var options
= this.options
,
569 active
= this.active
,
570 anchor
= $( event
.currentTarget
),
571 tab
= anchor
.closest( "li" ),
572 clickedIsActive
= tab
[ 0 ] === active
[ 0 ],
573 collapsing
= clickedIsActive
&& options
.collapsible
,
574 toShow
= collapsing
? $() : this._getPanelForTab( tab
),
575 toHide
= !active
.length
? $() : this._getPanelForTab( active
),
579 newTab
: collapsing
? $() : tab
,
583 event
.preventDefault();
585 if ( tab
.hasClass( "ui-state-disabled" ) ||
586 // tab is already loading
587 tab
.hasClass( "ui-tabs-loading" ) ||
588 // can't switch durning an animation
590 // click on active header, but not collapsible
591 ( clickedIsActive
&& !options
.collapsible
) ||
592 // allow canceling activation
593 ( this._trigger( "beforeActivate", event
, eventData
) === false ) ) {
597 options
.active
= collapsing
? false : this.tabs
.index( tab
);
599 this.active
= clickedIsActive
? $() : tab
;
604 if ( !toHide
.length
&& !toShow
.length
) {
605 $.error( "jQuery UI Tabs: Mismatching fragment identifier." );
608 if ( toShow
.length
) {
609 this.load( this.tabs
.index( tab
), event
);
611 this._toggle( event
, eventData
);
614 // handles show/hide for selecting tabs
615 _toggle: function( event
, eventData
) {
617 toShow
= eventData
.newPanel
,
618 toHide
= eventData
.oldPanel
;
622 function complete() {
623 that
.running
= false;
624 that
._trigger( "activate", event
, eventData
);
628 eventData
.newTab
.closest( "li" ).addClass( "ui-tabs-active ui-state-active" );
630 if ( toShow
.length
&& that
.options
.show
) {
631 that
._show( toShow
, that
.options
.show
, complete
);
638 // start out by hiding, then showing, then completing
639 if ( toHide
.length
&& this.options
.hide
) {
640 this._hide( toHide
, this.options
.hide
, function() {
641 eventData
.oldTab
.closest( "li" ).removeClass( "ui-tabs-active ui-state-active" );
645 eventData
.oldTab
.closest( "li" ).removeClass( "ui-tabs-active ui-state-active" );
650 toHide
.attr( "aria-hidden", "true" );
651 eventData
.oldTab
.attr({
652 "aria-selected": "false",
653 "aria-expanded": "false"
655 // If we're switching tabs, remove the old tab from the tab order.
656 // If we're opening from collapsed state, remove the previous tab from the tab order.
657 // If we're collapsing, then keep the collapsing tab in the tab order.
658 if ( toShow
.length
&& toHide
.length
) {
659 eventData
.oldTab
.attr( "tabIndex", -1 );
660 } else if ( toShow
.length
) {
661 this.tabs
.filter(function() {
662 return $( this ).attr( "tabIndex" ) === 0;
664 .attr( "tabIndex", -1 );
667 toShow
.attr( "aria-hidden", "false" );
668 eventData
.newTab
.attr({
669 "aria-selected": "true",
670 "aria-expanded": "true",
675 _activate: function( index
) {
677 active
= this._findActive( index
);
679 // trying to activate the already active panel
680 if ( active
[ 0 ] === this.active
[ 0 ] ) {
684 // trying to collapse, simulate a click on the current active header
685 if ( !active
.length
) {
686 active
= this.active
;
689 anchor
= active
.find( ".ui-tabs-anchor" )[ 0 ];
692 currentTarget
: anchor
,
693 preventDefault
: $.noop
697 _findActive: function( index
) {
698 return index
=== false ? $() : this.tabs
.eq( index
);
701 _getIndex: function( index
) {
702 // meta-function to give users option to provide a href string instead of a numerical index.
703 if ( typeof index
=== "string" ) {
704 index
= this.anchors
.index( this.anchors
.filter( "[href$='" + index
+ "']" ) );
710 _destroy: function() {
715 this.element
.removeClass( "ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible" );
718 .removeClass( "ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all" )
719 .removeAttr( "role" );
722 .removeClass( "ui-tabs-anchor" )
723 .removeAttr( "role" )
724 .removeAttr( "tabIndex" )
727 this.tablist
.unbind( this.eventNamespace
);
729 this.tabs
.add( this.panels
).each(function() {
730 if ( $.data( this, "ui-tabs-destroy" ) ) {
734 .removeClass( "ui-state-default ui-state-active ui-state-disabled " +
735 "ui-corner-top ui-corner-bottom ui-widget-content ui-tabs-active ui-tabs-panel" )
736 .removeAttr( "tabIndex" )
737 .removeAttr( "aria-live" )
738 .removeAttr( "aria-busy" )
739 .removeAttr( "aria-selected" )
740 .removeAttr( "aria-labelledby" )
741 .removeAttr( "aria-hidden" )
742 .removeAttr( "aria-expanded" )
743 .removeAttr( "role" );
747 this.tabs
.each(function() {
749 prev
= li
.data( "ui-tabs-aria-controls" );
752 .attr( "aria-controls", prev
)
753 .removeData( "ui-tabs-aria-controls" );
755 li
.removeAttr( "aria-controls" );
761 if ( this.options
.heightStyle
!== "content" ) {
762 this.panels
.css( "height", "" );
766 enable: function( index
) {
767 var disabled
= this.options
.disabled
;
768 if ( disabled
=== false ) {
772 if ( index
=== undefined ) {
775 index
= this._getIndex( index
);
776 if ( $.isArray( disabled
) ) {
777 disabled
= $.map( disabled
, function( num
) {
778 return num
!== index
? num
: null;
781 disabled
= $.map( this.tabs
, function( li
, num
) {
782 return num
!== index
? num
: null;
786 this._setupDisabled( disabled
);
789 disable: function( index
) {
790 var disabled
= this.options
.disabled
;
791 if ( disabled
=== true ) {
795 if ( index
=== undefined ) {
798 index
= this._getIndex( index
);
799 if ( $.inArray( index
, disabled
) !== -1 ) {
802 if ( $.isArray( disabled
) ) {
803 disabled
= $.merge( [ index
], disabled
).sort();
805 disabled
= [ index
];
808 this._setupDisabled( disabled
);
811 load: function( index
, event
) {
812 index
= this._getIndex( index
);
814 tab
= this.tabs
.eq( index
),
815 anchor
= tab
.find( ".ui-tabs-anchor" ),
816 panel
= this._getPanelForTab( tab
),
821 complete = function( jqXHR
, status
) {
822 if ( status
=== "abort" ) {
823 that
.panels
.stop( false, true );
826 tab
.removeClass( "ui-tabs-loading" );
827 panel
.removeAttr( "aria-busy" );
829 if ( jqXHR
=== that
.xhr
) {
835 if ( this._isLocal( anchor
[ 0 ] ) ) {
839 this.xhr
= $.ajax( this._ajaxSettings( anchor
, event
, eventData
) );
841 // support: jQuery <1.8
842 // jQuery <1.8 returns false if the request is canceled in beforeSend,
843 // but as of 1.8, $.ajax() always returns a jqXHR object.
844 if ( this.xhr
&& this.xhr
.statusText
!== "canceled" ) {
845 tab
.addClass( "ui-tabs-loading" );
846 panel
.attr( "aria-busy", "true" );
849 .done(function( response
, status
, jqXHR
) {
850 // support: jQuery <1.8
851 // http://bugs.jquery.com/ticket/11778
852 setTimeout(function() {
853 panel
.html( response
);
854 that
._trigger( "load", event
, eventData
);
856 complete( jqXHR
, status
);
859 .fail(function( jqXHR
, status
) {
860 // support: jQuery <1.8
861 // http://bugs.jquery.com/ticket/11778
862 setTimeout(function() {
863 complete( jqXHR
, status
);
869 _ajaxSettings: function( anchor
, event
, eventData
) {
872 url
: anchor
.attr( "href" ),
873 beforeSend: function( jqXHR
, settings
) {
874 return that
._trigger( "beforeLoad", event
,
875 $.extend( { jqXHR
: jqXHR
, ajaxSettings
: settings
}, eventData
) );
880 _getPanelForTab: function( tab
) {
881 var id
= $( tab
).attr( "aria-controls" );
882 return this.element
.find( this._sanitizeSelector( "#" + id
) );