/* * jQuery UI Tabs @VERSION * * Copyright (c) 2009 AUTHORS.txt (http://ui.jquery.com/about) * Dual licensed under the MIT (MIT-LICENSE.txt) * and GPL (GPL-LICENSE.txt) licenses. * * http://docs.jquery.com/UI/Tabs * * Depends: * ui.core.js */ (function($) { $.widget("ui.tabs", { _init: function() { // create tabs this._tabify(true); }, destroy: function() { var o = this.options; this.element.unbind('.tabs') .removeClass(o.navClass).removeData('tabs'); this.$tabs.each(function() { var href = $.data(this, 'href.tabs'); if (href) this.href = href; var $this = $(this).unbind('.tabs'); $.each(['href', 'load', 'cache'], function(i, prefix) { $this.removeData(prefix + '.tabs'); }); }); this.$lis.unbind('.tabs').add(this.$panels).each(function() { if ($.data(this, 'destroy.tabs')) $(this).remove(); else $(this).removeClass([o.tabClass, o.selectedClass, o.deselectableClass, o.disabledClass, o.panelClass, o.hideClass].join(' ')); }); if (o.cookie) this._cookie(null, o.cookie); }, _setData: function(key, value) { if ((/^selected/).test(key)) this.select(value); else { this.options[key] = value; this._tabify(); } }, length: function() { return this.$tabs.length; }, _tabId: function(a) { return a.title && a.title.replace(/\s/g, '_').replace(/[^A-Za-z0-9\-_:\.]/g, '') || this.options.idPrefix + $.data(a); }, _sanitizeSelector: function(hash) { return hash.replace(/:/g, '\\:'); // we need this because an id may contain a ":" }, _cookie: function() { var cookie = this.cookie || (this.cookie = 'ui-tabs-' + $.data(this.element[0])); return $.cookie.apply(null, [cookie].concat($.makeArray(arguments))); }, _tabify: function(init) { this.$lis = $('li:has(a[href])', this.element.is('div') ? $('> ul:first', this.element) : this.element); this.$tabs = this.$lis.map(function() { return $('a', this)[0]; }); this.$panels = $([]); var self = this, o = this.options; this.$tabs.each(function(i, a) { // inline tab if (a.hash && a.hash.replace('#', '')) // Safari 2 reports '#' for an empty hash self.$panels = self.$panels.add(self._sanitizeSelector(a.hash)); // remote tab else if ($(a).attr('href') != '#') { // prevent loading the page itself if href is just "#" $.data(a, 'href.tabs', a.href); // required for restore on destroy $.data(a, 'load.tabs', a.href); // mutable var id = self._tabId(a); a.href = '#' + id; var $panel = $('#' + id); if (!$panel.length) { $panel = $(o.panelTemplate).attr('id', id).addClass(o.panelClass) .insertAfter(self.$panels[i - 1] || self.element); $panel.data('destroy.tabs', true); } self.$panels = self.$panels.add($panel); } // invalid tab href else o.disabled.push(i + 1); }); // initialization from scratch if (init) { // attach necessary classes for styling if (this.element.is('div')) { // TODO replace hardcoded class names this.element.addClass('ui-tabs ui-widget ui-widget-content ui-corner-all'); $('> ul:first', this.element).addClass(o.navClass); } else { this.element.addClass(o.navClass); } this.$lis.addClass(o.tabClass); this.$panels.addClass(o.panelClass); // Selected tab // use "selected" option or try to retrieve: // 1. from fragment identifier in url // 2. from cookie // 3. from selected class attribute on
  • if (o.selected === undefined) { if (location.hash) { this.$tabs.each(function(i, a) { if (a.hash == location.hash) { o.selected = i; return false; // break } }); } else if (o.cookie) { var index = parseInt(self._cookie(), 10); if (index && self.$tabs[index]) o.selected = index; } else if (self.$lis.filter('.' + o.selectedClass).length) o.selected = self.$lis.index( self.$lis.filter('.' + o.selectedClass)[0] ); } o.selected = o.selected === null || o.selected !== undefined ? o.selected : 0; // first tab selected by default // Take disabling tabs via class attribute from HTML // into account and update option properly. // A selected tab cannot become disabled. o.disabled = $.unique(o.disabled.concat( $.map(this.$lis.filter('.' + o.disabledClass), function(n, i) { return self.$lis.index(n); } ) )).sort(); if ($.inArray(o.selected, o.disabled) != -1) o.disabled.splice($.inArray(o.selected, o.disabled), 1); // highlight selected tab this.$panels.addClass(o.hideClass); this.$lis.removeClass(o.selectedClass); if (o.selected !== null) { this.$panels.eq(o.selected).removeClass(o.hideClass); var classes = [o.selectedClass]; if (o.deselectable) classes.push(o.deselectableClass); this.$lis.eq(o.selected).addClass(classes.join(' ')); // seems to be expected behavior that the show callback is fired var onShow = function() { self._trigger('show', null, self.ui(self.$tabs[o.selected], self.$panels[o.selected])); }; // load if remote tab if ($.data(this.$tabs[o.selected], 'load.tabs')) this.load(o.selected, onShow); // just trigger show event else onShow(); } // states var handleState = function(state, el) { if (el.is(':not(.' + o.disabledClass + ')')) el.toggleClass('ui-state-' + state); }; this.$lis.bind('mouseover.tabs mouseout.tabs', function() { handleState('hover', $(this)); }); this.$tabs.bind('focus.tabs blur.tabs', function() { handleState('focus', $(this).parents('li:first')); }); // clean up to avoid memory leaks in certain versions of IE 6 $(window).bind('unload', function() { self.$lis.add(self.$tabs).unbind('.tabs'); self.$lis = self.$tabs = self.$panels = null; }); } // update selected after add/remove else o.selected = this.$lis.index( this.$lis.filter('.' + o.selectedClass)[0] ); // set or update cookie after init and add/remove respectively if (o.cookie) this._cookie(o.selected, o.cookie); // disable tabs for (var i = 0, li; li = this.$lis[i]; i++) $(li)[$.inArray(i, o.disabled) != -1 && !$(li).hasClass(o.selectedClass) ? 'addClass' : 'removeClass'](o.disabledClass); // reset cache if switching from cached to not cached if (o.cache === false) this.$tabs.removeData('cache.tabs'); // set up animations var hideFx, showFx; if (o.fx) { if (o.fx.constructor == Array) { hideFx = o.fx[0]; showFx = o.fx[1]; } else hideFx = showFx = o.fx; } // Reset certain styles left over from animation // and prevent IE's ClearType bug... function resetStyle($el, fx) { $el.css({ display: '' }); if ($.browser.msie && fx.opacity) $el[0].style.removeAttribute('filter'); } // Show a tab... var showTab = showFx ? function(clicked, $show) { $show.animate(showFx, showFx.duration || 'normal', function() { $show.removeClass(o.hideClass); resetStyle($show, showFx); self._trigger('show', null, self.ui(clicked, $show[0])); }); } : function(clicked, $show) { $show.removeClass(o.hideClass); self._trigger('show', null, self.ui(clicked, $show[0])); }; // Hide a tab, $show is optional... var hideTab = hideFx ? function(clicked, $hide, $show) { $hide.animate(hideFx, hideFx.duration || 'normal', function() { $hide.addClass(o.hideClass); resetStyle($hide, hideFx); if ($show) showTab(clicked, $show, $hide); }); } : function(clicked, $hide, $show) { $hide.addClass(o.hideClass); if ($show) showTab(clicked, $show); }; // Switch a tab... function switchTab(clicked, $li, $hide, $show) { var classes = [o.selectedClass]; if (o.deselectable) classes.push(o.deselectableClass); // TODO replace hardcoded class names $li.removeClass('ui-state-default').addClass(classes.join(' ')) .siblings().removeClass(classes.join(' ')).addClass('ui-state-default'); hideTab(clicked, $hide, $show); } // attach tab event handler, unbind to avoid duplicates from former tabifying... this.$tabs.unbind('.tabs').bind(o.event + '.tabs', function() { //var trueClick = event.clientX; // add to history only if true click occured, not a triggered click var $li = $(this).parents('li:eq(0)'), $hide = self.$panels.filter(':visible'), $show = $(self._sanitizeSelector(this.hash)); // If tab is already selected and not deselectable or tab disabled or // or is already loading or click callback returns false stop here. // Check if click handler returns false last so that it is not executed // for a disabled or loading tab! // TODO replace hardcoded class names if (($li.hasClass('ui-state-active') && !o.deselectable) || $li.hasClass(o.disabledClass) || $(this).hasClass(o.loadingClass) || self._trigger('select', null, self.ui(this, $show[0])) === false ) { this.blur(); return false; } o.selected = self.$tabs.index(this); // if tab may be closed // TODO replace hardcoded class names if (o.deselectable) { if ($li.hasClass('ui-state-active')) { self.options.selected = null; $li.removeClass([o.selectedClass, o.deselectableClass].join(' ')). addClass('ui-state-default'); self.$panels.stop(); hideTab(this, $hide); this.blur(); return false; } else if (!$hide.length) { self.$panels.stop(); var a = this; self.load(self.$tabs.index(this), function() { $li.addClass([o.selectedClass, o.deselectableClass].join(' ')) .removeClass('ui-state-default'); showTab(a, $show); }); this.blur(); return false; } } if (o.cookie) self._cookie(o.selected, o.cookie); // stop possibly running animations self.$panels.stop(); // show new tab if ($show.length) { var a = this; self.load(self.$tabs.index(this), $hide.length ? function() { switchTab(a, $li, $hide, $show); } : function() { $li.addClass(o.selectedClass).removeClass('ui-state-default'); showTab(a, $show); } ); } else throw 'jQuery UI Tabs: Mismatching fragment identifier.'; // Prevent IE from keeping other link focussed when using the back button // and remove dotted border from clicked link. This is controlled via CSS // in modern browsers; blur() removes focus from address bar in Firefox // which can become a usability and annoying problem with tabs('rotate'). if ($.browser.msie) this.blur(); return false; }); // disable click if event is configured to something else if (o.event != 'click') this.$tabs.bind('click.tabs', function(){return false;}); }, add: function(url, label, index) { if (index == undefined) index = this.$tabs.length; // append by default var o = this.options; var $li = $(o.tabTemplate.replace(/#\{href\}/g, url).replace(/#\{label\}/g, label)); $li.addClass(o.tabClass).data('destroy.tabs', true); var id = url.indexOf('#') == 0 ? url.replace('#', '') : this._tabId( $('a:first-child', $li)[0] ); // try to find an existing element before creating a new one var $panel = $('#' + id); if (!$panel.length) { $panel = $(o.panelTemplate).attr('id', id) .addClass(o.hideClass) .data('destroy.tabs', true); } $panel.addClass(o.panelClass); if (index >= this.$lis.length) { $li.appendTo(this.element); $panel.appendTo(this.element[0].parentNode); } else { $li.insertBefore(this.$lis[index]); $panel.insertBefore(this.$panels[index]); } o.disabled = $.map(o.disabled, function(n, i) { return n >= index ? ++n : n }); this._tabify(); if (this.$tabs.length == 1) { $li.addClass(o.selectedClass); $panel.removeClass(o.hideClass); var href = $.data(this.$tabs[0], 'load.tabs'); if (href) this.load(index, href); } // callback this._trigger('add', null, this.ui(this.$tabs[index], this.$panels[index])); }, remove: function(index) { var o = this.options, $li = this.$lis.eq(index).remove(), $panel = this.$panels.eq(index).remove(); // If selected tab was removed focus tab to the right or // in case the last tab was removed the tab to the left. if ($li.hasClass(o.selectedClass) && this.$tabs.length > 1) this.select(index + (index + 1 < this.$tabs.length ? 1 : -1)); o.disabled = $.map($.grep(o.disabled, function(n, i) { return n != index; }), function(n, i) { return n >= index ? --n : n }); this._tabify(); // callback this._trigger('remove', null, this.ui($li.find('a')[0], $panel[0])); }, enable: function(index) { var o = this.options; if ($.inArray(index, o.disabled) == -1) return; var $li = this.$lis.eq(index).removeClass(o.disabledClass); if ($.browser.safari) { // fix disappearing tab (that used opacity indicating disabling) after enabling in Safari 2... $li.css('display', 'inline-block'); setTimeout(function() { $li.css('display', 'block'); }, 0); } o.disabled = $.grep(o.disabled, function(n, i) { return n != index; }); // callback this._trigger('enable', null, this.ui(this.$tabs[index], this.$panels[index])); }, disable: function(index) { var self = this, o = this.options; if (index != o.selected) { // cannot disable already selected tab this.$lis.eq(index).addClass(o.disabledClass); o.disabled.push(index); o.disabled.sort(); // callback this._trigger('disable', null, this.ui(this.$tabs[index], this.$panels[index])); } }, select: function(index) { // TODO make null as argument work if (typeof index == 'string') index = this.$tabs.index( this.$tabs.filter('[href$=' + index + ']')[0] ); this.$tabs.eq(index).trigger(this.options.event + '.tabs'); }, load: function(index, callback) { // callback is for internal usage only var self = this, o = this.options, $a = this.$tabs.eq(index), a = $a[0], bypassCache = callback == undefined || callback === false, url = $a.data('load.tabs'); callback = callback || function() {}; // no remote or from cache - just finish with callback if (!url || !bypassCache && $.data(a, 'cache.tabs')) { callback(); return; } // load remote from here on var inner = function(parent) { var $parent = $(parent), $inner = $parent.find('*:last'); return $inner.length && $inner.is(':not(img)') && $inner || $parent; }; var cleanup = function() { self.$tabs.filter('.' + o.loadingClass).removeClass(o.loadingClass) .each(function() { if (o.spinner) inner(this).parent().html(inner(this).data('label.tabs')); }); self.xhr = null; }; if (o.spinner) { var label = inner(a).html(); inner(a).wrapInner('') .find('em').data('label.tabs', label).html(o.spinner); } var ajaxOptions = $.extend({}, o.ajaxOptions, { url: url, success: function(r, s) { $(self._sanitizeSelector(a.hash)).html(r); cleanup(); if (o.cache) $.data(a, 'cache.tabs', true); // if loaded once do not load them again // callbacks self._trigger('load', null, self.ui(self.$tabs[index], self.$panels[index])); try { o.ajaxOptions.success(r, s); } catch (event) {} // This callback is required because the switch has to take // place after loading has completed. Call last in order to // fire load before show callback... callback(); } }); if (this.xhr) { // terminate pending requests from other tabs and restore tab label this.xhr.abort(); cleanup(); } $a.addClass(o.loadingClass); self.xhr = $.ajax(ajaxOptions); }, url: function(index, url) { this.$tabs.eq(index).removeData('cache.tabs').data('load.tabs', url); }, ui: function(tab, panel) { return { options: this.options, tab: tab, panel: panel, index: this.$tabs.index(tab) }; } }); $.extend($.ui.tabs, { version: '@VERSION', getter: 'length', defaults: { ajaxOptions: null, cache: false, cookie: null, // e.g. { expires: 7, path: '/', domain: 'jquery.com', secure: true } deselectable: false, deselectableClass: 'ui-tabs-deselectable', disabled: [], disabledClass: 'ui-state-disabled', event: 'click', fx: null, // e.g. { height: 'toggle', opacity: 'toggle', duration: 200 } hideClass: 'ui-tabs-hide', idPrefix: 'ui-tabs-', loadingClass: 'ui-tabs-loading', navClass: 'ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all', tabClass: 'ui-state-default ui-corner-top', panelClass: 'ui-tabs-panel ui-widget-content ui-corner-bottom', panelTemplate: '
    ', selectedClass: 'ui-tabs-selected ui-state-active', spinner: 'Loading…', tabTemplate: '
  • #{label}
  • ' } }); /* * Tabs Extensions */ /* * Rotate */ $.extend($.ui.tabs.prototype, { rotation: null, rotate: function(ms, continuing) { continuing = continuing || false; var self = this, t = this.options.selected; function start() { self.rotation = setInterval(function() { t = ++t < self.$tabs.length ? t : 0; self.select(t); }, ms); } function stop(event) { if (!event || event.clientX) { // only in case of a true click clearInterval(self.rotation); } } // start interval if (ms) { start(); if (!continuing) this.$tabs.bind(this.options.event + '.tabs', stop); else this.$tabs.bind(this.options.event + '.tabs', function() { stop(); t = self.options.selected; start(); }); } // stop interval else { stop(); this.$tabs.unbind(this.options.event + '.tabs', stop); } } }); })(jQuery);