2 * jQuery UI Tabs @VERSION
4 * Copyright (c) 2007, 2008 Klaus Hartl (stilbuero.de)
5 * Dual licensed under the MIT (MIT-LICENSE.txt)
6 * and GPL (GPL-LICENSE.txt) licenses.
8 * http://docs.jquery.com/UI/Tabs
20 _setData: function(key, value) {
21 if ((/^selected/).test(key))
24 this.options[key] = value;
29 return this.$tabs.length;
32 return a.title && a.title.replace(/\s/g, '_').replace(/[^A-Za-z0-9\-_:\.]/g, '')
33 || this.options.idPrefix + $.data(a);
35 ui: function(tab, panel) {
37 options: this.options,
40 index: this.$tabs.index(tab)
43 _sanitizeSelector: function(hash) {
44 return hash.replace(/:/g, '\\:'); // we need this because an id may contain a ":"
47 var cookie = this.cookie || (this.cookie = 'ui-tabs-' + $.data(this.element[0]));
48 return $.cookie.apply(null, [cookie].concat($.makeArray(arguments)));
50 _tabify: function(init) {
52 this.$lis = $('li:has(a[href])', this.element);
53 this.$tabs = this.$lis.map(function() { return $('a', this)[0]; });
56 var self = this, o = this.options;
58 this.$tabs.each(function(i, a) {
60 if (a.hash && a.hash.replace('#', '')) // Safari 2 reports '#' for an empty hash
61 self.$panels = self.$panels.add(self._sanitizeSelector(a.hash));
63 else if ($(a).attr('href') != '#') { // prevent loading the page itself if href is just "#"
64 $.data(a, 'href.tabs', a.href); // required for restore on destroy
65 $.data(a, 'load.tabs', a.href); // mutable
66 var id = self._tabId(a);
68 var $panel = $('#' + id);
70 $panel = $(o.panelTemplate).attr('id', id).addClass(o.panelClass)
71 .insertAfter(self.$panels[i - 1] || self.element);
72 $panel.data('destroy.tabs', true);
74 self.$panels = self.$panels.add($panel);
78 o.disabled.push(i + 1);
81 // initialization from scratch
84 // attach necessary classes for styling if not present
85 this.element.addClass(o.navClass);
86 this.$panels.addClass(o.panelClass);
89 // use "selected" option or try to retrieve:
90 // 1. from fragment identifier in url
92 // 3. from selected class attribute on <li>
93 if (o.selected === undefined) {
95 this.$tabs.each(function(i, a) {
96 if (a.hash == location.hash) {
98 return false; // break
103 var index = parseInt(self._cookie(), 10);
104 if (index && self.$tabs[index]) o.selected = index;
106 else if (self.$lis.filter('.' + o.selectedClass).length)
107 o.selected = self.$lis.index( self.$lis.filter('.' + o.selectedClass)[0] );
109 o.selected = o.selected === null || o.selected !== undefined ? o.selected : 0; // first tab selected by default
111 // Take disabling tabs via class attribute from HTML
112 // into account and update option properly.
113 // A selected tab cannot become disabled.
114 o.disabled = $.unique(o.disabled.concat(
115 $.map(this.$lis.filter('.' + o.disabledClass),
116 function(n, i) { return self.$lis.index(n); } )
118 if ($.inArray(o.selected, o.disabled) != -1)
119 o.disabled.splice($.inArray(o.selected, o.disabled), 1);
121 // highlight selected tab
122 this.$panels.addClass(o.hideClass);
123 this.$lis.removeClass(o.selectedClass);
124 if (o.selected !== null) {
125 this.$panels.eq(o.selected).removeClass(o.hideClass);
126 var classes = [o.selectedClass];
127 if (o.deselectable) classes.push(o.deselectableClass);
128 this.$lis.eq(o.selected).addClass(classes.join(' '));
130 // seems to be expected behavior that the show callback is fired
131 var onShow = function() {
132 self._trigger('show', null,
133 self.ui(self.$tabs[o.selected], self.$panels[o.selected]));
136 // load if remote tab
137 if ($.data(this.$tabs[o.selected], 'load.tabs'))
138 this.load(o.selected, onShow);
139 // just trigger show event
143 // clean up to avoid memory leaks in certain versions of IE 6
144 $(window).bind('unload', function() {
145 self.$tabs.unbind('.tabs');
146 self.$lis = self.$tabs = self.$panels = null;
150 // update selected after add/remove
152 o.selected = this.$lis.index( this.$lis.filter('.' + o.selectedClass)[0] );
154 // set or update cookie after init and add/remove respectively
155 if (o.cookie) this._cookie(o.selected, o.cookie);
158 for (var i = 0, li; li = this.$lis[i]; i++)
159 $(li)[$.inArray(i, o.disabled) != -1 && !$(li).hasClass(o.selectedClass) ? 'addClass' : 'removeClass'](o.disabledClass);
161 // reset cache if switching from cached to not cached
162 if (o.cache === false) this.$tabs.removeData('cache.tabs');
167 if (o.fx.constructor == Array) {
171 else hideFx = showFx = o.fx;
174 // Reset certain styles left over from animation
175 // and prevent IE's ClearType bug...
176 function resetStyle($el, fx) {
177 $el.css({ display: '' });
178 if ($.browser.msie && fx.opacity) $el[0].style.removeAttribute('filter');
182 var showTab = showFx ?
183 function(clicked, $show) {
184 $show.animate(showFx, showFx.duration || 'normal', function() {
185 $show.removeClass(o.hideClass);
186 resetStyle($show, showFx);
187 self._trigger('show', null, self.ui(clicked, $show[0]));
190 function(clicked, $show) {
191 $show.removeClass(o.hideClass);
192 self._trigger('show', null, self.ui(clicked, $show[0]));
195 // Hide a tab, $show is optional...
196 var hideTab = hideFx ?
197 function(clicked, $hide, $show) {
198 $hide.animate(hideFx, hideFx.duration || 'normal', function() {
199 $hide.addClass(o.hideClass);
200 resetStyle($hide, hideFx);
201 if ($show) showTab(clicked, $show, $hide);
204 function(clicked, $hide, $show) {
205 $hide.addClass(o.hideClass);
206 if ($show) showTab(clicked, $show);
210 function switchTab(clicked, $li, $hide, $show) {
211 var classes = [o.selectedClass];
212 if (o.deselectable) classes.push(o.deselectableClass);
213 $li.addClass(classes.join(' ')).siblings().removeClass(classes.join(' '));
214 hideTab(clicked, $hide, $show);
217 // attach tab event handler, unbind to avoid duplicates from former tabifying...
218 this.$tabs.unbind('.tabs').bind(o.event + '.tabs', function() {
220 //var trueClick = e.clientX; // add to history only if true click occured, not a triggered click
221 var $li = $(this).parents('li:eq(0)'),
222 $hide = self.$panels.filter(':visible'),
223 $show = $(self._sanitizeSelector(this.hash));
225 // If tab is already selected and not deselectable or tab disabled or
226 // or is already loading or click callback returns false stop here.
227 // Check if click handler returns false last so that it is not executed
228 // for a disabled or loading tab!
229 if (($li.hasClass(o.selectedClass) && !o.deselectable)
230 || $li.hasClass(o.disabledClass)
231 || $(this).hasClass(o.loadingClass)
232 || self._trigger('select', null, self.ui(this, $show[0])) === false
238 o.selected = self.$tabs.index(this);
240 // if tab may be closed
241 if (o.deselectable) {
242 if ($li.hasClass(o.selectedClass)) {
243 self.options.selected = null;
244 $li.removeClass([o.selectedClass, o.deselectableClass].join(' '));
246 hideTab(this, $hide);
249 } else if (!$hide.length) {
252 self.load(self.$tabs.index(this), function() {
253 $li.addClass([o.selectedClass, o.deselectableClass].join(' '));
261 if (o.cookie) self._cookie(o.selected, o.cookie);
263 // stop possibly running animations
269 self.load(self.$tabs.index(this), $hide.length ?
271 switchTab(a, $li, $hide, $show);
274 $li.addClass(o.selectedClass);
279 throw 'jQuery UI Tabs: Mismatching fragment identifier.';
281 // Prevent IE from keeping other link focussed when using the back button
282 // and remove dotted border from clicked link. This is controlled via CSS
283 // in modern browsers; blur() removes focus from address bar in Firefox
284 // which can become a usability and annoying problem with tabs('rotate').
285 if ($.browser.msie) this.blur();
291 // disable click if event is configured to something else
292 if (o.event != 'click') this.$tabs.bind('click.tabs', function(){return false;});
295 add: function(url, label, index) {
296 if (index == undefined)
297 index = this.$tabs.length; // append by default
299 var o = this.options;
300 var $li = $(o.tabTemplate.replace(/#\{href\}/g, url).replace(/#\{label\}/g, label));
301 $li.data('destroy.tabs', true);
303 var id = url.indexOf('#') == 0 ? url.replace('#', '') : this._tabId( $('a:first-child', $li)[0] );
305 // try to find an existing element before creating a new one
306 var $panel = $('#' + id);
307 if (!$panel.length) {
308 $panel = $(o.panelTemplate).attr('id', id)
309 .addClass(o.hideClass)
310 .data('destroy.tabs', true);
312 $panel.addClass(o.panelClass);
313 if (index >= this.$lis.length) {
314 $li.appendTo(this.element);
315 $panel.appendTo(this.element[0].parentNode);
317 $li.insertBefore(this.$lis[index]);
318 $panel.insertBefore(this.$panels[index]);
321 o.disabled = $.map(o.disabled,
322 function(n, i) { return n >= index ? ++n : n });
326 if (this.$tabs.length == 1) {
327 $li.addClass(o.selectedClass);
328 $panel.removeClass(o.hideClass);
329 var href = $.data(this.$tabs[0], 'load.tabs');
331 this.load(index, href);
335 this._trigger('add', null, this.ui(this.$tabs[index], this.$panels[index]));
337 remove: function(index) {
338 var o = this.options, $li = this.$lis.eq(index).remove(),
339 $panel = this.$panels.eq(index).remove();
341 // If selected tab was removed focus tab to the right or
342 // in case the last tab was removed the tab to the left.
343 if ($li.hasClass(o.selectedClass) && this.$tabs.length > 1)
344 this.select(index + (index + 1 < this.$tabs.length ? 1 : -1));
346 o.disabled = $.map($.grep(o.disabled, function(n, i) { return n != index; }),
347 function(n, i) { return n >= index ? --n : n });
352 this._trigger('remove', null, this.ui($li.find('a')[0], $panel[0]));
354 enable: function(index) {
355 var o = this.options;
356 if ($.inArray(index, o.disabled) == -1)
359 var $li = this.$lis.eq(index).removeClass(o.disabledClass);
360 if ($.browser.safari) { // fix disappearing tab (that used opacity indicating disabling) after enabling in Safari 2...
361 $li.css('display', 'inline-block');
362 setTimeout(function() {
363 $li.css('display', 'block');
367 o.disabled = $.grep(o.disabled, function(n, i) { return n != index; });
370 this._trigger('enable', null, this.ui(this.$tabs[index], this.$panels[index]));
372 disable: function(index) {
373 var self = this, o = this.options;
374 if (index != o.selected) { // cannot disable already selected tab
375 this.$lis.eq(index).addClass(o.disabledClass);
377 o.disabled.push(index);
381 this._trigger('disable', null, this.ui(this.$tabs[index], this.$panels[index]));
384 select: function(index) {
385 // TODO make null as argument work
386 if (typeof index == 'string')
387 index = this.$tabs.index( this.$tabs.filter('[href$=' + index + ']')[0] );
388 this.$tabs.eq(index).trigger(this.options.event + '.tabs');
390 load: function(index, callback) { // callback is for internal usage only
392 var self = this, o = this.options, $a = this.$tabs.eq(index), a = $a[0],
393 bypassCache = callback == undefined || callback === false, url = $a.data('load.tabs');
395 callback = callback || function() {};
397 // no remote or from cache - just finish with callback
398 if (!url || !bypassCache && $.data(a, 'cache.tabs')) {
403 // load remote from here on
405 var inner = function(parent) {
406 var $parent = $(parent), $inner = $parent.find('*:last');
407 return $inner.length && $inner.is(':not(img)') && $inner || $parent;
409 var cleanup = function() {
410 self.$tabs.filter('.' + o.loadingClass).removeClass(o.loadingClass)
413 inner(this).parent().html(inner(this).data('label.tabs'));
419 var label = inner(a).html();
420 inner(a).wrapInner('<em></em>')
421 .find('em').data('label.tabs', label).html(o.spinner);
424 var ajaxOptions = $.extend({}, o.ajaxOptions, {
426 success: function(r, s) {
427 $(self._sanitizeSelector(a.hash)).html(r);
431 $.data(a, 'cache.tabs', true); // if loaded once do not load them again
434 self._trigger('load', null, self.ui(self.$tabs[index], self.$panels[index]));
436 o.ajaxOptions.success(r, s);
440 // This callback is required because the switch has to take
441 // place after loading has completed. Call last in order to
442 // fire load before show callback...
447 // terminate pending requests from other tabs and restore tab label
451 $a.addClass(o.loadingClass);
452 self.xhr = $.ajax(ajaxOptions);
454 url: function(index, url) {
455 this.$tabs.eq(index).removeData('cache.tabs').data('load.tabs', url);
457 destroy: function() {
458 var o = this.options;
459 this.element.unbind('.tabs')
460 .removeClass(o.navClass).removeData('tabs');
461 this.$tabs.each(function() {
462 var href = $.data(this, 'href.tabs');
465 var $this = $(this).unbind('.tabs');
466 $.each(['href', 'load', 'cache'], function(i, prefix) {
467 $this.removeData(prefix + '.tabs');
470 this.$lis.add(this.$panels).each(function() {
471 if ($.data(this, 'destroy.tabs'))
474 $(this).removeClass([o.selectedClass, o.deselectableClass,
475 o.disabledClass, o.panelClass, o.hideClass].join(' '));
478 this._cookie(null, o.cookie);
482 $.extend($.ui.tabs, {
490 cookie: null, // e.g. { expires: 7, path: '/', domain: 'jquery.com', secure: true }
492 spinner: 'Loading…',
494 idPrefix: 'ui-tabs-',
497 fx: null, // e.g. { height: 'toggle', opacity: 'toggle', duration: 200 }
499 tabTemplate: '<li><a href="#{href}"><span>#{label}</span></a></li>',
500 panelTemplate: '<div></div>',
502 navClass: 'ui-tabs-nav',
503 selectedClass: 'ui-tabs-selected',
504 deselectableClass: 'ui-tabs-deselectable',
505 disabledClass: 'ui-tabs-disabled',
506 panelClass: 'ui-tabs-panel',
507 hideClass: 'ui-tabs-hide',
508 loadingClass: 'ui-tabs-loading'
519 $.extend($.ui.tabs.prototype, {
521 rotate: function(ms, continuing) {
523 continuing = continuing || false;
525 var self = this, t = this.options.selected;
528 self.rotation = setInterval(function() {
529 t = ++t < self.$tabs.length ? t : 0;
535 if (!e || e.clientX) { // only in case of a true click
536 clearInterval(self.rotation);
544 this.$tabs.bind(this.options.event + '.tabs', stop);
546 this.$tabs.bind(this.options.event + '.tabs', function() {
548 t = self.options.selected;
555 this.$tabs.unbind(this.options.event + '.tabs', stop);