2 * jQuery UI Tabs 1.8.24
4 * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
5 * Dual licensed under the MIT or GPL Version 2 licenses.
6 * http://jquery.org/license
8 * http://docs.jquery.com/UI/Tabs
14 (function( $, undefined ) {
19 function getNextTabId() {
23 function getNextListId() {
27 $.widget( "ui.tabs", {
32 cookie
: null, // e.g. { expires: 7, path: '/', domain: 'jquery.com', secure: true }
38 fx
: null, // e.g. { height: 'toggle', opacity: 'toggle', duration: 200 }
41 panelTemplate
: "<div></div>",
45 spinner
: "<em>Loading…</em>",
46 tabTemplate
: "<li><a href='#{href}'><span>#{label}</span></a></li>"
53 _setOption: function( key
, value
) {
54 if ( key
== "selected" ) {
55 if (this.options
.collapsible
&& value
== this.options
.selected
) {
60 this.options
[ key
] = value
;
65 _tabId: function( a
) {
66 return a
.title
&& a
.title
.replace( /\s/g, "_" ).replace( /[^\w\u00c0-\uFFFF-]/g, "" ) ||
67 this.options
.idPrefix
+ getNextTabId();
70 _sanitizeSelector: function( hash
) {
71 // we need this because an id may contain a ":"
72 return hash
.replace( /:/g
, "\\:" );
76 var cookie
= this.cookie
||
77 ( this.cookie
= this.options
.cookie
.name
|| "ui-tabs-" + getNextListId() );
78 return $.cookie
.apply( null, [ cookie
].concat( $.makeArray( arguments
) ) );
81 _ui: function( tab
, panel
) {
85 index
: this.anchors
.index( tab
)
89 _cleanup: function() {
90 // restore all former loading tabs labels
91 this.lis
.filter( ".ui-state-processing" )
92 .removeClass( "ui-state-processing" )
93 .find( "span:data(label.tabs)" )
96 el
.html( el
.data( "label.tabs" ) ).removeData( "label.tabs" );
100 _tabify: function( init
) {
103 fragmentId
= /^#.+/; // Safari 2 reports '#' for an empty hash
105 this.list
= this.element
.find( "ol,ul" ).eq( 0 );
106 this.lis
= $( " > li:has(a[href])", this.list
);
107 this.anchors
= this.lis
.map(function() {
108 return $( "a", this )[ 0 ];
110 this.panels
= $( [] );
112 this.anchors
.each(function( i
, a
) {
113 var href
= $( a
).attr( "href" );
114 // For dynamically created HTML that contains a hash as href IE < 8 expands
115 // such href to the full page url with hash and then misinterprets tab as ajax.
116 // Same consideration applies for an added tab with a fragment identifier
117 // since a[href=#fragment-identifier] does unexpectedly not match.
118 // Thus normalize href attribute...
119 var hrefBase
= href
.split( "#" )[ 0 ],
121 if ( hrefBase
&& ( hrefBase
=== location
.toString().split( "#" )[ 0 ] ||
122 ( baseEl
= $( "base" )[ 0 ]) && hrefBase
=== baseEl
.href
) ) {
128 if ( fragmentId
.test( href
) ) {
129 self
.panels
= self
.panels
.add( self
.element
.find( self
._sanitizeSelector( href
) ) );
131 // prevent loading the page itself if href is just "#"
132 } else if ( href
&& href
!== "#" ) {
133 // required for restore on destroy
134 $.data( a
, "href.tabs", href
);
136 // TODO until #3808 is fixed strip fragment identifier from url
137 // (IE fails to load from such url)
138 $.data( a
, "load.tabs", href
.replace( /#.*$/, "" ) );
140 var id
= self
._tabId( a
);
142 var $panel
= self
.element
.find( "#" + id
);
143 if ( !$panel
.length
) {
144 $panel
= $( o
.panelTemplate
)
146 .addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" )
147 .insertAfter( self
.panels
[ i
- 1 ] || self
.list
);
148 $panel
.data( "destroy.tabs", true );
150 self
.panels
= self
.panels
.add( $panel
);
153 o
.disabled
.push( i
);
157 // initialization from scratch
159 // attach necessary classes for styling
160 this.element
.addClass( "ui-tabs ui-widget ui-widget-content ui-corner-all" );
161 this.list
.addClass( "ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all" );
162 this.lis
.addClass( "ui-state-default ui-corner-top" );
163 this.panels
.addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" );
166 // use "selected" option or try to retrieve:
167 // 1. from fragment identifier in url
169 // 3. from selected class attribute on <li>
170 if ( o
.selected
=== undefined ) {
171 if ( location
.hash
) {
172 this.anchors
.each(function( i
, a
) {
173 if ( a
.hash
== location
.hash
) {
179 if ( typeof o
.selected
!== "number" && o
.cookie
) {
180 o
.selected
= parseInt( self
._cookie(), 10 );
182 if ( typeof o
.selected
!== "number" && this.lis
.filter( ".ui-tabs-selected" ).length
) {
183 o
.selected
= this.lis
.index( this.lis
.filter( ".ui-tabs-selected" ) );
185 o
.selected
= o
.selected
|| ( this.lis
.length
? 0 : -1 );
186 } else if ( o
.selected
=== null ) { // usage of null is deprecated, TODO remove in next release
190 // sanity check - default to first tab...
191 o
.selected
= ( ( o
.selected
>= 0 && this.anchors
[ o
.selected
] ) || o
.selected
< 0 )
195 // Take disabling tabs via class attribute from HTML
196 // into account and update option properly.
197 // A selected tab cannot become disabled.
198 o
.disabled
= $.unique( o
.disabled
.concat(
199 $.map( this.lis
.filter( ".ui-state-disabled" ), function( n
, i
) {
200 return self
.lis
.index( n
);
204 if ( $.inArray( o
.selected
, o
.disabled
) != -1 ) {
205 o
.disabled
.splice( $.inArray( o
.selected
, o
.disabled
), 1 );
208 // highlight selected tab
209 this.panels
.addClass( "ui-tabs-hide" );
210 this.lis
.removeClass( "ui-tabs-selected ui-state-active" );
211 // check for length avoids error when initializing empty list
212 if ( o
.selected
>= 0 && this.anchors
.length
) {
213 self
.element
.find( self
._sanitizeSelector( self
.anchors
[ o
.selected
].hash
) ).removeClass( "ui-tabs-hide" );
214 this.lis
.eq( o
.selected
).addClass( "ui-tabs-selected ui-state-active" );
216 // seems to be expected behavior that the show callback is fired
217 self
.element
.queue( "tabs", function() {
218 self
._trigger( "show", null,
219 self
._ui( self
.anchors
[ o
.selected
], self
.element
.find( self
._sanitizeSelector( self
.anchors
[ o
.selected
].hash
) )[ 0 ] ) );
222 this.load( o
.selected
);
225 // clean up to avoid memory leaks in certain versions of IE 6
226 // TODO: namespace this event
227 $( window
).bind( "unload", function() {
228 self
.lis
.add( self
.anchors
).unbind( ".tabs" );
229 self
.lis
= self
.anchors
= self
.panels
= null;
231 // update selected after add/remove
233 o
.selected
= this.lis
.index( this.lis
.filter( ".ui-tabs-selected" ) );
236 // update collapsible
237 // TODO: use .toggleClass()
238 this.element
[ o
.collapsible
? "addClass" : "removeClass" ]( "ui-tabs-collapsible" );
240 // set or update cookie after init and add/remove respectively
242 this._cookie( o
.selected
, o
.cookie
);
246 for ( var i
= 0, li
; ( li
= this.lis
[ i
] ); i
++ ) {
247 $( li
)[ $.inArray( i
, o
.disabled
) != -1 &&
248 // TODO: use .toggleClass()
249 !$( li
).hasClass( "ui-tabs-selected" ) ? "addClass" : "removeClass" ]( "ui-state-disabled" );
252 // reset cache if switching from cached to not cached
253 if ( o
.cache
=== false ) {
254 this.anchors
.removeData( "cache.tabs" );
257 // remove all handlers before, tabify may run on existing tabs after add or option change
258 this.lis
.add( this.anchors
).unbind( ".tabs" );
260 if ( o
.event
!== "mouseover" ) {
261 var addState = function( state
, el
) {
262 if ( el
.is( ":not(.ui-state-disabled)" ) ) {
263 el
.addClass( "ui-state-" + state
);
266 var removeState = function( state
, el
) {
267 el
.removeClass( "ui-state-" + state
);
269 this.lis
.bind( "mouseover.tabs" , function() {
270 addState( "hover", $( this ) );
272 this.lis
.bind( "mouseout.tabs", function() {
273 removeState( "hover", $( this ) );
275 this.anchors
.bind( "focus.tabs", function() {
276 addState( "focus", $( this ).closest( "li" ) );
278 this.anchors
.bind( "blur.tabs", function() {
279 removeState( "focus", $( this ).closest( "li" ) );
286 if ( $.isArray( o
.fx
) ) {
290 hideFx
= showFx
= o
.fx
;
294 // Reset certain styles left over from animation
295 // and prevent IE's ClearType bug...
296 function resetStyle( $el
, fx
) {
297 $el
.css( "display", "" );
298 if ( !$.support
.opacity
&& fx
.opacity
) {
299 $el
[ 0 ].style
.removeAttribute( "filter" );
305 ? function( clicked
, $show
) {
306 $( clicked
).closest( "li" ).addClass( "ui-tabs-selected ui-state-active" );
307 $show
.hide().removeClass( "ui-tabs-hide" ) // avoid flicker that way
308 .animate( showFx
, showFx
.duration
|| "normal", function() {
309 resetStyle( $show
, showFx
);
310 self
._trigger( "show", null, self
._ui( clicked
, $show
[ 0 ] ) );
313 : function( clicked
, $show
) {
314 $( clicked
).closest( "li" ).addClass( "ui-tabs-selected ui-state-active" );
315 $show
.removeClass( "ui-tabs-hide" );
316 self
._trigger( "show", null, self
._ui( clicked
, $show
[ 0 ] ) );
319 // Hide a tab, $show is optional...
321 ? function( clicked
, $hide
) {
322 $hide
.animate( hideFx
, hideFx
.duration
|| "normal", function() {
323 self
.lis
.removeClass( "ui-tabs-selected ui-state-active" );
324 $hide
.addClass( "ui-tabs-hide" );
325 resetStyle( $hide
, hideFx
);
326 self
.element
.dequeue( "tabs" );
329 : function( clicked
, $hide
, $show
) {
330 self
.lis
.removeClass( "ui-tabs-selected ui-state-active" );
331 $hide
.addClass( "ui-tabs-hide" );
332 self
.element
.dequeue( "tabs" );
335 // attach tab event handler, unbind to avoid duplicates from former tabifying...
336 this.anchors
.bind( o
.event
+ ".tabs", function() {
338 $li
= $(el
).closest( "li" ),
339 $hide
= self
.panels
.filter( ":not(.ui-tabs-hide)" ),
340 $show
= self
.element
.find( self
._sanitizeSelector( el
.hash
) );
342 // If tab is already selected and not collapsible or tab disabled or
343 // or is already loading or click callback returns false stop here.
344 // Check if click handler returns false last so that it is not executed
345 // for a disabled or loading tab!
346 if ( ( $li
.hasClass( "ui-tabs-selected" ) && !o
.collapsible
) ||
347 $li
.hasClass( "ui-state-disabled" ) ||
348 $li
.hasClass( "ui-state-processing" ) ||
349 self
.panels
.filter( ":animated" ).length
||
350 self
._trigger( "select", null, self
._ui( this, $show
[ 0 ] ) ) === false ) {
355 o
.selected
= self
.anchors
.index( this );
359 // if tab may be closed
360 if ( o
.collapsible
) {
361 if ( $li
.hasClass( "ui-tabs-selected" ) ) {
365 self
._cookie( o
.selected
, o
.cookie
);
368 self
.element
.queue( "tabs", function() {
369 hideTab( el
, $hide
);
370 }).dequeue( "tabs" );
374 } else if ( !$hide
.length
) {
376 self
._cookie( o
.selected
, o
.cookie
);
379 self
.element
.queue( "tabs", function() {
380 showTab( el
, $show
);
383 // TODO make passing in node possible, see also http://dev.jqueryui.com/ticket/3171
384 self
.load( self
.anchors
.index( this ) );
392 self
._cookie( o
.selected
, o
.cookie
);
396 if ( $show
.length
) {
397 if ( $hide
.length
) {
398 self
.element
.queue( "tabs", function() {
399 hideTab( el
, $hide
);
402 self
.element
.queue( "tabs", function() {
403 showTab( el
, $show
);
406 self
.load( self
.anchors
.index( this ) );
408 throw "jQuery UI Tabs: Mismatching fragment identifier.";
411 // Prevent IE from keeping other link focussed when using the back button
412 // and remove dotted border from clicked link. This is controlled via CSS
413 // in modern browsers; blur() removes focus from address bar in Firefox
414 // which can become a usability and annoying problem with tabs('rotate').
415 if ( $.browser
.msie
) {
420 // disable click in any case
421 this.anchors
.bind( "click.tabs", function(){
426 _getIndex: function( index
) {
427 // meta-function to give users option to provide a href string instead of a numerical index.
428 // also sanitizes numerical indexes to valid values.
429 if ( typeof index
== "string" ) {
430 index
= this.anchors
.index( this.anchors
.filter( "[href$='" + index
+ "']" ) );
436 destroy: function() {
437 var o
= this.options
;
443 .removeClass( "ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible" )
444 .removeData( "tabs" );
446 this.list
.removeClass( "ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all" );
448 this.anchors
.each(function() {
449 var href
= $.data( this, "href.tabs" );
453 var $this = $( this ).unbind( ".tabs" );
454 $.each( [ "href", "load", "cache" ], function( i
, prefix
) {
455 $this.removeData( prefix
+ ".tabs" );
459 this.lis
.unbind( ".tabs" ).add( this.panels
).each(function() {
460 if ( $.data( this, "destroy.tabs" ) ) {
463 $( this ).removeClass([
480 this._cookie( null, o
.cookie
);
486 add: function( url
, label
, index
) {
487 if ( index
=== undefined ) {
488 index
= this.anchors
.length
;
493 $li
= $( o
.tabTemplate
.replace( /#\{href\}/g, url
).replace( /#\{label\}/g, label
) ),
494 id
= !url
.indexOf( "#" ) ? url
.replace( "#", "" ) : this._tabId( $( "a", $li
)[ 0 ] );
496 $li
.addClass( "ui-state-default ui-corner-top" ).data( "destroy.tabs", true );
498 // try to find an existing element before creating a new one
499 var $panel
= self
.element
.find( "#" + id
);
500 if ( !$panel
.length
) {
501 $panel
= $( o
.panelTemplate
)
503 .data( "destroy.tabs", true );
505 $panel
.addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom ui-tabs-hide" );
507 if ( index
>= this.lis
.length
) {
508 $li
.appendTo( this.list
);
509 $panel
.appendTo( this.list
[ 0 ].parentNode
);
511 $li
.insertBefore( this.lis
[ index
] );
512 $panel
.insertBefore( this.panels
[ index
] );
515 o
.disabled
= $.map( o
.disabled
, function( n
, i
) {
516 return n
>= index
? ++n
: n
;
521 if ( this.anchors
.length
== 1 ) {
523 $li
.addClass( "ui-tabs-selected ui-state-active" );
524 $panel
.removeClass( "ui-tabs-hide" );
525 this.element
.queue( "tabs", function() {
526 self
._trigger( "show", null, self
._ui( self
.anchors
[ 0 ], self
.panels
[ 0 ] ) );
532 this._trigger( "add", null, this._ui( this.anchors
[ index
], this.panels
[ index
] ) );
536 remove: function( index
) {
537 index
= this._getIndex( index
);
538 var o
= this.options
,
539 $li
= this.lis
.eq( index
).remove(),
540 $panel
= this.panels
.eq( index
).remove();
542 // If selected tab was removed focus tab to the right or
543 // in case the last tab was removed the tab to the left.
544 if ( $li
.hasClass( "ui-tabs-selected" ) && this.anchors
.length
> 1) {
545 this.select( index
+ ( index
+ 1 < this.anchors
.length
? 1 : -1 ) );
549 $.grep( o
.disabled
, function(n
, i
) {
553 return n
>= index
? --n
: n
;
558 this._trigger( "remove", null, this._ui( $li
.find( "a" )[ 0 ], $panel
[ 0 ] ) );
562 enable: function( index
) {
563 index
= this._getIndex( index
);
564 var o
= this.options
;
565 if ( $.inArray( index
, o
.disabled
) == -1 ) {
569 this.lis
.eq( index
).removeClass( "ui-state-disabled" );
570 o
.disabled
= $.grep( o
.disabled
, function( n
, i
) {
574 this._trigger( "enable", null, this._ui( this.anchors
[ index
], this.panels
[ index
] ) );
578 disable: function( index
) {
579 index
= this._getIndex( index
);
580 var self
= this, o
= this.options
;
581 // cannot disable already selected tab
582 if ( index
!= o
.selected
) {
583 this.lis
.eq( index
).addClass( "ui-state-disabled" );
585 o
.disabled
.push( index
);
588 this._trigger( "disable", null, this._ui( this.anchors
[ index
], this.panels
[ index
] ) );
594 select: function( index
) {
595 index
= this._getIndex( index
);
597 if ( this.options
.collapsible
&& this.options
.selected
!= -1 ) {
598 index
= this.options
.selected
;
603 this.anchors
.eq( index
).trigger( this.options
.event
+ ".tabs" );
607 load: function( index
) {
608 index
= this._getIndex( index
);
611 a
= this.anchors
.eq( index
)[ 0 ],
612 url
= $.data( a
, "load.tabs" );
616 // not remote or from cache
617 if ( !url
|| this.element
.queue( "tabs" ).length
!== 0 && $.data( a
, "cache.tabs" ) ) {
618 this.element
.dequeue( "tabs" );
622 // load remote from here on
623 this.lis
.eq( index
).addClass( "ui-state-processing" );
626 var span
= $( "span", a
);
627 span
.data( "label.tabs", span
.html() ).html( o
.spinner
);
630 this.xhr
= $.ajax( $.extend( {}, o
.ajaxOptions
, {
632 success: function( r
, s
) {
633 self
.element
.find( self
._sanitizeSelector( a
.hash
) ).html( r
);
635 // take care of tab labels
639 $.data( a
, "cache.tabs", true );
642 self
._trigger( "load", null, self
._ui( self
.anchors
[ index
], self
.panels
[ index
] ) );
644 o
.ajaxOptions
.success( r
, s
);
648 error: function( xhr
, s
, e
) {
649 // take care of tab labels
652 self
._trigger( "load", null, self
._ui( self
.anchors
[ index
], self
.panels
[ index
] ) );
654 // Passing index avoid a race condition when this method is
655 // called after the user has selected another tab.
656 // Pass the anchor that initiated this request allows
657 // loadError to manipulate the tab content panel via $(a.hash)
658 o
.ajaxOptions
.error( xhr
, s
, index
, a
);
664 // last, so that load event is fired before show...
665 self
.element
.dequeue( "tabs" );
671 // stop possibly running animations
672 this.element
.queue( [] );
673 this.panels
.stop( false, true );
675 // "tabs" queue must not contain more than two elements,
676 // which are the callbacks for the latest clicked tab...
677 this.element
.queue( "tabs", this.element
.queue( "tabs" ).splice( -2, 2 ) );
679 // terminate pending requests from other tabs
685 // take care of tab labels
690 url: function( index
, url
) {
691 this.anchors
.eq( index
).removeData( "cache.tabs" ).data( "load.tabs", url
);
696 return this.anchors
.length
;
700 $.extend( $.ui
.tabs
, {
711 $.extend( $.ui
.tabs
.prototype, {
713 rotate: function( ms
, continuing
) {
717 var rotate
= self
._rotate
|| ( self
._rotate = function( e
) {
718 clearTimeout( self
.rotation
);
719 self
.rotation
= setTimeout(function() {
721 self
.select( ++t
< self
.anchors
.length
? t
: 0 );
729 var stop
= self
._unrotate
|| ( self
._unrotate
= !continuing
731 if (e
.clientX
) { // in case of a true click
741 this.element
.bind( "tabsshow", rotate
);
742 this.anchors
.bind( o
.event
+ ".tabs", stop
);
746 clearTimeout( self
.rotation
);
747 this.element
.unbind( "tabsshow", rotate
);
748 this.anchors
.unbind( o
.event
+ ".tabs", stop
);
750 delete this._unrotate
;