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;