2 * jQuery UI Tabs 1.10.2
5 * Copyright 2013 jQuery Foundation and other contributors
6 * Released under the MIT license.
7 * http://jquery.org/license
9 * http://api.jqueryui.com/tabs/
15 (function( $, undefined ) {
20 function getNextTabId() {
24 function isLocal( anchor ) {
25 return anchor.hash.length > 1 &&
26 decodeURIComponent( anchor.href.replace( rhash, "" ) ) ===
27 decodeURIComponent( location.href.replace( rhash, "" ) );
30 $.widget( "ui.tabs", {
37 heightStyle: "content",
50 options = this.options;
55 .addClass( "ui-tabs ui-widget ui-widget-content ui-corner-all" )
56 .toggleClass( "ui-tabs-collapsible", options.collapsible )
57 // Prevent users from focusing disabled tabs via click
58 .delegate( ".ui-tabs-nav > li", "mousedown" + this.eventNamespace, function( event ) {
59 if ( $( this ).is( ".ui-state-disabled" ) ) {
60 event.preventDefault();
64 // Preventing the default action in mousedown doesn't prevent IE
65 // from focusing the element, so if the anchor gets focused, blur.
66 // We don't have to worry about focusing the previously focused
67 // element since clicking on a non-focusable element should focus
69 .delegate( ".ui-tabs-anchor", "focus" + this.eventNamespace, function() {
70 if ( $( this ).closest( "li" ).is( ".ui-state-disabled" ) ) {
76 options.active = this._initialActive();
78 // Take disabling tabs via class attribute from HTML
79 // into account and update option properly.
80 if ( $.isArray( options.disabled ) ) {
81 options.disabled = $.unique( options.disabled.concat(
82 $.map( this.tabs.filter( ".ui-state-disabled" ), function( li ) {
83 return that.tabs.index( li );
88 // check for length avoids error when initializing empty list
89 if ( this.options.active !== false && this.anchors.length ) {
90 this.active = this._findActive( options.active );
97 if ( this.active.length ) {
98 this.load( options.active );
102 _initialActive: function() {
103 var active = this.options.active,
104 collapsible = this.options.collapsible,
105 locationHash = location.hash.substring( 1 );
107 if ( active === null ) {
108 // check the fragment identifier in the URL
109 if ( locationHash ) {
110 this.tabs.each(function( i, tab ) {
111 if ( $( tab ).attr( "aria-controls" ) === locationHash ) {
118 // check for a tab marked active via a class
119 if ( active === null ) {
120 active = this.tabs.index( this.tabs.filter( ".ui-tabs-active" ) );
123 // no active tab, set to false
124 if ( active === null || active === -1 ) {
125 active = this.tabs.length ? 0 : false;
129 // handle numbers: negative, out of range
130 if ( active !== false ) {
131 active = this.tabs.index( this.tabs.eq( active ) );
132 if ( active === -1 ) {
133 active = collapsible ? false : 0;
137 // don't allow collapsible: false and active: false
138 if ( !collapsible && active === false && this.anchors.length ) {
145 _getCreateEventData: function() {
148 panel: !this.active.length ? $() : this._getPanelForTab( this.active )
152 _tabKeydown: function( event ) {
153 /*jshint maxcomplexity:15*/
154 var focusedTab = $( this.document[0].activeElement ).closest( "li" ),
155 selectedIndex = this.tabs.index( focusedTab ),
158 if ( this._handlePageNav( event ) ) {
162 switch ( event.keyCode ) {
163 case $.ui.keyCode.RIGHT:
164 case $.ui.keyCode.DOWN:
167 case $.ui.keyCode.UP:
168 case $.ui.keyCode.LEFT:
169 goingForward = false;
172 case $.ui.keyCode.END:
173 selectedIndex = this.anchors.length - 1;
175 case $.ui.keyCode.HOME:
178 case $.ui.keyCode.SPACE:
179 // Activate only, no collapsing
180 event.preventDefault();
181 clearTimeout( this.activating );
182 this._activate( selectedIndex );
184 case $.ui.keyCode.ENTER:
185 // Toggle (cancel delayed activation, allow collapsing)
186 event.preventDefault();
187 clearTimeout( this.activating );
188 // Determine if we should collapse or activate
189 this._activate( selectedIndex === this.options.active ? false : selectedIndex );
195 // Focus the appropriate tab, based on which key was pressed
196 event.preventDefault();
197 clearTimeout( this.activating );
198 selectedIndex = this._focusNextTab( selectedIndex, goingForward );
200 // Navigating with control key will prevent automatic activation
201 if ( !event.ctrlKey ) {
202 // Update aria-selected immediately so that AT think the tab is already selected.
203 // Otherwise AT may confuse the user by stating that they need to activate the tab,
204 // but the tab will already be activated by the time the announcement finishes.
205 focusedTab.attr( "aria-selected", "false" );
206 this.tabs.eq( selectedIndex ).attr( "aria-selected", "true" );
208 this.activating = this._delay(function() {
209 this.option( "active", selectedIndex );
214 _panelKeydown: function( event ) {
215 if ( this._handlePageNav( event ) ) {
219 // Ctrl+up moves focus to the current tab
220 if ( event.ctrlKey && event.keyCode === $.ui.keyCode.UP ) {
221 event.preventDefault();
226 // Alt+page up/down moves focus to the previous/next tab (and activates)
227 _handlePageNav: function( event ) {
228 if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_UP ) {
229 this._activate( this._focusNextTab( this.options.active - 1, false ) );
232 if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_DOWN ) {
233 this._activate( this._focusNextTab( this.options.active + 1, true ) );
238 _findNextTab: function( index, goingForward ) {
239 var lastTabIndex = this.tabs.length - 1;
241 function constrain() {
242 if ( index > lastTabIndex ) {
246 index = lastTabIndex;
251 while ( $.inArray( constrain(), this.options.disabled ) !== -1 ) {
252 index = goingForward ? index + 1 : index - 1;
258 _focusNextTab: function( index, goingForward ) {
259 index = this._findNextTab( index, goingForward );
260 this.tabs.eq( index ).focus();
264 _setOption: function( key, value ) {
265 if ( key === "active" ) {
266 // _activate() will handle invalid values and update this.options
267 this._activate( value );
271 if ( key === "disabled" ) {
272 // don't use the widget factory's disabled handling
273 this._setupDisabled( value );
277 this._super( key, value);
279 if ( key === "collapsible" ) {
280 this.element.toggleClass( "ui-tabs-collapsible", value );
281 // Setting collapsible: false while collapsed; open first panel
282 if ( !value && this.options.active === false ) {
287 if ( key === "event" ) {
288 this._setupEvents( value );
291 if ( key === "heightStyle" ) {
292 this._setupHeightStyle( value );
296 _tabId: function( tab ) {
297 return tab.attr( "aria-controls" ) || "ui-tabs-" + getNextTabId();
300 _sanitizeSelector: function( hash ) {
301 return hash ? hash.replace( /[!"$%&'()*+,.\/:;<=>?@\[\]\^`{|}~]/g, "\\$&" ) : "";
304 refresh: function() {
305 var options = this.options,
306 lis = this.tablist.children( ":has(a[href])" );
308 // get disabled tabs from class attribute from HTML
309 // this will get converted to a boolean if needed in _refresh()
310 options.disabled = $.map( lis.filter( ".ui-state-disabled" ), function( tab ) {
311 return lis.index( tab );
316 // was collapsed or no tabs
317 if ( options.active === false || !this.anchors.length ) {
318 options.active = false;
320 // was active, but active tab is gone
321 } else if ( this.active.length && !$.contains( this.tablist[ 0 ], this.active[ 0 ] ) ) {
322 // all remaining tabs are disabled
323 if ( this.tabs.length === options.disabled.length ) {
324 options.active = false;
326 // activate previous tab
328 this._activate( this._findNextTab( Math.max( 0, options.active - 1 ), false ) );
330 // was active, active tab still exists
332 // make sure active index is correct
333 options.active = this.tabs.index( this.active );
339 _refresh: function() {
340 this._setupDisabled( this.options.disabled );
341 this._setupEvents( this.options.event );
342 this._setupHeightStyle( this.options.heightStyle );
344 this.tabs.not( this.active ).attr({
345 "aria-selected": "false",
348 this.panels.not( this._getPanelForTab( this.active ) )
351 "aria-expanded": "false",
352 "aria-hidden": "true"
355 // Make sure one tab is in the tab order
356 if ( !this.active.length ) {
357 this.tabs.eq( 0 ).attr( "tabIndex", 0 );
360 .addClass( "ui-tabs-active ui-state-active" )
362 "aria-selected": "true",
365 this._getPanelForTab( this.active )
368 "aria-expanded": "true",
369 "aria-hidden": "false"
374 _processTabs: function() {
377 this.tablist = this._getList()
378 .addClass( "ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all" )
379 .attr( "role", "tablist" );
381 this.tabs = this.tablist.find( "> li:has(a[href])" )
382 .addClass( "ui-state-default ui-corner-top" )
388 this.anchors = this.tabs.map(function() {
389 return $( "a", this )[ 0 ];
391 .addClass( "ui-tabs-anchor" )
393 role: "presentation",
399 this.anchors.each(function( i, anchor ) {
400 var selector, panel, panelId,
401 anchorId = $( anchor ).uniqueId().attr( "id" ),
402 tab = $( anchor ).closest( "li" ),
403 originalAriaControls = tab.attr( "aria-controls" );
406 if ( isLocal( anchor ) ) {
407 selector = anchor.hash;
408 panel = that.element.find( that._sanitizeSelector( selector ) );
411 panelId = that._tabId( tab );
412 selector = "#" + panelId;
413 panel = that.element.find( selector );
414 if ( !panel.length ) {
415 panel = that._createPanel( panelId );
416 panel.insertAfter( that.panels[ i - 1 ] || that.tablist );
418 panel.attr( "aria-live", "polite" );
422 that.panels = that.panels.add( panel );
424 if ( originalAriaControls ) {
425 tab.data( "ui-tabs-aria-controls", originalAriaControls );
428 "aria-controls": selector.substring( 1 ),
429 "aria-labelledby": anchorId
431 panel.attr( "aria-labelledby", anchorId );
435 .addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" )
436 .attr( "role", "tabpanel" );
439 // allow overriding how to find the list for rare usage scenarios (#7715)
440 _getList: function() {
441 return this.element.find( "ol,ul" ).eq( 0 );
444 _createPanel: function( id ) {
447 .addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" )
448 .data( "ui-tabs-destroy", true );
451 _setupDisabled: function( disabled ) {
452 if ( $.isArray( disabled ) ) {
453 if ( !disabled.length ) {
455 } else if ( disabled.length === this.anchors.length ) {
461 for ( var i = 0, li; ( li = this.tabs[ i ] ); i++ ) {
462 if ( disabled === true || $.inArray( i, disabled ) !== -1 ) {
464 .addClass( "ui-state-disabled" )
465 .attr( "aria-disabled", "true" );
468 .removeClass( "ui-state-disabled" )
469 .removeAttr( "aria-disabled" );
473 this.options.disabled = disabled;
476 _setupEvents: function( event ) {
478 click: function( event ) {
479 event.preventDefault();
483 $.each( event.split(" "), function( index, eventName ) {
484 events[ eventName ] = "_eventHandler";
488 this._off( this.anchors.add( this.tabs ).add( this.panels ) );
489 this._on( this.anchors, events );
490 this._on( this.tabs, { keydown: "_tabKeydown" } );
491 this._on( this.panels, { keydown: "_panelKeydown" } );
493 this._focusable( this.tabs );
494 this._hoverable( this.tabs );
497 _setupHeightStyle: function( heightStyle ) {
499 parent = this.element.parent();
501 if ( heightStyle === "fill" ) {
502 maxHeight = parent.height();
503 maxHeight -= this.element.outerHeight() - this.element.height();
505 this.element.siblings( ":visible" ).each(function() {
506 var elem = $( this ),
507 position = elem.css( "position" );
509 if ( position === "absolute" || position === "fixed" ) {
512 maxHeight -= elem.outerHeight( true );
515 this.element.children().not( this.panels ).each(function() {
516 maxHeight -= $( this ).outerHeight( true );
519 this.panels.each(function() {
520 $( this ).height( Math.max( 0, maxHeight -
521 $( this ).innerHeight() + $( this ).height() ) );
523 .css( "overflow", "auto" );
524 } else if ( heightStyle === "auto" ) {
526 this.panels.each(function() {
527 maxHeight = Math.max( maxHeight, $( this ).height( "" ).height() );
528 }).height( maxHeight );
532 _eventHandler: function( event ) {
533 var options = this.options,
534 active = this.active,
535 anchor = $( event.currentTarget ),
536 tab = anchor.closest( "li" ),
537 clickedIsActive = tab[ 0 ] === active[ 0 ],
538 collapsing = clickedIsActive && options.collapsible,
539 toShow = collapsing ? $() : this._getPanelForTab( tab ),
540 toHide = !active.length ? $() : this._getPanelForTab( active ),
544 newTab: collapsing ? $() : tab,
548 event.preventDefault();
550 if ( tab.hasClass( "ui-state-disabled" ) ||
551 // tab is already loading
552 tab.hasClass( "ui-tabs-loading" ) ||
553 // can't switch durning an animation
555 // click on active header, but not collapsible
556 ( clickedIsActive && !options.collapsible ) ||
557 // allow canceling activation
558 ( this._trigger( "beforeActivate", event, eventData ) === false ) ) {
562 options.active = collapsing ? false : this.tabs.index( tab );
564 this.active = clickedIsActive ? $() : tab;
569 if ( !toHide.length && !toShow.length ) {
570 $.error( "jQuery UI Tabs: Mismatching fragment identifier." );
573 if ( toShow.length ) {
574 this.load( this.tabs.index( tab ), event );
576 this._toggle( event, eventData );
579 // handles show/hide for selecting tabs
580 _toggle: function( event, eventData ) {
582 toShow = eventData.newPanel,
583 toHide = eventData.oldPanel;
587 function complete() {
588 that.running = false;
589 that._trigger( "activate", event, eventData );
593 eventData.newTab.closest( "li" ).addClass( "ui-tabs-active ui-state-active" );
595 if ( toShow.length && that.options.show ) {
596 that._show( toShow, that.options.show, complete );
603 // start out by hiding, then showing, then completing
604 if ( toHide.length && this.options.hide ) {
605 this._hide( toHide, this.options.hide, function() {
606 eventData.oldTab.closest( "li" ).removeClass( "ui-tabs-active ui-state-active" );
610 eventData.oldTab.closest( "li" ).removeClass( "ui-tabs-active ui-state-active" );
616 "aria-expanded": "false",
617 "aria-hidden": "true"
619 eventData.oldTab.attr( "aria-selected", "false" );
620 // If we're switching tabs, remove the old tab from the tab order.
621 // If we're opening from collapsed state, remove the previous tab from the tab order.
622 // If we're collapsing, then keep the collapsing tab in the tab order.
623 if ( toShow.length && toHide.length ) {
624 eventData.oldTab.attr( "tabIndex", -1 );
625 } else if ( toShow.length ) {
626 this.tabs.filter(function() {
627 return $( this ).attr( "tabIndex" ) === 0;
629 .attr( "tabIndex", -1 );
633 "aria-expanded": "true",
634 "aria-hidden": "false"
636 eventData.newTab.attr({
637 "aria-selected": "true",
642 _activate: function( index ) {
644 active = this._findActive( index );
646 // trying to activate the already active panel
647 if ( active[ 0 ] === this.active[ 0 ] ) {
651 // trying to collapse, simulate a click on the current active header
652 if ( !active.length ) {
653 active = this.active;
656 anchor = active.find( ".ui-tabs-anchor" )[ 0 ];
659 currentTarget: anchor,
660 preventDefault: $.noop
664 _findActive: function( index ) {
665 return index === false ? $() : this.tabs.eq( index );
668 _getIndex: function( index ) {
669 // meta-function to give users option to provide a href string instead of a numerical index.
670 if ( typeof index === "string" ) {
671 index = this.anchors.index( this.anchors.filter( "[href$='" + index + "']" ) );
677 _destroy: function() {
682 this.element.removeClass( "ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible" );
685 .removeClass( "ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all" )
686 .removeAttr( "role" );
689 .removeClass( "ui-tabs-anchor" )
690 .removeAttr( "role" )
691 .removeAttr( "tabIndex" )
694 this.tabs.add( this.panels ).each(function() {
695 if ( $.data( this, "ui-tabs-destroy" ) ) {
699 .removeClass( "ui-state-default ui-state-active ui-state-disabled " +
700 "ui-corner-top ui-corner-bottom ui-widget-content ui-tabs-active ui-tabs-panel" )
701 .removeAttr( "tabIndex" )
702 .removeAttr( "aria-live" )
703 .removeAttr( "aria-busy" )
704 .removeAttr( "aria-selected" )
705 .removeAttr( "aria-labelledby" )
706 .removeAttr( "aria-hidden" )
707 .removeAttr( "aria-expanded" )
708 .removeAttr( "role" );
712 this.tabs.each(function() {
714 prev = li.data( "ui-tabs-aria-controls" );
717 .attr( "aria-controls", prev )
718 .removeData( "ui-tabs-aria-controls" );
720 li.removeAttr( "aria-controls" );
726 if ( this.options.heightStyle !== "content" ) {
727 this.panels.css( "height", "" );
731 enable: function( index ) {
732 var disabled = this.options.disabled;
733 if ( disabled === false ) {
737 if ( index === undefined ) {
740 index = this._getIndex( index );
741 if ( $.isArray( disabled ) ) {
742 disabled = $.map( disabled, function( num ) {
743 return num !== index ? num : null;
746 disabled = $.map( this.tabs, function( li, num ) {
747 return num !== index ? num : null;
751 this._setupDisabled( disabled );
754 disable: function( index ) {
755 var disabled = this.options.disabled;
756 if ( disabled === true ) {
760 if ( index === undefined ) {
763 index = this._getIndex( index );
764 if ( $.inArray( index, disabled ) !== -1 ) {
767 if ( $.isArray( disabled ) ) {
768 disabled = $.merge( [ index ], disabled ).sort();
770 disabled = [ index ];
773 this._setupDisabled( disabled );
776 load: function( index, event ) {
777 index = this._getIndex( index );
779 tab = this.tabs.eq( index ),
780 anchor = tab.find( ".ui-tabs-anchor" ),
781 panel = this._getPanelForTab( tab ),
788 if ( isLocal( anchor[ 0 ] ) ) {
792 this.xhr = $.ajax( this._ajaxSettings( anchor, event, eventData ) );
794 // support: jQuery <1.8
795 // jQuery <1.8 returns false if the request is canceled in beforeSend,
796 // but as of 1.8, $.ajax() always returns a jqXHR object.
797 if ( this.xhr && this.xhr.statusText !== "canceled" ) {
798 tab.addClass( "ui-tabs-loading" );
799 panel.attr( "aria-busy", "true" );
802 .success(function( response ) {
803 // support: jQuery <1.8
804 // http://bugs.jquery.com/ticket/11778
805 setTimeout(function() {
806 panel.html( response );
807 that._trigger( "load", event, eventData );
810 .complete(function( jqXHR, status ) {
811 // support: jQuery <1.8
812 // http://bugs.jquery.com/ticket/11778
813 setTimeout(function() {
814 if ( status === "abort" ) {
815 that.panels.stop( false, true );
818 tab.removeClass( "ui-tabs-loading" );
819 panel.removeAttr( "aria-busy" );
821 if ( jqXHR === that.xhr ) {
829 _ajaxSettings: function( anchor, event, eventData ) {
832 url: anchor.attr( "href" ),
833 beforeSend: function( jqXHR, settings ) {
834 return that._trigger( "beforeLoad", event,
835 $.extend( { jqXHR : jqXHR, ajaxSettings: settings }, eventData ) );
840 _getPanelForTab: function( tab ) {
841 var id = $( tab ).attr( "aria-controls" );
842 return this.element.find( this._sanitizeSelector( "#" + id ) );