diff --git a/tests/unit/tabs/spinner.gif b/tests/unit/tabs/spinner.gif new file mode 100644 index 000000000..85b99d46b Binary files /dev/null and b/tests/unit/tabs/spinner.gif differ diff --git a/tests/unit/tabs/tabs.html b/tests/unit/tabs/tabs.html index d9cb4d138..69cd8f481 100644 --- a/tests/unit/tabs/tabs.html +++ b/tests/unit/tabs/tabs.html @@ -24,9 +24,9 @@
@@ -34,9 +34,9 @@
diff --git a/tests/unit/tabs/tabs_core.js b/tests/unit/tabs/tabs_core.js index ba5f3d014..79e95ea41 100644 --- a/tests/unit/tabs/tabs_core.js +++ b/tests/unit/tabs/tabs_core.js @@ -7,4 +7,33 @@ var el; module("tabs: core"); +test('ajax', function() { + expect(4); + stop(); + + el = $('#tabs2'); + + el.tabs({ + selected: 2, + load: function() { + // spinner: default spinner + equals($('li:eq(2) > a > span', el).length, 1, "should restore tab markup after spinner is removed"); + equals($('li:eq(2) > a > span', el).html(), '3', "should restore tab label after spinner is removed"); + el.tabs('destroy'); + el.tabs({ + selected: 2, + spinner: '', + load: function() { + // spinner: image + equals($('li:eq(2) > a > span', el).length, 1, "should restore tab markup after spinner is removed"); + equals($('li:eq(2) > a > span', el).html(), '3', "should restore tab label after spinner is removed"); + start(); + } + }); + } + }); + +}); + + })(jQuery); diff --git a/tests/unit/tabs/tabs_defaults.js b/tests/unit/tabs/tabs_defaults.js index 306e8c86e..f5a583e2c 100644 --- a/tests/unit/tabs/tabs_defaults.js +++ b/tests/unit/tabs/tabs_defaults.js @@ -12,7 +12,7 @@ var tabs_defaults = { fx: null, idPrefix: 'ui-tabs-', panelTemplate: '
', - spinner: 'Loading…', + spinner: 'Loading…', tabTemplate: '
  • #{label}
  • ' }; diff --git a/tests/unit/tabs/test.html b/tests/unit/tabs/test.html new file mode 100644 index 000000000..cd59e6415 --- /dev/null +++ b/tests/unit/tabs/test.html @@ -0,0 +1 @@ +

    …content loaded via Ajax.

    \ No newline at end of file diff --git a/themes/base/ui.tabs.css b/themes/base/ui.tabs.css index ae67ad57a..dcd51b048 100644 --- a/themes/base/ui.tabs.css +++ b/themes/base/ui.tabs.css @@ -5,7 +5,7 @@ .ui-tabs .ui-tabs-nav li { position: relative; float: left; border-bottom-width: 0 !important; margin: 0 .2em -1px 0; padding: 0; } .ui-tabs .ui-tabs-nav li a { float: left; text-decoration: none; padding: .5em 1em; } .ui-tabs .ui-tabs-nav li.ui-tabs-selected { padding-bottom: .1em; border-bottom-width: 0; } -.ui-tabs .ui-tabs-nav li.ui-tabs-selected a { cursor: text; } +.ui-tabs .ui-tabs-nav li.ui-tabs-selected a, .ui-tabs .ui-tabs-nav li.ui-state-processing a { cursor: text; } .ui-tabs .ui-tabs-nav li a, .ui-tabs.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-selected a { cursor: pointer; } /* first selector in group seems obsolete, but required to overcome bug in Opera applying cursor: text overall if defined elsewhere... */ .ui-tabs .ui-tabs-panel { padding: 1em 1.4em; display: block; border-width: 0; background: none; } .ui-tabs .ui-tabs-hide { display: none !important; } diff --git a/ui/ui.tabs.js b/ui/ui.tabs.js index 3208d60b5..57ff72f94 100644 --- a/ui/ui.tabs.js +++ b/ui/ui.tabs.js @@ -24,14 +24,14 @@ $.widget("ui.tabs", { 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(); } }, @@ -58,6 +58,16 @@ $.widget("ui.tabs", { }; }, + _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')); + }); + }, + _tabify: function(init) { this.list = this.element.children('ul:first, ol:first').eq(0); @@ -70,7 +80,7 @@ $.widget("ui.tabs", { var fragmentId = /^#.+/; // Safari 2 reports '#' for an empty hash this.$tabs.each(function(i, a) { var href = $(a).attr('href'); - + // For dynamically created HTML that contains a hash as href IE expands // such href to the full page url with hash and then misinterprets tab as ajax... if (href.split('#')[0] == location.toString().split('#')[0]) href = a.hash; @@ -183,10 +193,10 @@ $.widget("ui.tabs", { 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); @@ -199,7 +209,7 @@ $.widget("ui.tabs", { // remove all handlers before, tabify may run on existing tabs after add or option change this.$lis.add(this.$tabs).unbind('.tabs'); - + if (o.event != 'mouseover') { var handleState = function(state, el) { if (el.is(':not(.ui-state-disabled)')) el.toggleClass('ui-state-' + state); @@ -211,7 +221,7 @@ $.widget("ui.tabs", { handleState('focus', $(this).closest('li')); }); } - + // set up animations var hideFx, showFx; if (o.fx) { @@ -276,7 +286,7 @@ $.widget("ui.tabs", { // for a disabled or loading tab! if (($li.hasClass('ui-tabs-selected') && !o.collapsible) || $li.hasClass('ui-state-disabled') - || $(this).hasClass('ui-tabs-loading') + || $li.hasClass('ui-state-processing') || self._trigger('select', null, self._ui(this, $show[0])) === false ) { this.blur(); @@ -285,6 +295,8 @@ $.widget("ui.tabs", { o.selected = self.$tabs.index(this); + self.abort(); + // if tab may be closed TODO avoid redundant code in this block if (o.collapsible) { if ($li.hasClass('ui-tabs-selected')) { @@ -292,13 +304,11 @@ $.widget("ui.tabs", { if (o.cookie) self._cookie(o.selected, o.cookie); $li.removeClass('ui-tabs-selected ui-state-active') .addClass('ui-state-default'); - self.$panels.stop(); hideTab(this, $hide); this.blur(); return false; } else if (!$hide.length) { if (o.cookie) self._cookie(o.selected, o.cookie); - self.$panels.stop(); var a = this; self.load(self.$tabs.index(this), function() { $li.addClass('ui-tabs-selected ui-state-active') @@ -312,9 +322,6 @@ $.widget("ui.tabs", { if (o.cookie) self._cookie(o.selected, o.cookie); - // stop possibly running animations - self.$panels.stop(false, true); - // show new tab if ($show.length) { var a = this; @@ -327,8 +334,10 @@ $.widget("ui.tabs", { showTab(a, $show); } ); - } else + } + 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 @@ -474,21 +483,23 @@ $.widget("ui.tabs", { select: function(index) { if (typeof index == 'string') index = this.$tabs.index(this.$tabs.filter('[href$=' + index + ']')); - + else if (index === null) index = -1; if (index == -1 && this.options.collapsible) index = this.options.selected; - + this.$tabs.eq(index).trigger(this.options.event + '.tabs'); }, load: function(index, callback) { // callback is for internal usage only callback = callback || function() {}; - - var self = this, o = this.options, $a = this.$tabs.eq(index), a = $a[0], - bypassCache = callback == undefined, url = $a.data('load.tabs'); + + var self = this, o = this.options, a = this.$tabs.eq(index)[0], + bypassCache = callback == undefined, url = $.data(a, 'load.tabs'); + + this.abort(); // not remote or from cache - just finish with callback if (!url || !bypassCache && $.data(a, 'cache.tabs')) { @@ -497,55 +508,53 @@ $.widget("ui.tabs", { } // 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('.ui-tabs-loading').removeClass('ui-tabs-loading') - .each(function() { - if (o.spinner) - inner(this).parent().html(inner(this).data('label.tabs')); - }); - self.xhr = null; - }; + this.$lis.eq(index).addClass('ui-state-processing'); if (o.spinner) { - var label = inner(a).html(); - inner(a).wrapInner('') - .find('em').data('label.tabs', label).html(o.spinner); + var span = $('span', a); + span.data('label.tabs', span.html()).html(o.spinner); } - var ajaxOptions = $.extend({}, o.ajaxOptions, { + this.xhr = $.ajax($.extend({}, o.ajaxOptions, { url: url, success: function(r, s) { $(self._sanitizeSelector(a.hash)).html(r); - cleanup(); - if (o.cache) + // 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.$tabs[index], self.$panels[index])); try { o.ajaxOptions.success(r, s); } - catch (er) {} + catch (e) {} // 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(); } - }); + })); + }, + + abort: function() { + // stop possibly running animations + this.$panels.stop(false, true); + + // terminate pending requests from other tabs if (this.xhr) { - // terminate pending requests from other tabs and restore tab label this.xhr.abort(); - cleanup(); + delete this.xhr; } - $a.addClass('ui-tabs-loading'); - self.xhr = $.ajax(ajaxOptions); + + // take care of tab labels + this._cleanup(); + }, url: function(index, url) { @@ -571,7 +580,7 @@ $.extend($.ui.tabs, { fx: null, // e.g. { height: 'toggle', opacity: 'toggle', duration: 200 } idPrefix: 'ui-tabs-', panelTemplate: '
    ', - spinner: 'Loading…', + spinner: 'Loading…', tabTemplate: '
  • #{label}
  • ' } });