2 * Tabs 3 - New Wave Tabs
4 * Copyright (c) 2007 Klaus Hartl (stilbuero.de)
5 * Dual licensed under the MIT (MIT-LICENSE.txt)
6 * and GPL (GPL-LICENSE.txt) licenses.
11 // if the UI scope is not availalable, add it
14 // tabs initialization
15 $.fn.tabs = function(initial, options) {
16 if (initial && initial.constructor == Object) { // shift arguments
20 options = options || {};
22 initial = initial && initial.constructor == Number && --initial || 0;
24 return this.each(function() {
25 new $.ui.tabs(this, $.extend(options, { initial: initial }));
29 // other chainable tabs methods
30 $.each(['Add', 'Remove', 'Enable', 'Disable', 'Click', 'Load'], function(i, method) {
31 $.fn['tabs' + method] = function() {
33 return this.each(function() {
34 var instance = $.ui.tabs.getInstance(this);
35 instance[method.toLowerCase()].apply(instance, args);
39 $.fn.tabsSelected = function() {
42 var instance = $.ui.tabs.getInstance(this[0]),
44 selected = $lis.index( $lis.filter('.' + instance.options.selectedClass)[0] );
46 return selected >= 0 ? ++selected : -1;
50 $.ui.tabs = function(el, options) {
54 this.options = $.extend({
60 // TODO bookmarkable: $.ajaxHistory ? true : false,
62 unselect: options.unselected ? true : false,
65 spinner: 'Loading…',
80 remove: function() {},
81 enable: function() {},
82 disable: function() {},
89 navClass: 'ui-tabs-nav',
90 selectedClass: 'ui-tabs-selected',
91 disabledClass: 'ui-tabs-disabled',
92 containerClass: 'ui-tabs-container',
93 hideClass: 'ui-tabs-hide',
94 loadingClass: 'ui-tabs-loading'
100 // save instance for later
101 var uuid = 'tabs' + $.ui.tabs.prototype.count++;
102 $.ui.tabs.instances[uuid] = this;
103 $.data(el, 'tabsUUID', uuid);
108 $.ui.tabs.instances = {};
109 $.ui.tabs.getInstance = function(el) {
110 return $.ui.tabs.instances[$.data(el, 'tabsUUID')];
114 $.extend($.ui.tabs.prototype, {
116 tabify: function(init) {
118 this.$tabs = $('a:first-child', this.source);
119 this.$containers = $([]);
121 var self = this, o = this.options;
123 this.$tabs.each(function(i, a) {
125 if (a.hash && a.hash.replace('#', '')) { // safari 2 reports '#' for an empty hash
126 self.$containers = self.$containers.add(a.hash);
130 $.data(a, 'href', a.href);
131 var id = a.title && a.title.replace(/\s/g, '_') || o.idPrefix + (self.count + 1) + '-' + (i + 1);
133 self.$containers = self.$containers.add(
134 $('#' + id)[0] || $('<div id="' + id + '" class="' + o.containerClass + '"></div>')
135 .insertAfter( self.$containers[i - 1] || self.source )
142 // Try to retrieve initial tab from fragment identifier in url if present,
143 // otherwise try to find selected class attribute on <li>.
144 this.$tabs.each(function(i, a) {
146 if (a.hash == location.hash) {
148 // prevent page scroll to fragment
149 //if (($.browser.msie || $.browser.opera) && !o.remote) {
150 if ($.browser.msie || $.browser.opera) {
151 var $toShow = $(location.hash), toShowId = $toShow.attr('id');
152 $toShow.attr('id', '');
153 setTimeout(function() {
154 $toShow.attr('id', toShowId); // restore id
158 return false; // break
160 } else if ( $(a).parents('li:eq(0)').is('li.' + o.selectedClass) ) {
162 return false; // break
166 // attach necessary classes for styling if not present
167 $(this.source).is('.' + o.navClass) || $(this.source).addClass(o.navClass);
168 this.$containers.each(function() {
170 $this.is('.' + o.containerClass) || $this.addClass(o.containerClass);
174 var $lis = $('li', this.source);
175 this.$containers.addClass(o.hideClass);
176 $lis.removeClass(o.selectedClass);
178 this.$containers.slice(o.initial, o.initial + 1).show();
179 $lis.slice(o.initial, o.initial + 1).addClass(o.selectedClass);
182 // load if remote tab
183 if ($.data(this.$tabs[o.initial], 'href')) {
184 this.load(o.initial + 1, $.data(this.$tabs[o.initial], 'href'));
186 $.removeData(this.$tabs[o.initial], 'href'); // if loaded once do not load them again
191 for (var i = 0, position; position = o.disabled[i]; i++) {
192 this.disable(position);
198 var showAnim = {}, showSpeed = o.fxShowSpeed || o.fxSpeed,
199 hideAnim = {}, hideSpeed = o.fxHideSpeed || o.fxSpeed;
200 if (o.fxSlide || o.fxFade) {
202 showAnim['height'] = 'show';
203 hideAnim['height'] = 'hide';
206 showAnim['opacity'] = 'show';
207 hideAnim['opacity'] = 'hide';
212 } else { // use some kind of animation to prevent browser scrolling to the tab
213 showAnim['min-width'] = 0; // avoid opacity, causes flicker in Firefox
214 showSpeed = 1; // as little as 1 is sufficient
218 } else { // use some kind of animation to prevent browser scrolling to the tab
219 hideAnim['min-width'] = 0; // avoid opacity, causes flicker in Firefox
220 hideSpeed = 1; // as little as 1 is sufficient
224 // reset some styles to maintain print style sheets etc.
225 var resetCSS = { display: '', overflow: '', height: '' };
226 if (!$.browser.msie) { // not in IE to prevent ClearType font issue
227 resetCSS['opacity'] = '';
230 // Hide a tab, animation prevents browser scrolling to fragment,
231 // $show is optional.
232 function hideTab(clicked, $hide, $show) {
233 $hide.animate(hideAnim, hideSpeed, function() { //
234 $hide.addClass(o.hideClass).css(resetCSS); // maintain flexible height and accessibility in print etc.
235 if ($.browser.msie) {
236 $hide[0].style.filter = '';
238 o.hide(clicked, $hide[0], $show && $show[0] || null);
240 showTab(clicked, $show, $hide);
245 // Show a tab, animation prevents browser scrolling to fragment,
247 function showTab(clicked, $show, $hide) {
248 if (!(o.fxSlide || o.fxFade || o.fxShow)) {
249 $show.css('display', 'block'); // prevent occasionally occuring flicker in Firefox cause by gap between showing and hiding the tab containers
251 $show.animate(showAnim, showSpeed, function() {
252 $show.removeClass(o.hideClass).css(resetCSS); // maintain flexible height and accessibility in print etc.
253 if ($.browser.msie) {
254 $show[0].style.filter = '';
256 o.show(clicked, $show[0], $hide && $hide[0] || null);
261 function switchTab(clicked, $hide, $show) {
262 /*if (o.bookmarkable && trueClick) { // add to history only if true click occured, not a triggered click
263 $.ajaxHistory.update(clicked.hash);
265 $(clicked).parents('li:eq(0)').addClass(o.selectedClass)
266 .siblings().removeClass(o.selectedClass);
267 hideTab(clicked, $hide, $show);
271 function tabClick(e) {
273 //var trueClick = e.clientX; // add to history only if true click occured, not a triggered click
274 var $li = $(this).parents('li:eq(0)'),
275 $hide = self.$containers.filter(':visible'),
276 $show = $(this.hash);
278 // If tab is already selected and not unselectable or tab disabled or click callback returns false stop here.
279 // Check if click handler returns false last so that it is not executed for a disabled tab!
280 if (($li.is('.' + o.selectedClass) && !o.unselect) || $li.is('.' + o.disabledClass)
281 || o.click(this, $show[0], $hide[0]) === false) {
286 // if tab may be closed
288 if ($li.is('.' + o.selectedClass)) {
289 $li.removeClass(o.selectedClass);
290 self.$containers.stop();
291 hideTab(this, $hide);
294 } else if (!$hide.length) {
295 $li.addClass(o.selectedClass);
296 self.$containers.stop();
297 showTab(this, $show);
303 // stop possibly running animations
304 self.$containers.stop();
309 // prevent scrollbar scrolling to 0 and than back in IE7, happens only if bookmarking/history is enabled
310 /*if ($.browser.msie && o.bookmarkable) {
311 var showId = this.hash.replace('#', '');
312 $show.attr('id', '');
313 setTimeout(function() {
314 $show.attr('id', showId); // restore id
318 if ($.data(this, 'href')) { // remote tab
320 self.load(self.$tabs.index(this) + 1, $.data(this, 'href'), function() {
321 switchTab(a, $hide, $show);
324 $.removeData(this, 'href'); // if loaded once do not load them again
327 switchTab(this, $hide, $show);
330 // Set scrollbar to saved position - need to use timeout with 0 to prevent browser scroll to target of hash
331 /*var scrollX = window.pageXOffset || document.documentElement && document.documentElement.scrollLeft || document.body.scrollLeft || 0;
332 var scrollY = window.pageYOffset || document.documentElement && document.documentElement.scrollTop || document.body.scrollTop || 0;
333 setTimeout(function() {
334 scrollTo(scrollX, scrollY);
338 throw 'jQuery UI Tabs: Mismatching fragment identifier.';
341 this.blur(); // prevent IE from keeping other link focussed when using the back button
343 //return o.bookmarkable && !!trueClick; // convert trueClick == undefined to Boolean required in IE
348 // attach click event, avoid duplicates from former tabifying
349 this.$tabs.unbind(o.event, tabClick).bind(o.event, tabClick);
352 add: function(url, text, position) {
354 var o = this.options;
355 position = position || this.$tabs.length; // append by default
356 if (position >= this.$tabs.length) {
357 var method = 'insertAfter';
358 position = this.$tabs.length;
360 var method = 'insertBefore';
362 if (url.indexOf('#') == 0) { // ajax container is created by tabify automatically
363 var $container = $(url);
364 // try to find an existing element before creating a new one
365 ($container.length && $container || $('<div id="' + url.replace('#', '') + '" class="' + o.containerClass + ' ' + o.hideClass + '"></div>'))
366 [method](this.$containers[position - 1]);
368 $('<li><a href="' + url + '"><span>' + text + '</span></a></li>')
369 [method](this.$tabs.slice(position - 1, position).parents('li:eq(0)'));
371 o.add(this.$tabs[position - 1], this.$containers[position - 1]); // callback
373 throw 'jQuery UI Tabs: Not enough arguments to add tab.';
376 remove: function(position) {
377 if (position && position.constructor == Number) {
378 var $removedTab = this.$tabs.slice(position - 1, position).parents('li:eq(0)').remove();
379 var $removedContainer = this.$containers.slice(position - 1, position).remove();
381 this.options.remove($removedTab[0], $removedContainer[0]); // callback
384 enable: function(position) {
385 var $li = this.$tabs.slice(position - 1, position).parents('li:eq(0)'), o = this.options;
386 $li.removeClass(o.disabledClass);
387 if ($.browser.safari) { // fix disappearing tab after enabling in Safari... TODO check Safari 3
388 $li.animate({ opacity: 1 }, 1, function() {
389 $li.css({ opacity: '' });
392 o.enable(this.$tabs[position - 1], this.$containers[position - 1]); // callback
394 disable: function(position) {
395 var $li = this.$tabs.slice(position - 1, position).parents('li:eq(0)'), o = this.options;
396 if ($.browser.safari) { // fix opacity of tab after disabling in Safari... TODO check Safari 3
397 $li.animate({ opacity: 0 }, 1, function() {
398 $li.css({ opacity: '' });
401 $li.addClass(this.options.disabledClass);
402 o.disable(this.$tabs[position - 1], this.$containers[position - 1]); // callback
404 click: function(position) {
405 this.$tabs.slice(position - 1, position).trigger('click');
407 load: function(position, url, callback) {
410 $a = this.$tabs.slice(position - 1, position).addClass(o.loadingClass),
411 $span = $('span', $a),
415 if (url && url.constructor == Function) {
421 $.data($a[0], 'href', url);
426 $span.html('<em>' + o.spinner + '</em>');
428 setTimeout(function() { // timeout is again required in IE, "wait" for id being restored
429 $($a[0].hash).load(url, function() {
433 $a.removeClass(o.loadingClass);
434 // This callback is required because the switch has to take place after loading
436 if (callback && callback.constructor == Function) {
439 o.load(self.$tabs[position - 1], self.$containers[position - 1]); // callback