diff options
author | Richard Worth <rdworth@gmail.com> | 2009-09-17 10:39:12 +0000 |
---|---|---|
committer | Richard Worth <rdworth@gmail.com> | 2009-09-17 10:39:12 +0000 |
commit | e2d873e6f06c6d4539302653d63540ba0b6f5e48 (patch) | |
tree | cca59581cddd66b4fedcac6d09ae15e9fabf7837 /ui/jquery.ui.tabs.js | |
parent | 79c433fcab0fa80347de326577a52f1ca22d1ae7 (diff) | |
download | jquery-ui-e2d873e6f06c6d4539302653d63540ba0b6f5e48.tar.gz jquery-ui-e2d873e6f06c6d4539302653d63540ba0b6f5e48.zip |
renamed all ui.*.js files to jquery.ui.*.js, all effects.*.js files to jquery.effects.*.js per announcement and discussion here http://groups.google.com/group/jquery-ui-dev/msg/d565a0c56e0cb935
Diffstat (limited to 'ui/jquery.ui.tabs.js')
-rw-r--r-- | ui/jquery.ui.tabs.js | 708 |
1 files changed, 708 insertions, 0 deletions
diff --git a/ui/jquery.ui.tabs.js b/ui/jquery.ui.tabs.js new file mode 100644 index 000000000..172177cf2 --- /dev/null +++ b/ui/jquery.ui.tabs.js @@ -0,0 +1,708 @@ +/* + * jQuery UI Tabs @VERSION + * + * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT (MIT-LICENSE.txt) + * and GPL (GPL-LICENSE.txt) licenses. + * + * http://docs.jquery.com/UI/Tabs + * + * Depends: + * jquery.ui.core.js + */ +(function($) { + +$.widget("ui.tabs", { + + _init: function() { + if (this.options.deselectable !== undefined) { + this.options.collapsible = this.options.deselectable; + } + this._tabify(true); + }, + + _setData: function(key, value) { + if (key == 'selected') { + if (this.options.collapsible && value == this.options.selected) { + return; + } + this.select(value); + } + else { + this.options[key] = value; + if (key == 'deselectable') { + this.options.collapsible = value; + } + this._tabify(); + } + }, + + _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 = this.options.cookie.name || 'ui-tabs-' + $.data(this.list[0])); + return $.cookie.apply(null, [cookie].concat($.makeArray(arguments))); + }, + + _ui: function(tab, panel) { + return { + tab: tab, + panel: panel, + index: this.anchors.index(tab) + }; + }, + + _cleanup: function() { + // restore all former loading tabs labels + this.lis.filter('.ui-state-processing').removeClass('ui-state-processing') + .find('span:data(label.tabs)') + .each(function() { + var el = $(this); + el.html(el.data('label.tabs')).removeData('label.tabs'); + }); + }, + + _tabify: function(init) { + + this.list = this.element.children('ul:first'); + this.lis = $('li:has(a[href])', this.list); + this.anchors = this.lis.map(function() { return $('a', this)[0]; }); + this.panels = $([]); + + var self = this, o = this.options; + + var fragmentId = /^#.+/; // Safari 2 reports '#' for an empty hash + this.anchors.each(function(i, a) { + var href = $(a).attr('href'); + + // For dynamically created HTML that contains a hash as href IE < 8 expands + // such href to the full page url with hash and then misinterprets tab as ajax. + // Same consideration applies for an added tab with a fragment identifier + // since a[href=#fragment-identifier] does unexpectedly not match. + // Thus normalize href attribute... + var hrefBase = href.split('#')[0], baseEl; + if (hrefBase && (hrefBase === location.toString().split('#')[0] || + (baseEl = $('base')[0]) && hrefBase === baseEl.href)) { + href = a.hash; + a.href = href; + } + + // inline tab + if (fragmentId.test(href)) { + self.panels = self.panels.add(self._sanitizeSelector(href)); + } + + // remote tab + else if (href != '#') { // prevent loading the page itself if href is just "#" + $.data(a, 'href.tabs', href); // required for restore on destroy + + // TODO until #3808 is fixed strip fragment identifier from url + // (IE fails to load from such url) + $.data(a, 'load.tabs', href.replace(/#.*$/, '')); // mutable data + + var id = self._tabId(a); + a.href = '#' + id; + var $panel = $('#' + id); + if (!$panel.length) { + $panel = $(o.panelTemplate).attr('id', id).addClass('ui-tabs-panel ui-widget-content ui-corner-bottom') + .insertAfter(self.panels[i - 1] || self.list); + $panel.data('destroy.tabs', true); + } + self.panels = self.panels.add($panel); + } + + // invalid tab href + else { + o.disabled.push(i); + } + }); + + // initialization from scratch + if (init) { + + // attach necessary classes for styling + this.element.addClass('ui-tabs ui-widget ui-widget-content ui-corner-all'); + this.list.addClass('ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all'); + this.lis.addClass('ui-state-default ui-corner-top'); + this.panels.addClass('ui-tabs-panel ui-widget-content ui-corner-bottom'); + + // Selected tab + // use "selected" option or try to retrieve: + // 1. from fragment identifier in url + // 2. from cookie + // 3. from selected class attribute on <li> + if (o.selected === undefined) { + if (location.hash) { + this.anchors.each(function(i, a) { + if (a.hash == location.hash) { + o.selected = i; + return false; // break + } + }); + } + if (typeof o.selected != 'number' && o.cookie) { + o.selected = parseInt(self._cookie(), 10); + } + if (typeof o.selected != 'number' && this.lis.filter('.ui-tabs-selected').length) { + o.selected = this.lis.index(this.lis.filter('.ui-tabs-selected')); + } + o.selected = o.selected || 0; + } + else if (o.selected === null) { // usage of null is deprecated, TODO remove in next release + o.selected = -1; + } + + // sanity check - default to first tab... + o.selected = ((o.selected >= 0 && this.anchors[o.selected]) || o.selected < 0) ? o.selected : 0; + + // 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('.ui-state-disabled'), + 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('ui-tabs-hide'); + this.lis.removeClass('ui-tabs-selected ui-state-active'); + if (o.selected >= 0 && this.anchors.length) { // check for length avoids error when initializing empty list + this.panels.eq(o.selected).removeClass('ui-tabs-hide'); + this.lis.eq(o.selected).addClass('ui-tabs-selected ui-state-active'); + + // seems to be expected behavior that the show callback is fired + self.element.queue("tabs", function() { + self._trigger('show', null, self._ui(self.anchors[o.selected], self.panels[o.selected])); + }); + + this.load(o.selected); + } + + // clean up to avoid memory leaks in certain versions of IE 6 + $(window).bind('unload', function() { + self.lis.add(self.anchors).unbind('.tabs'); + self.lis = self.anchors = self.panels = null; + }); + + } + // update selected after add/remove + else { + o.selected = this.lis.index(this.lis.filter('.ui-tabs-selected')); + } + + // update collapsible + this.element[o.collapsible ? 'addClass' : 'removeClass']('ui-tabs-collapsible'); + + // 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('ui-tabs-selected') ? 'addClass' : 'removeClass']('ui-state-disabled'); + } + + // reset cache if switching from cached to not cached + if (o.cache === false) { + this.anchors.removeData('cache.tabs'); + } + + // remove all handlers before, tabify may run on existing tabs after add or option change + this.lis.add(this.anchors).unbind('.tabs'); + + if (o.event != 'mouseover') { + var addState = function(state, el) { + if (el.is(':not(.ui-state-disabled)')) { + el.addClass('ui-state-' + state); + } + }; + var removeState = function(state, el) { + el.removeClass('ui-state-' + state); + }; + this.lis.bind('mouseover.tabs', function() { + addState('hover', $(this)); + }); + this.lis.bind('mouseout.tabs', function() { + removeState('hover', $(this)); + }); + this.anchors.bind('focus.tabs', function() { + addState('focus', $(this).closest('li')); + }); + this.anchors.bind('blur.tabs', function() { + removeState('focus', $(this).closest('li')); + }); + } + + // set up animations + var hideFx, showFx; + if (o.fx) { + if ($.isArray(o.fx)) { + 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 (!$.support.opacity && fx.opacity) { + $el[0].style.removeAttribute('filter'); + } + } + + // Show a tab... + var showTab = showFx ? + function(clicked, $show) { + $(clicked).closest('li').removeClass('ui-state-default').addClass('ui-tabs-selected ui-state-active'); + $show.hide().removeClass('ui-tabs-hide') // avoid flicker that way + .animate(showFx, showFx.duration || 'normal', function() { + resetStyle($show, showFx); + self._trigger('show', null, self._ui(clicked, $show[0])); + }); + } : + function(clicked, $show) { + $(clicked).closest('li').removeClass('ui-state-default').addClass('ui-tabs-selected ui-state-active'); + $show.removeClass('ui-tabs-hide'); + self._trigger('show', null, self._ui(clicked, $show[0])); + }; + + // Hide a tab, $show is optional... + var hideTab = hideFx ? + function(clicked, $hide) { + $hide.animate(hideFx, hideFx.duration || 'normal', function() { + self.lis.removeClass('ui-tabs-selected ui-state-active').addClass('ui-state-default'); + $hide.addClass('ui-tabs-hide'); + resetStyle($hide, hideFx); + self.element.dequeue("tabs"); + }); + } : + function(clicked, $hide, $show) { + self.lis.removeClass('ui-tabs-selected ui-state-active').addClass('ui-state-default'); + $hide.addClass('ui-tabs-hide'); + self.element.dequeue("tabs"); + }; + + // attach tab event handler, unbind to avoid duplicates from former tabifying... + this.anchors.bind(o.event + '.tabs', function() { + var el = this, $li = $(this).closest('li'), $hide = self.panels.filter(':not(.ui-tabs-hide)'), + $show = $(self._sanitizeSelector(this.hash)); + + // If tab is already selected and not collapsible 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! + if (($li.hasClass('ui-tabs-selected') && !o.collapsible) || + $li.hasClass('ui-state-disabled') || + $li.hasClass('ui-state-processing') || + self._trigger('select', null, self._ui(this, $show[0])) === false) { + this.blur(); + return false; + } + + o.selected = self.anchors.index(this); + + self.abort(); + + // if tab may be closed + if (o.collapsible) { + if ($li.hasClass('ui-tabs-selected')) { + o.selected = -1; + + if (o.cookie) { + self._cookie(o.selected, o.cookie); + } + + self.element.queue("tabs", function() { + hideTab(el, $hide); + }).dequeue("tabs"); + + this.blur(); + return false; + } + else if (!$hide.length) { + if (o.cookie) { + self._cookie(o.selected, o.cookie); + } + + self.element.queue("tabs", function() { + showTab(el, $show); + }); + + self.load(self.anchors.index(this)); // TODO make passing in node possible, see also http://dev.jqueryui.com/ticket/3171 + + this.blur(); + return false; + } + } + + if (o.cookie) { + self._cookie(o.selected, o.cookie); + } + + // show new tab + if ($show.length) { + if ($hide.length) { + self.element.queue("tabs", function() { + hideTab(el, $hide); + }); + } + self.element.queue("tabs", function() { + showTab(el, $show); + }); + + self.load(self.anchors.index(this)); + } + 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(); + } + + }); + + // disable click in any case + this.anchors.bind('click.tabs', function(){return false;}); + + }, + + destroy: function() { + var o = this.options; + + this.abort(); + + this.element.unbind('.tabs') + .removeClass('ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible') + .removeData('tabs'); + + this.list.removeClass('ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all'); + + this.anchors.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([ + 'ui-state-default', + 'ui-corner-top', + 'ui-tabs-selected', + 'ui-state-active', + 'ui-state-hover', + 'ui-state-focus', + 'ui-state-disabled', + 'ui-tabs-panel', + 'ui-widget-content', + 'ui-corner-bottom', + 'ui-tabs-hide' + ].join(' ')); + } + }); + + if (o.cookie) { + this._cookie(null, o.cookie); + } + + return this; + }, + + add: function(url, label, index) { + if (index === undefined) { + index = this.anchors.length; // append by default + } + + var self = this, o = this.options, + $li = $(o.tabTemplate.replace(/#\{href\}/g, url).replace(/#\{label\}/g, label)), + id = !url.indexOf('#') ? url.replace('#', '') : this._tabId($('a', $li)[0]); + + $li.addClass('ui-state-default ui-corner-top').data('destroy.tabs', true); + + // try to find an existing element before creating a new one + var $panel = $('#' + id); + if (!$panel.length) { + $panel = $(o.panelTemplate).attr('id', id).data('destroy.tabs', true); + } + $panel.addClass('ui-tabs-panel ui-widget-content ui-corner-bottom ui-tabs-hide'); + + if (index >= this.lis.length) { + $li.appendTo(this.list); + $panel.appendTo(this.list[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.anchors.length == 1) { // after tabify + $li.addClass('ui-tabs-selected ui-state-active'); + $panel.removeClass('ui-tabs-hide'); + this.element.queue("tabs", function() { + self._trigger('show', null, self._ui(self.anchors[0], self.panels[0])); + }); + + this.load(0); + } + + // callback + this._trigger('add', null, this._ui(this.anchors[index], this.panels[index])); + return this; + }, + + 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('ui-tabs-selected') && this.anchors.length > 1) { + this.select(index + (index + 1 < this.anchors.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])); + return this; + }, + + enable: function(index) { + var o = this.options; + if ($.inArray(index, o.disabled) == -1) { + return; + } + + this.lis.eq(index).removeClass('ui-state-disabled'); + o.disabled = $.grep(o.disabled, function(n, i) { return n != index; }); + + // callback + this._trigger('enable', null, this._ui(this.anchors[index], this.panels[index])); + return this; + }, + + disable: function(index) { + var self = this, o = this.options; + if (index != o.selected) { // cannot disable already selected tab + this.lis.eq(index).addClass('ui-state-disabled'); + + o.disabled.push(index); + o.disabled.sort(); + + // callback + this._trigger('disable', null, this._ui(this.anchors[index], this.panels[index])); + } + + return this; + }, + + select: function(index) { + if (typeof index == 'string') { + index = this.anchors.index(this.anchors.filter('[href$=' + index + ']')); + } + else if (index === null) { // usage of null is deprecated, TODO remove in next release + index = -1; + } + if (index == -1 && this.options.collapsible) { + index = this.options.selected; + } + + this.anchors.eq(index).trigger(this.options.event + '.tabs'); + return this; + }, + + load: function(index) { + var self = this, o = this.options, a = this.anchors.eq(index)[0], url = $.data(a, 'load.tabs'); + + this.abort(); + + // not remote or from cache + if (!url || this.element.queue("tabs").length !== 0 && $.data(a, 'cache.tabs')) { + this.element.dequeue("tabs"); + return; + } + + // load remote from here on + this.lis.eq(index).addClass('ui-state-processing'); + + if (o.spinner) { + var span = $('span', a); + span.data('label.tabs', span.html()).html(o.spinner); + } + + this.xhr = $.ajax($.extend({}, o.ajaxOptions, { + url: url, + success: function(r, s) { + $(self._sanitizeSelector(a.hash)).html(r); + + // take care of tab labels + self._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.anchors[index], self.panels[index])); + try { + o.ajaxOptions.success(r, s); + } + catch (e) {} + + // last, so that load event is fired before show... + self.element.dequeue("tabs"); + } + })); + + return this; + }, + + abort: function() { + // stop possibly running animations + this.element.queue([]); + this.panels.stop(false, true); + + // "tabs" queue must not contain more than two elements, + // which are the callbacks for the latest clicked tab... + this.element.queue("tabs", this.element.queue("tabs").splice(-2, 2)); + + // terminate pending requests from other tabs + if (this.xhr) { + this.xhr.abort(); + delete this.xhr; + } + + // take care of tab labels + this._cleanup(); + return this; + }, + + url: function(index, url) { + this.anchors.eq(index).removeData('cache.tabs').data('load.tabs', url); + return this; + }, + + length: function() { + return this.anchors.length; + } + +}); + +$.extend($.ui.tabs, { + version: '@VERSION', + defaults: { + add: null, + ajaxOptions: null, + cache: false, + cookie: null, // e.g. { expires: 7, path: '/', domain: 'jquery.com', secure: true } + collapsible: false, + disable: null, + disabled: [], + enable: null, + event: 'click', + fx: null, // e.g. { height: 'toggle', opacity: 'toggle', duration: 200 } + idPrefix: 'ui-tabs-', + load: null, + panelTemplate: '<div></div>', + remove: null, + select: null, + show: null, + spinner: '<em>Loading…</em>', + tabTemplate: '<li><a href="#{href}"><span>#{label}</span></a></li>' + } +}); + +/* + * Tabs Extensions + */ + +/* + * Rotate + */ +$.extend($.ui.tabs.prototype, { + rotation: null, + rotate: function(ms, continuing) { + + var self = this, o = this.options; + + var rotate = self._rotate || (self._rotate = function(e) { + clearTimeout(self.rotation); + self.rotation = setTimeout(function() { + var t = o.selected; + self.select( ++t < self.anchors.length ? t : 0 ); + }, ms); + + if (e) { + e.stopPropagation(); + } + }); + + var stop = self._unrotate || (self._unrotate = !continuing ? + function(e) { + if (e.clientX) { // in case of a true click + self.rotate(null); + } + } : + function(e) { + t = o.selected; + rotate(); + }); + + // start rotation + if (ms) { + this.element.bind('tabsshow', rotate); + this.anchors.bind(o.event + '.tabs', stop); + rotate(); + } + // stop rotation + else { + clearTimeout(self.rotation); + this.element.unbind('tabsshow', rotate); + this.anchors.unbind(o.event + '.tabs', stop); + delete this._rotate; + delete this._unrotate; + } + + return this; + } +}); + +})(jQuery); |