2 * jQuery UI Tabs 1.11.2
5 * Copyright 2014 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 key will prevent automatic activation
205 if ( !event.ctrlKey ) {
206 // Update aria-selected immediately so that AT think the tab is already selected.
207 // Otherwise AT may confuse the user by stating that they need to activate the tab,
208 // but the tab will already be activated by the time the announcement finishes.
209 focusedTab.attr( "aria-selected", "false" );
210 this.tabs.eq( selectedIndex ).attr( "aria-selected", "true" );
212 this.activating = this._delay(function() {
213 this.option( "active", selectedIndex );
218 _panelKeydown: function( event ) {
219 if ( this._handlePageNav( event ) ) {
223 // Ctrl+up moves focus to the current tab
224 if ( event.ctrlKey && event.keyCode === $.ui.keyCode.UP ) {
225 event.preventDefault();
230 // Alt+page up/down moves focus to the previous/next tab (and activates)
231 _handlePageNav: function( event ) {
232 if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_UP ) {
233 this._activate( this._focusNextTab( this.options.active - 1, false ) );
236 if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_DOWN ) {
237 this._activate( this._focusNextTab( this.options.active + 1, true ) );
242 _findNextTab: function( index, goingForward ) {
243 var lastTabIndex = this.tabs.length - 1;
245 function constrain() {
246 if ( index > lastTabIndex ) {
250 index = lastTabIndex;
255 while ( $.inArray( constrain(), this.options.disabled ) !== -1 ) {
256 index = goingForward ? index + 1 : index - 1;
262 _focusNextTab: function( index, goingForward ) {
263 index = this._findNextTab( index, goingForward );
264 this.tabs.eq( index ).focus();
268 _setOption: function( key, value ) {
269 if ( key === "active" ) {
270 // _activate() will handle invalid values and update this.options
271 this._activate( value );
275 if ( key === "disabled" ) {
276 // don't use the widget factory's disabled handling
277 this._setupDisabled( value );
281 this._super( key, value);
283 if ( key === "collapsible" ) {
284 this.element.toggleClass( "ui-tabs-collapsible", value );
285 // Setting collapsible: false while collapsed; open first panel
286 if ( !value && this.options.active === false ) {
291 if ( key === "event" ) {
292 this._setupEvents( value );
295 if ( key === "heightStyle" ) {
296 this._setupHeightStyle( value );
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",
346 "aria-expanded": "false",
349 this.panels.not( this._getPanelForTab( this.active ) )
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",
363 "aria-expanded": "true",
366 this._getPanelForTab( this.active )
369 "aria-hidden": "false"
374 _processTabs: function() {
376 prevTabs = this.tabs,
377 prevAnchors = this.anchors,
378 prevPanels = this.panels;
380 this.tablist = this._getList()
381 .addClass( "ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all" )
382 .attr( "role", "tablist" )
384 // Prevent users from focusing disabled tabs via click
385 .delegate( "> li", "mousedown" + this.eventNamespace, function( event ) {
386 if ( $( this ).is( ".ui-state-disabled" ) ) {
387 event.preventDefault();
392 // Preventing the default action in mousedown doesn't prevent IE
393 // from focusing the element, so if the anchor gets focused, blur.
394 // We don't have to worry about focusing the previously focused
395 // element since clicking on a non-focusable element should focus
397 .delegate( ".ui-tabs-anchor", "focus" + this.eventNamespace, function() {
398 if ( $( this ).closest( "li" ).is( ".ui-state-disabled" ) ) {
403 this.tabs = this.tablist.find( "> li:has(a[href])" )
404 .addClass( "ui-state-default ui-corner-top" )
410 this.anchors = this.tabs.map(function() {
411 return $( "a", this )[ 0 ];
413 .addClass( "ui-tabs-anchor" )
415 role: "presentation",
421 this.anchors.each(function( i, anchor ) {
422 var selector, panel, panelId,
423 anchorId = $( anchor ).uniqueId().attr( "id" ),
424 tab = $( anchor ).closest( "li" ),
425 originalAriaControls = tab.attr( "aria-controls" );
428 if ( that._isLocal( anchor ) ) {
429 selector = anchor.hash;
430 panelId = selector.substring( 1 );
431 panel = that.element.find( that._sanitizeSelector( selector ) );
434 // If the tab doesn't already have aria-controls,
435 // generate an id by using a throw-away element
436 panelId = tab.attr( "aria-controls" ) || $( {} ).uniqueId()[ 0 ].id;
437 selector = "#" + panelId;
438 panel = that.element.find( selector );
439 if ( !panel.length ) {
440 panel = that._createPanel( panelId );
441 panel.insertAfter( that.panels[ i - 1 ] || that.tablist );
443 panel.attr( "aria-live", "polite" );
447 that.panels = that.panels.add( panel );
449 if ( originalAriaControls ) {
450 tab.data( "ui-tabs-aria-controls", originalAriaControls );
453 "aria-controls": panelId,
454 "aria-labelledby": anchorId
456 panel.attr( "aria-labelledby", anchorId );
460 .addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" )
461 .attr( "role", "tabpanel" );
463 // Avoid memory leaks (#10056)
465 this._off( prevTabs.not( this.tabs ) );
466 this._off( prevAnchors.not( this.anchors ) );
467 this._off( prevPanels.not( this.panels ) );
471 // allow overriding how to find the list for rare usage scenarios (#7715)
472 _getList: function() {
473 return this.tablist || this.element.find( "ol,ul" ).eq( 0 );
476 _createPanel: function( id ) {
479 .addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" )
480 .data( "ui-tabs-destroy", true );
483 _setupDisabled: function( disabled ) {
484 if ( $.isArray( disabled ) ) {
485 if ( !disabled.length ) {
487 } else if ( disabled.length === this.anchors.length ) {
493 for ( var i = 0, li; ( li = this.tabs[ i ] ); i++ ) {
494 if ( disabled === true || $.inArray( i, disabled ) !== -1 ) {
496 .addClass( "ui-state-disabled" )
497 .attr( "aria-disabled", "true" );
500 .removeClass( "ui-state-disabled" )
501 .removeAttr( "aria-disabled" );
505 this.options.disabled = disabled;
508 _setupEvents: function( event ) {
511 $.each( event.split(" "), function( index, eventName ) {
512 events[ eventName ] = "_eventHandler";
516 this._off( this.anchors.add( this.tabs ).add( this.panels ) );
517 // Always prevent the default action, even when disabled
518 this._on( true, this.anchors, {
519 click: function( event ) {
520 event.preventDefault();
523 this._on( this.anchors, events );
524 this._on( this.tabs, { keydown: "_tabKeydown" } );
525 this._on( this.panels, { keydown: "_panelKeydown" } );
527 this._focusable( this.tabs );
528 this._hoverable( this.tabs );
531 _setupHeightStyle: function( heightStyle ) {
533 parent = this.element.parent();
535 if ( heightStyle === "fill" ) {
536 maxHeight = parent.height();
537 maxHeight -= this.element.outerHeight() - this.element.height();
539 this.element.siblings( ":visible" ).each(function() {
540 var elem = $( this ),
541 position = elem.css( "position" );
543 if ( position === "absolute" || position === "fixed" ) {
546 maxHeight -= elem.outerHeight( true );
549 this.element.children().not( this.panels ).each(function() {
550 maxHeight -= $( this ).outerHeight( true );
553 this.panels.each(function() {
554 $( this ).height( Math.max( 0, maxHeight -
555 $( this ).innerHeight() + $( this ).height() ) );
557 .css( "overflow", "auto" );
558 } else if ( heightStyle === "auto" ) {
560 this.panels.each(function() {
561 maxHeight = Math.max( maxHeight, $( this ).height( "" ).height() );
562 }).height( maxHeight );
566 _eventHandler: function( event ) {
567 var options = this.options,
568 active = this.active,
569 anchor = $( event.currentTarget ),
570 tab = anchor.closest( "li" ),
571 clickedIsActive = tab[ 0 ] === active[ 0 ],
572 collapsing = clickedIsActive && options.collapsible,
573 toShow = collapsing ? $() : this._getPanelForTab( tab ),
574 toHide = !active.length ? $() : this._getPanelForTab( active ),
578 newTab: collapsing ? $() : tab,
582 event.preventDefault();
584 if ( tab.hasClass( "ui-state-disabled" ) ||
585 // tab is already loading
586 tab.hasClass( "ui-tabs-loading" ) ||
587 // can't switch durning an animation
589 // click on active header, but not collapsible
590 ( clickedIsActive && !options.collapsible ) ||
591 // allow canceling activation
592 ( this._trigger( "beforeActivate", event, eventData ) === false ) ) {
596 options.active = collapsing ? false : this.tabs.index( tab );
598 this.active = clickedIsActive ? $() : tab;
603 if ( !toHide.length && !toShow.length ) {
604 $.error( "jQuery UI Tabs: Mismatching fragment identifier." );
607 if ( toShow.length ) {
608 this.load( this.tabs.index( tab ), event );
610 this._toggle( event, eventData );
613 // handles show/hide for selecting tabs
614 _toggle: function( event, eventData ) {
616 toShow = eventData.newPanel,
617 toHide = eventData.oldPanel;
621 function complete() {
622 that.running = false;
623 that._trigger( "activate", event, eventData );
627 eventData.newTab.closest( "li" ).addClass( "ui-tabs-active ui-state-active" );
629 if ( toShow.length && that.options.show ) {
630 that._show( toShow, that.options.show, complete );
637 // start out by hiding, then showing, then completing
638 if ( toHide.length && this.options.hide ) {
639 this._hide( toHide, this.options.hide, function() {
640 eventData.oldTab.closest( "li" ).removeClass( "ui-tabs-active ui-state-active" );
644 eventData.oldTab.closest( "li" ).removeClass( "ui-tabs-active ui-state-active" );
649 toHide.attr( "aria-hidden", "true" );
650 eventData.oldTab.attr({
651 "aria-selected": "false",
652 "aria-expanded": "false"
654 // If we're switching tabs, remove the old tab from the tab order.
655 // If we're opening from collapsed state, remove the previous tab from the tab order.
656 // If we're collapsing, then keep the collapsing tab in the tab order.
657 if ( toShow.length && toHide.length ) {
658 eventData.oldTab.attr( "tabIndex", -1 );
659 } else if ( toShow.length ) {
660 this.tabs.filter(function() {
661 return $( this ).attr( "tabIndex" ) === 0;
663 .attr( "tabIndex", -1 );
666 toShow.attr( "aria-hidden", "false" );
667 eventData.newTab.attr({
668 "aria-selected": "true",
669 "aria-expanded": "true",
674 _activate: function( index ) {
676 active = this._findActive( index );
678 // trying to activate the already active panel
679 if ( active[ 0 ] === this.active[ 0 ] ) {
683 // trying to collapse, simulate a click on the current active header
684 if ( !active.length ) {
685 active = this.active;
688 anchor = active.find( ".ui-tabs-anchor" )[ 0 ];
691 currentTarget: anchor,
692 preventDefault: $.noop
696 _findActive: function( index ) {
697 return index === false ? $() : this.tabs.eq( index );
700 _getIndex: function( index ) {
701 // meta-function to give users option to provide a href string instead of a numerical index.
702 if ( typeof index === "string" ) {
703 index = this.anchors.index( this.anchors.filter( "[href$='" + index + "']" ) );
709 _destroy: function() {
714 this.element.removeClass( "ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible" );
717 .removeClass( "ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all" )
718 .removeAttr( "role" );
721 .removeClass( "ui-tabs-anchor" )
722 .removeAttr( "role" )
723 .removeAttr( "tabIndex" )
726 this.tablist.unbind( this.eventNamespace );
728 this.tabs.add( this.panels ).each(function() {
729 if ( $.data( this, "ui-tabs-destroy" ) ) {
733 .removeClass( "ui-state-default ui-state-active ui-state-disabled " +
734 "ui-corner-top ui-corner-bottom ui-widget-content ui-tabs-active ui-tabs-panel" )
735 .removeAttr( "tabIndex" )
736 .removeAttr( "aria-live" )
737 .removeAttr( "aria-busy" )
738 .removeAttr( "aria-selected" )
739 .removeAttr( "aria-labelledby" )
740 .removeAttr( "aria-hidden" )
741 .removeAttr( "aria-expanded" )
742 .removeAttr( "role" );
746 this.tabs.each(function() {
748 prev = li.data( "ui-tabs-aria-controls" );
751 .attr( "aria-controls", prev )
752 .removeData( "ui-tabs-aria-controls" );
754 li.removeAttr( "aria-controls" );
760 if ( this.options.heightStyle !== "content" ) {
761 this.panels.css( "height", "" );
765 enable: function( index ) {
766 var disabled = this.options.disabled;
767 if ( disabled === false ) {
771 if ( index === undefined ) {
774 index = this._getIndex( index );
775 if ( $.isArray( disabled ) ) {
776 disabled = $.map( disabled, function( num ) {
777 return num !== index ? num : null;
780 disabled = $.map( this.tabs, function( li, num ) {
781 return num !== index ? num : null;
785 this._setupDisabled( disabled );
788 disable: function( index ) {
789 var disabled = this.options.disabled;
790 if ( disabled === true ) {
794 if ( index === undefined ) {
797 index = this._getIndex( index );
798 if ( $.inArray( index, disabled ) !== -1 ) {
801 if ( $.isArray( disabled ) ) {
802 disabled = $.merge( [ index ], disabled ).sort();
804 disabled = [ index ];
807 this._setupDisabled( disabled );
810 load: function( index, event ) {
811 index = this._getIndex( index );
813 tab = this.tabs.eq( index ),
814 anchor = tab.find( ".ui-tabs-anchor" ),
815 panel = this._getPanelForTab( tab ),
822 if ( this._isLocal( anchor[ 0 ] ) ) {
826 this.xhr = $.ajax( this._ajaxSettings( anchor, event, eventData ) );
828 // support: jQuery <1.8
829 // jQuery <1.8 returns false if the request is canceled in beforeSend,
830 // but as of 1.8, $.ajax() always returns a jqXHR object.
831 if ( this.xhr && this.xhr.statusText !== "canceled" ) {
832 tab.addClass( "ui-tabs-loading" );
833 panel.attr( "aria-busy", "true" );
836 .success(function( response ) {
837 // support: jQuery <1.8
838 // http://bugs.jquery.com/ticket/11778
839 setTimeout(function() {
840 panel.html( response );
841 that._trigger( "load", event, eventData );
844 .complete(function( jqXHR, status ) {
845 // support: jQuery <1.8
846 // http://bugs.jquery.com/ticket/11778
847 setTimeout(function() {
848 if ( status === "abort" ) {
849 that.panels.stop( false, true );
852 tab.removeClass( "ui-tabs-loading" );
853 panel.removeAttr( "aria-busy" );
855 if ( jqXHR === that.xhr ) {
863 _ajaxSettings: function( anchor, event, eventData ) {
866 url: anchor.attr( "href" ),
867 beforeSend: function( jqXHR, settings ) {
868 return that._trigger( "beforeLoad", event,
869 $.extend( { jqXHR: jqXHR, ajaxSettings: settings }, eventData ) );
874 _getPanelForTab: function( tab ) {
875 var id = $( tab ).attr( "aria-controls" );
876 return this.element.find( this._sanitizeSelector( "#" + id ) );