4 * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
5 * Dual licensed under the MIT (MIT-LICENSE.txt)
6 * and GPL (GPL-LICENSE.txt) licenses.
8 * http://docs.jquery.com/UI/Tabs
18 if (this.options
.deselectable
!== undefined) {
19 this.options
.collapsible
= this.options
.deselectable
;
24 _setData: function(key
, value
) {
25 if (key
== 'selected') {
26 if (this.options
.collapsible
&& value
== this.options
.selected
) {
32 this.options
[key
] = value
;
33 if (key
== 'deselectable') {
34 this.options
.collapsible
= value
;
41 return a
.title
&& a
.title
.replace(/\s/g, '_').replace(/[^A-Za-z0-9\-_:\.]/g, '') ||
42 this.options
.idPrefix
+ $.data(a
);
45 _sanitizeSelector: function(hash
) {
46 return hash
.replace(/:/g, '\\:'); // we need
this because an id may contain a
":"
50 var cookie
= this.cookie
|| (this.cookie
= this.options
.cookie
.name
|| 'ui-tabs-' + $.data(this.list
[0]));
51 return $.cookie
.apply(null, [cookie
].concat($.makeArray(arguments
)));
54 _ui: function(tab
, panel
) {
58 index
: this.anchors
.index(tab
)
62 _cleanup: function() {
63 // restore all former loading tabs labels
64 this.lis
.filter('.ui-state-processing').removeClass('ui-state-processing')
65 .find('span:data(label.tabs)')
68 el
.html(el
.data('label.tabs')).removeData('label.tabs');
72 _tabify: function(init
) {
74 this.list
= this.element
.children('ul:first');
75 this.lis
= $('li:has(a[href])', this.list
);
76 this.anchors
= this.lis
.map(function() { return $('a', this)[0]; });
79 var self
= this, o
= this.options
;
81 var fragmentId
= /^#.+/; // Safari 2 reports '#' for an empty hash
82 this.anchors
.each(function(i
, a
) {
83 var href
= $(a
).attr('href');
85 // For dynamically created HTML that contains a hash as href IE < 8 expands
86 // such href to the full page url with hash and then misinterprets tab as ajax.
87 // Same consideration applies for an added tab with a fragment identifier
88 // since a[href=#fragment-identifier] does unexpectedly not match.
89 // Thus normalize href attribute...
90 var hrefBase
= href
.split('#')[0], baseEl
;
91 if (hrefBase
&& (hrefBase
=== location
.toString().split('#')[0] ||
92 (baseEl
= $('base')[0]) && hrefBase
=== baseEl
.href
)) {
98 if (fragmentId
.test(href
)) {
99 self
.panels
= self
.panels
.add(self
._sanitizeSelector(href
));
103 else if (href
!= '#') { // prevent loading the page itself if href is just "#"
104 $.data(a
, 'href.tabs', href
); // required for restore on destroy
106 // TODO until #3808 is fixed strip fragment identifier from url
107 // (IE fails to load from such url)
108 $.data(a
, 'load.tabs', href
.replace(/#.*$/, '')); // mutable data
110 var id
= self
._tabId(a
);
112 var $panel
= $('#' + id
);
113 if (!$panel
.length
) {
114 $panel
= $(o
.panelTemplate
).attr('id', id
).addClass('ui-tabs-panel ui-widget-content ui-corner-bottom')
115 .insertAfter(self
.panels
[i
- 1] || self
.list
);
116 $panel
.data('destroy.tabs', true);
118 self
.panels
= self
.panels
.add($panel
);
127 // initialization from scratch
130 // attach necessary classes for styling
131 this.element
.addClass('ui-tabs ui-widget ui-widget-content ui-corner-all');
132 this.list
.addClass('ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all');
133 this.lis
.addClass('ui-state-default ui-corner-top');
134 this.panels
.addClass('ui-tabs-panel ui-widget-content ui-corner-bottom');
137 // use "selected" option or try to retrieve:
138 // 1. from fragment identifier in url
140 // 3. from selected class attribute on <li>
141 if (o
.selected
=== undefined) {
143 this.anchors
.each(function(i
, a
) {
144 if (a
.hash
== location
.hash
) {
146 return false; // break
150 if (typeof o
.selected
!= 'number' && o
.cookie
) {
151 o
.selected
= parseInt(self
._cookie(), 10);
153 if (typeof o
.selected
!= 'number' && this.lis
.filter('.ui-tabs-selected').length
) {
154 o
.selected
= this.lis
.index(this.lis
.filter('.ui-tabs-selected'));
156 o
.selected
= o
.selected
|| 0;
158 else if (o
.selected
=== null) { // usage of null is deprecated, TODO remove in next release
162 // sanity check - default to first tab...
163 o
.selected
= ((o
.selected
>= 0 && this.anchors
[o
.selected
]) || o
.selected
< 0) ? o
.selected
: 0;
165 // Take disabling tabs via class attribute from HTML
166 // into account and update option properly.
167 // A selected tab cannot become disabled.
168 o
.disabled
= $.unique(o
.disabled
.concat(
169 $.map(this.lis
.filter('.ui-state-disabled'),
170 function(n
, i
) { return self
.lis
.index(n
); } )
173 if ($.inArray(o
.selected
, o
.disabled
) != -1) {
174 o
.disabled
.splice($.inArray(o
.selected
, o
.disabled
), 1);
177 // highlight selected tab
178 this.panels
.addClass('ui-tabs-hide');
179 this.lis
.removeClass('ui-tabs-selected ui-state-active');
180 if (o
.selected
>= 0 && this.anchors
.length
) { // check for length avoids error when initializing empty list
181 this.panels
.eq(o
.selected
).removeClass('ui-tabs-hide');
182 this.lis
.eq(o
.selected
).addClass('ui-tabs-selected ui-state-active');
184 // seems to be expected behavior that the show callback is fired
185 self
.element
.queue("tabs", function() {
186 self
._trigger('show', null, self
._ui(self
.anchors
[o
.selected
], self
.panels
[o
.selected
]));
189 this.load(o
.selected
);
192 // clean up to avoid memory leaks in certain versions of IE 6
193 $(window
).bind('unload', function() {
194 self
.lis
.add(self
.anchors
).unbind('.tabs');
195 self
.lis
= self
.anchors
= self
.panels
= null;
199 // update selected after add/remove
201 o
.selected
= this.lis
.index(this.lis
.filter('.ui-tabs-selected'));
204 // update collapsible
205 this.element
[o
.collapsible
? 'addClass' : 'removeClass']('ui-tabs-collapsible');
207 // set or update cookie after init and add/remove respectively
209 this._cookie(o
.selected
, o
.cookie
);
213 for (var i
= 0, li
; (li
= this.lis
[i
]); i
++) {
214 $(li
)[$.inArray(i
, o
.disabled
) != -1 &&
215 !$(li
).hasClass('ui-tabs-selected') ? 'addClass' : 'removeClass']('ui-state-disabled');
218 // reset cache if switching from cached to not cached
219 if (o
.cache
=== false) {
220 this.anchors
.removeData('cache.tabs');
223 // remove all handlers before, tabify may run on existing tabs after add or option change
224 this.lis
.add(this.anchors
).unbind('.tabs');
226 if (o
.event
!= 'mouseover') {
227 var addState = function(state
, el
) {
228 if (el
.is(':not(.ui-state-disabled)')) {
229 el
.addClass('ui-state-' + state
);
232 var removeState = function(state
, el
) {
233 el
.removeClass('ui-state-' + state
);
235 this.lis
.bind('mouseover.tabs', function() {
236 addState('hover', $(this));
238 this.lis
.bind('mouseout.tabs', function() {
239 removeState('hover', $(this));
241 this.anchors
.bind('focus.tabs', function() {
242 addState('focus', $(this).closest('li'));
244 this.anchors
.bind('blur.tabs', function() {
245 removeState('focus', $(this).closest('li'));
252 if ($.isArray(o
.fx
)) {
257 hideFx
= showFx
= o
.fx
;
261 // Reset certain styles left over from animation
262 // and prevent IE's ClearType bug...
263 function resetStyle($el
, fx
) {
264 $el
.css({ display
: '' });
265 if ($.browser
.msie
&& fx
.opacity
) {
266 $el
[0].style
.removeAttribute('filter');
271 var showTab
= showFx
?
272 function(clicked
, $show
) {
273 $(clicked
).closest('li').removeClass('ui-state-default').addClass('ui-tabs-selected ui-state-active');
274 $show
.hide().removeClass('ui-tabs-hide') // avoid flicker that way
275 .animate(showFx
, showFx
.duration
|| 'normal', function() {
276 resetStyle($show
, showFx
);
277 self
._trigger('show', null, self
._ui(clicked
, $show
[0]));
280 function(clicked
, $show
) {
281 $(clicked
).closest('li').removeClass('ui-state-default').addClass('ui-tabs-selected ui-state-active');
282 $show
.removeClass('ui-tabs-hide');
283 self
._trigger('show', null, self
._ui(clicked
, $show
[0]));
286 // Hide a tab, $show is optional...
287 var hideTab
= hideFx
?
288 function(clicked
, $hide
) {
289 $hide
.animate(hideFx
, hideFx
.duration
|| 'normal', function() {
290 self
.lis
.removeClass('ui-tabs-selected ui-state-active').addClass('ui-state-default');
291 $hide
.addClass('ui-tabs-hide');
292 resetStyle($hide
, hideFx
);
293 self
.element
.dequeue("tabs");
296 function(clicked
, $hide
, $show
) {
297 self
.lis
.removeClass('ui-tabs-selected ui-state-active').addClass('ui-state-default');
298 $hide
.addClass('ui-tabs-hide');
299 self
.element
.dequeue("tabs");
302 // attach tab event handler, unbind to avoid duplicates from former tabifying...
303 this.anchors
.bind(o
.event
+ '.tabs', function() {
304 var el
= this, $li
= $(this).closest('li'), $hide
= self
.panels
.filter(':not(.ui-tabs-hide)'),
305 $show
= $(self
._sanitizeSelector(this.hash
));
307 // If tab is already selected and not collapsible or tab disabled or
308 // or is already loading or click callback returns false stop here.
309 // Check if click handler returns false last so that it is not executed
310 // for a disabled or loading tab!
311 if (($li
.hasClass('ui-tabs-selected') && !o
.collapsible
) ||
312 $li
.hasClass('ui-state-disabled') ||
313 $li
.hasClass('ui-state-processing') ||
314 self
._trigger('select', null, self
._ui(this, $show
[0])) === false) {
319 o
.selected
= self
.anchors
.index(this);
323 // if tab may be closed
325 if ($li
.hasClass('ui-tabs-selected')) {
329 self
._cookie(o
.selected
, o
.cookie
);
332 self
.element
.queue("tabs", function() {
339 else if (!$hide
.length
) {
341 self
._cookie(o
.selected
, o
.cookie
);
344 self
.element
.queue("tabs", function() {
348 self
.load(self
.anchors
.index(this)); // TODO make passing in node possible, see also http://dev.jqueryui.com/ticket/3171
356 self
._cookie(o
.selected
, o
.cookie
);
362 self
.element
.queue("tabs", function() {
366 self
.element
.queue("tabs", function() {
370 self
.load(self
.anchors
.index(this));
373 throw 'jQuery UI Tabs: Mismatching fragment identifier.';
376 // Prevent IE from keeping other link focussed when using the back button
377 // and remove dotted border from clicked link. This is controlled via CSS
378 // in modern browsers; blur() removes focus from address bar in Firefox
379 // which can become a usability and annoying problem with tabs('rotate').
380 if ($.browser
.msie
) {
386 // disable click in any case
387 this.anchors
.bind('click.tabs', function(){return false;});
391 destroy: function() {
392 var o
= this.options
;
396 this.element
.unbind('.tabs')
397 .removeClass('ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible')
400 this.list
.removeClass('ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all');
402 this.anchors
.each(function() {
403 var href
= $.data(this, 'href.tabs');
407 var $this = $(this).unbind('.tabs');
408 $.each(['href', 'load', 'cache'], function(i
, prefix
) {
409 $this.removeData(prefix
+ '.tabs');
413 this.lis
.unbind('.tabs').add(this.panels
).each(function() {
414 if ($.data(this, 'destroy.tabs')) {
418 $(this).removeClass([
435 this._cookie(null, o
.cookie
);
439 add: function(url
, label
, index
) {
440 if (index
=== undefined) {
441 index
= this.anchors
.length
; // append by default
444 var self
= this, o
= this.options
,
445 $li
= $(o
.tabTemplate
.replace(/#\{href\}/g, url
).replace(/#\{label\}/g, label
)),
446 id
= !url
.indexOf('#') ? url
.replace('#', '') : this._tabId($('a', $li
)[0]);
448 $li
.addClass('ui-state-default ui-corner-top').data('destroy.tabs', true);
450 // try to find an existing element before creating a new one
451 var $panel
= $('#' + id
);
452 if (!$panel
.length
) {
453 $panel
= $(o
.panelTemplate
).attr('id', id
).data('destroy.tabs', true);
455 $panel
.addClass('ui-tabs-panel ui-widget-content ui-corner-bottom ui-tabs-hide');
457 if (index
>= this.lis
.length
) {
458 $li
.appendTo(this.list
);
459 $panel
.appendTo(this.list
[0].parentNode
);
462 $li
.insertBefore(this.lis
[index
]);
463 $panel
.insertBefore(this.panels
[index
]);
466 o
.disabled
= $.map(o
.disabled
,
467 function(n
, i
) { return n
>= index
? ++n
: n
; });
471 if (this.anchors
.length
== 1) { // after tabify
472 $li
.addClass('ui-tabs-selected ui-state-active');
473 $panel
.removeClass('ui-tabs-hide');
474 this.element
.queue("tabs", function() {
475 self
._trigger('show', null, self
._ui(self
.anchors
[0], self
.panels
[0]));
482 this._trigger('add', null, this._ui(this.anchors
[index
], this.panels
[index
]));
485 remove: function(index
) {
486 var o
= this.options
, $li
= this.lis
.eq(index
).remove(),
487 $panel
= this.panels
.eq(index
).remove();
489 // If selected tab was removed focus tab to the right or
490 // in case the last tab was removed the tab to the left.
491 if ($li
.hasClass('ui-tabs-selected') && this.anchors
.length
> 1) {
492 this.select(index
+ (index
+ 1 < this.anchors
.length
? 1 : -1));
495 o
.disabled
= $.map($.grep(o
.disabled
, function(n
, i
) { return n
!= index
; }),
496 function(n
, i
) { return n
>= index
? --n
: n
; });
501 this._trigger('remove', null, this._ui($li
.find('a')[0], $panel
[0]));
504 enable: function(index
) {
505 var o
= this.options
;
506 if ($.inArray(index
, o
.disabled
) == -1) {
510 this.lis
.eq(index
).removeClass('ui-state-disabled');
511 o
.disabled
= $.grep(o
.disabled
, function(n
, i
) { return n
!= index
; });
514 this._trigger('enable', null, this._ui(this.anchors
[index
], this.panels
[index
]));
517 disable: function(index
) {
518 var self
= this, o
= this.options
;
519 if (index
!= o
.selected
) { // cannot disable already selected tab
520 this.lis
.eq(index
).addClass('ui-state-disabled');
522 o
.disabled
.push(index
);
526 this._trigger('disable', null, this._ui(this.anchors
[index
], this.panels
[index
]));
530 select: function(index
) {
531 if (typeof index
== 'string') {
532 index
= this.anchors
.index(this.anchors
.filter('[href$=' + index
+ ']'));
534 else if (index
=== null) { // usage of null is deprecated, TODO remove in next release
537 if (index
== -1 && this.options
.collapsible
) {
538 index
= this.options
.selected
;
541 this.anchors
.eq(index
).trigger(this.options
.event
+ '.tabs');
544 load: function(index
) {
545 var self
= this, o
= this.options
, a
= this.anchors
.eq(index
)[0], url
= $.data(a
, 'load.tabs');
549 // not remote or from cache
550 if (!url
|| this.element
.queue("tabs").length
!== 0 && $.data(a
, 'cache.tabs')) {
551 this.element
.dequeue("tabs");
555 // load remote from here on
556 this.lis
.eq(index
).addClass('ui-state-processing');
559 var span
= $('span', a
);
560 span
.data('label.tabs', span
.html()).html(o
.spinner
);
563 this.xhr
= $.ajax($.extend({}, o
.ajaxOptions
, {
565 success: function(r
, s
) {
566 $(self
._sanitizeSelector(a
.hash
)).html(r
);
568 // take care of tab labels
572 $.data(a
, 'cache.tabs', true); // if loaded once do not load them again
576 self
._trigger('load', null, self
._ui(self
.anchors
[index
], self
.panels
[index
]));
578 o
.ajaxOptions
.success(r
, s
);
582 // last, so that load event is fired before show...
583 self
.element
.dequeue("tabs");
589 // stop possibly running animations
590 this.element
.queue([]);
591 this.panels
.stop(false, true);
593 // terminate pending requests from other tabs
599 // take care of tab labels
604 url: function(index
, url
) {
605 this.anchors
.eq(index
).removeData('cache.tabs').data('load.tabs', url
);
609 return this.anchors
.length
;
614 $.extend($.ui
.tabs
, {
620 cookie
: null, // e.g. { expires: 7, path: '/', domain: 'jquery.com', secure: true }
624 fx
: null, // e.g. { height: 'toggle', opacity: 'toggle', duration: 200 }
625 idPrefix
: 'ui-tabs-',
626 panelTemplate
: '<div></div>',
627 spinner
: '<em>Loading…</em>',
628 tabTemplate
: '<li><a href="#{href}"><span>#{label}</span></a></li>'
639 $.extend($.ui
.tabs
.prototype, {
641 rotate: function(ms
, continuing
) {
643 var self
= this, o
= this.options
;
645 var rotate
= self
._rotate
|| (self
._rotate = function(e
) {
646 clearTimeout(self
.rotation
);
647 self
.rotation
= setTimeout(function() {
649 self
.select( ++t
< self
.anchors
.length
? t
: 0 );
657 var stop
= self
._unrotate
|| (self
._unrotate
= !continuing
?
659 if (e
.clientX
) { // in case of a true click
670 this.element
.bind('tabsshow', rotate
);
671 this.anchors
.bind(o
.event
+ '.tabs', stop
);
676 clearTimeout(self
.rotation
);
677 this.element
.unbind('tabsshow', rotate
);
678 this.anchors
.unbind(o
.event
+ '.tabs', stop
);
680 delete this._unrotate
;