diff options
author | Scott González <scott.gonzalez@gmail.com> | 2011-03-29 15:40:28 -0400 |
---|---|---|
committer | Scott González <scott.gonzalez@gmail.com> | 2011-03-29 15:40:28 -0400 |
commit | fa13c4a60218cba40f61b66b92d9a520051783d1 (patch) | |
tree | 899837798b35ef4584600eeb8de0b689aaaf3f0b | |
parent | cd29170ae02b4a7eb290848f8523244c621e2893 (diff) | |
parent | e9ae04a394e63e5b012f28fc40a04e71c4f935d9 (diff) | |
download | jquery-ui-fa13c4a60218cba40f61b66b92d9a520051783d1.tar.gz jquery-ui-fa13c4a60218cba40f61b66b92d9a520051783d1.zip |
Merge branch 'tabs_1.9' of https://github.com/petersendidit/jquery-ui into petersendidit-tabs
-rw-r--r-- | demos/tabs/ajax.html | 3 | ||||
-rw-r--r-- | tests/unit/tabs/tabs.html | 3 | ||||
-rw-r--r-- | tests/unit/tabs/tabs_core.js | 30 | ||||
-rw-r--r-- | tests/unit/tabs/tabs_defaults.js | 18 | ||||
-rw-r--r-- | tests/unit/tabs/tabs_defaults_deprecated.js | 30 | ||||
-rw-r--r-- | tests/unit/tabs/tabs_deprecated.html | 122 | ||||
-rw-r--r-- | tests/unit/tabs/tabs_deprecated.js | 343 | ||||
-rw-r--r-- | tests/unit/tabs/tabs_events.js | 94 | ||||
-rw-r--r-- | tests/unit/tabs/tabs_methods.js | 120 | ||||
-rw-r--r-- | tests/unit/tabs/tabs_options.js | 93 | ||||
-rw-r--r-- | tests/unit/tabs/tabs_tickets.js | 58 | ||||
-rw-r--r-- | themes/base/jquery.ui.tabs.css | 7 | ||||
-rwxr-xr-x | ui/jquery.ui.tabs.js | 1189 |
13 files changed, 1359 insertions, 751 deletions
diff --git a/demos/tabs/ajax.html b/demos/tabs/ajax.html index c62625791..284de57f9 100644 --- a/demos/tabs/ajax.html +++ b/demos/tabs/ajax.html @@ -14,7 +14,8 @@ $( "#tabs" ).tabs({ ajaxOptions: { error: function( xhr, status, index, anchor ) { - $( anchor.hash ).html( + var selector = $( anchor ).attr( "aria-controls" ); + $( selector ).html( "Couldn't load this tab. We'll try to fix this as soon as possible. " + "If this wouldn't be a demo." ); } diff --git a/tests/unit/tabs/tabs.html b/tests/unit/tabs/tabs.html index 74855ca9a..02fbfe3bb 100644 --- a/tests/unit/tabs/tabs.html +++ b/tests/unit/tabs/tabs.html @@ -7,6 +7,9 @@ <link type="text/css" href="../../../themes/base/jquery.ui.tabs.css" rel="stylesheet" /> <script type="text/javascript" src="../../../jquery-1.5.1.js"></script> + <script> + $.uiBackCompat = false; + </script> <script type="text/javascript" src="../../../external/jquery.cookie.js"></script> <script type="text/javascript" src="../../../ui/jquery.ui.core.js"></script> <script type="text/javascript" src="../../../ui/jquery.ui.widget.js"></script> diff --git a/tests/unit/tabs/tabs_core.js b/tests/unit/tabs/tabs_core.js index 652788bba..7d9074819 100644 --- a/tests/unit/tabs/tabs_core.js +++ b/tests/unit/tabs/tabs_core.js @@ -25,34 +25,4 @@ test('navigation markup', function() { el.tabs('destroy'); }); -test('ajax', function() { - expect(4); - stop(); - - el = $('#tabs2'); - - el.tabs({ - selected: 2, - load: function() { - // spinner: default spinner - setTimeout(function() { - 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: '<img src="spinner.gif" alt="">', - 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(); - } - }); - }, 1); - } - }); - -}); - })(jQuery); diff --git a/tests/unit/tabs/tabs_defaults.js b/tests/unit/tabs/tabs_defaults.js index ef93c69ee..0f33cc28c 100644 --- a/tests/unit/tabs/tabs_defaults.js +++ b/tests/unit/tabs/tabs_defaults.js @@ -3,24 +3,14 @@ */ var tabs_defaults = { - add: null, - ajaxOptions: null, - cache: false, + activate: null, + beforeload: null, + beforeActivate: null, collapsible: false, - cookie: null, - disable: null, disabled: false, - enable: null, event: "click", fx: null, - 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>" + load: null }; // FAIL: falsy values break the cookie option diff --git a/tests/unit/tabs/tabs_defaults_deprecated.js b/tests/unit/tabs/tabs_defaults_deprecated.js new file mode 100644 index 000000000..03ee13d3d --- /dev/null +++ b/tests/unit/tabs/tabs_defaults_deprecated.js @@ -0,0 +1,30 @@ +/* + * tabs_defaults.js + */ + +var tabs_defaults = { + activate: null, + add: null, + ajaxOptions: null, + beforeload: null, + beforeActivate: null, + cache: false, + collapsible: false, + cookie: null, + disable: null, + disabled: false, + enable: null, + event: "click", + fx: null, + 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>" +}; + +// FAIL: falsy values break the cookie option +commonWidgetTests( "tabs", { defaults: tabs_defaults } ); diff --git a/tests/unit/tabs/tabs_deprecated.html b/tests/unit/tabs/tabs_deprecated.html new file mode 100644 index 000000000..3b927675c --- /dev/null +++ b/tests/unit/tabs/tabs_deprecated.html @@ -0,0 +1,122 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8" /> + <title>jQuery UI Tabs Test Suite</title> + + <link type="text/css" href="../../../themes/base/jquery.ui.tabs.css" rel="stylesheet" /> + + <script type="text/javascript" src="../../../jquery-1.5.1.js"></script> + <script type="text/javascript" src="../../../external/jquery.cookie.js"></script> + <script type="text/javascript" src="../../../ui/jquery.ui.core.js"></script> + <script type="text/javascript" src="../../../ui/jquery.ui.widget.js"></script> + <script type="text/javascript" src="../../../ui/jquery.ui.tabs.js"></script> + + <link rel="stylesheet" href="../../../external/qunit.css" type="text/css"/> + <script type="text/javascript" src="../../../external/qunit.js"></script> + <script type="text/javascript" src="../../jquery.simulate.js"></script> + <script type="text/javascript" src="../testsuite.js"></script> + + <script type="text/javascript" src="tabs_core.js"></script> + <script type="text/javascript" src="tabs_defaults_deprecated.js"></script> + <script type="text/javascript" src="tabs_events.js"></script> + <script type="text/javascript" src="tabs_methods.js"></script> + <script type="text/javascript" src="tabs_options.js"></script> + <script type="text/javascript" src="tabs_tickets.js"></script> + <script type="text/javascript" src="tabs_deprecated.js"></script> + + <script type="text/javascript"> + // disable this stale testsuite for testswarm only + var url = window.location.search; + url = decodeURIComponent( url.slice( url.indexOf("swarmURL=") + 9 ) ); + if ( url && url.indexOf("http") == 0 ) { + // reset config to kill previous tests; make sure testsuite.js is loaded afterwards to init the testswarm script + QUnit.init(); + test("tabs", function() { ok(true, "disabled tabs testsuite"); }); + } + </script> + <script type="text/javascript" src="../swarminject.js"></script> +</head> +<body> + +<h1 id="qunit-header">jQuery UI Tabs Test Suite (deprecated)</h1> +<h2 id="qunit-banner"></h2> +<div id="qunit-testrunner-toolbar"></div> +<h2 id="qunit-userAgent"></h2> +<ol id="qunit-tests"> +</ol> + +<div id="qunit-fixture"> + + <div id="tabs1"> + <ul> + <li><a href="#fragment-1"><span>1</span></a></li> + <li><a href="#fragment-2"><span>2</span></a></li> + <li><a href="#fragment-3"><span>3</span></a></li> + </ul> + <div id="fragment-1"></div> + <div id="fragment-2"></div> + <div id="fragment-3"></div> + </div> + <div id="tabs2"> + <ul> + <li><a href="#colon:test"><span>1</span></a></li> + <li><a href="#inline-style"><span>2</span></a></li> + <li><a href="data/test.html#test"><span>3</span></a></li> + <li><a href="data/test.html" title="∫ßáö Սե"><span>4</span></a></li> + </ul> + <div id="colon:test"></div> + <div style="height: 300px;" id="inline-style"></div> + </div> + <div id="tabs3"> + <div> + <ul id="tabs3-list"> + <li><a href="#tabs3-1">1</a></li> + </ul> + </div> + </div> + <div id="tabs4"> + <ul id="tabs4-list"> + <li><a href="#tabs4-1">1</a></li> + </ul> + <ol> + <li><a href="#tabs4-1">1</a></li> + </ol> + </div> + <div id="tabs4a"> + <ol id="tabs4a-list"> + <li><a href="#tabs4a-1">1</a></li> + </ol> + <ul> + <li><a href="#tabs4a-1">1</a></li> + </ul> + </div> + <div id="tabs5"> + <div> + <ul id="tabs5-list"></ul> + </div> + </div> + <div id="tabs6"> + <ul id="tabs6-list"> + <li><a href="#tabs6-1">1</a> + <ul> + <li><a href="#item6-3">3</a></li> + <li><a href="#item6-4">4</a></li> + </ul> + </li> + <li><a href="#tabs6-2">2</a></li> + </ul> + <div id="tabs6-1"></div> + <div id="tabs6-2"></div> + </div> + <div id="tabs7"> + <ul id="tabs7-list"> + <li><a href="#tabs7-1">1</a></li> + <li><a href="#tabs7-2">2</a></li> + </ul> + <div id="tabs7-2"></div> + <div id="tabs7-1"></div> + </div> + </div> +</body> +</html> diff --git a/tests/unit/tabs/tabs_deprecated.js b/tests/unit/tabs/tabs_deprecated.js new file mode 100644 index 000000000..b784c724e --- /dev/null +++ b/tests/unit/tabs/tabs_deprecated.js @@ -0,0 +1,343 @@ +(function( $ ) { + +module("tabs (deprecated): core"); + +test( "#4581 - title attribute for remote tabs does not support foreign languages", function() { + expect( 1 ); + + $( "#tabs2" ).tabs({ + selected: 3, + beforeload: function( event, ui ) { + event.preventDefault(); + equal( ui.panel.id, "∫ßáö_Սե", "proper title" ); + } + }); +}); + +module("tabs (deprecated): options"); + +test('ajaxOptions', function() { + ok(false, "missing test - untested code is broken code."); +}); + +test('cache', function() { + ok(false, "missing test - untested code is broken code."); +}); + +test('idPrefix', function() { + ok(false, "missing test - untested code is broken code."); +}); + +test('tabTemplate', function() { + ok(false, "missing test - untested code is broken code."); +}); + +test('panelTemplate', function() { + ok(false, "missing test - untested code is broken code."); +}); + +test('cookie', function() { + expect(6); + + el = $('#tabs1'); + var cookieName = 'tabs_test', cookieObj = { name: cookieName }; + $.cookie(cookieName, null); // blank state + var cookie = function() { + return parseInt($.cookie(cookieName), 10); + }; + + el.tabs({ cookie: cookieObj }); + equals(cookie(), 0, 'initial cookie value'); + + el.tabs('destroy'); + el.tabs({ active: 1, cookie: cookieObj }); + equals(cookie(), 1, 'initial cookie value, from active property'); + + el.tabs('option', 'active', 2); + equals(cookie(), 2, 'cookie value updated after activating'); + + el.tabs('destroy'); + $.cookie(cookieName, 1); + el.tabs({ cookie: cookieObj }); + equals(cookie(), 1, 'initial cookie value, from existing cookie'); + + el.tabs('destroy'); + el.tabs({ cookie: cookieObj, collapsible: true }); + el.tabs('option', 'active', false); + equals(cookie(), -1, 'cookie value for all tabs unselected'); + + el.tabs('destroy'); + ok($.cookie(cookieName) === null, 'erase cookie after destroy'); + +}); + + +test('spinner', function() { + expect(4); + stop(); + + el = $('#tabs2'); + + el.tabs({ + selected: 2, + load: function() { + // spinner: default spinner + setTimeout(function() { + 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: '<img src="spinner.gif" alt="">', + 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(); + } + }); + }, 1); + } + }); +}); + +test('selected', function() { + expect(11); + + el = $('#tabs1').tabs(); + equals(el.tabs('option', 'selected'), 0, 'should be 0 by default'); + + el.tabs('destroy'); + //set a hash in the url + location.hash = '#fragment-2'; + //selection of tab with divs ordered differently than list + el = $('#tabs1').tabs(); + equals(el.tabs('option', 'selected'), 1, 'second tab should be selected'); + + el.tabs('destroy'); + //set a hash in the url + location.hash = '#tabs7-2'; + //selection of tab with divs ordered differently than list + el = $('#tabs7').tabs(); + equals(el.tabs('option', 'selected'), 1, 'second tab should be selected'); + + el.tabs('destroy'); + el = $('#tabs1').tabs({ selected: -1 }); + equals(el.tabs('option', 'selected'), -1, 'should be -1 for all tabs unselected'); + equals( $('li.ui-tabs-selected', el).length, 0, 'no tab should be selected' ); + equals( $('div:hidden', '#tabs1').length, 3, 'all panels should be hidden' ); + + el.tabs('destroy'); + el.tabs({ selected: null }); + equals(el.tabs('option', 'selected'), -1, 'should be -1 for all tabs unselected with value null (deprecated)'); + + el.tabs('destroy'); + el.tabs({ selected: 1 }); + equals(el.tabs('option', 'selected'), 1, 'should be specified tab'); + + el.tabs('destroy'); + el.tabs({ selected: 99 }); + equals(el.tabs('option', 'selected'), 0, 'selected should default to zero if given value is out of index'); + + el.tabs('destroy'); + el.tabs({ collapsible: true }); + el.tabs('option', 'selected', 0); + equals(el.tabs('option', 'selected'), 0, 'should not collapse tab if value is same as selected'); + + el.tabs('destroy'); + el = $('#tabs1').tabs(); + el.tabs('select', 1); + equals(el.tabs('option', 'selected'), 1, 'should select tab'); +}); + +module("tabs (deprecated): events"); + +test('enable', function() { + expect(4); + + var uiObj; + el = $('#tabs1').tabs({ + disabled: [ 0, 1 ], + enable: function (event, ui) { + uiObj = ui; + } + }); + el.tabs('enable', 1); + ok(uiObj !== undefined, 'trigger callback'); + equals(uiObj.tab, $('a', el)[1], 'contain tab as DOM anchor element'); + equals(uiObj.panel, $('div', el)[1], 'contain panel as DOM div element'); + equals(uiObj.index, 1, 'contain index'); +}); + +test('disable', function() { + expect(4); + + var uiObj; + el = $('#tabs1').tabs({ + disable: function (event, ui) { + uiObj = ui; + } + }); + el.tabs('disable', 1); + ok(uiObj !== undefined, 'trigger callback'); + equals(uiObj.tab, $('a', el)[1], 'contain tab as DOM anchor element'); + equals(uiObj.panel, $('div', el)[1], 'contain panel as DOM div element'); + equals(uiObj.index, 1, 'contain index'); +}); + +test('add', function() { + + // TODO move to methods, not at all event related... + + var el = $('<div id="tabs"><ul></ul></div>').tabs(); + equals(el.tabs('option', 'selected'), -1, 'Initially empty, no selected tab'); + + el.tabs('add', '#test1', 'Test 1'); + equals(el.tabs('option', 'selected'), 0, 'First tab added should be auto selected'); + + el.tabs('add', '#test2', 'Test 2'); + equals(el.tabs('option', 'selected'), 0, 'Second tab added should not be auto selected'); + +}); + +test('remove', function() { + ok(false, "missing test - untested code is broken code."); +}); + +test('show', function() { + expect(5); + + var uiObj, eventObj; + el = $('#tabs1').tabs({ + show: function(event, ui) { + uiObj = ui; + eventObj = event; + } + }); + ok(uiObj !== undefined, 'trigger callback after initialization'); + equals(uiObj.tab, $('a', el)[0], 'contain tab as DOM anchor element'); + equals(uiObj.panel, $('div', el)[0], 'contain panel as DOM div element'); + equals(uiObj.index, 0, 'contain index'); + + el.find( "li:eq(1) a" ).simulate( "click" ); + equals( eventObj.originalEvent.type, "click", "show triggered by click" ); + +}); + +test('select', function() { + expect(7); + + var eventObj; + el = $('#tabs1').tabs({ + select: function(event, ui) { + ok(true, 'select triggered after initialization'); + equals(this, el[0], "context of callback"); + equals(event.type, 'tabsselect', 'event type in callback'); + equals(ui.tab, el.find('a')[1], 'contain tab as DOM anchor element'); + equals(ui.panel, el.find('div')[1], 'contain panel as DOM div element'); + equals(ui.index, 1, 'contain index'); + evenObj = event; + } + }); + el.tabs('select', 1); + + el.find( "li:eq(1) a" ).simulate( "click" ); + equals( evenObj.originalEvent.type, "click", "select triggered by click" ); +}); + +module("tabs (deprecated): methods"); + +test('add', function() { + expect(4); + + el = $('#tabs1').tabs(); + el.tabs('add', '#new', 'New'); + + var added = $('li:last', el).simulate('mouseover'); + ok(added.is('.ui-state-hover'), 'should add mouseover handler to added tab'); + added.simulate('mouseout'); + var other = $('li:first', el).simulate('mouseover'); + ok(other.is('.ui-state-hover'), 'should not remove mouseover handler from existing tab'); + other.simulate('mouseout'); + + equals($('a', added).attr('href'), '#new', 'should not expand href to full url of current page'); + + ok(false, "missing test - untested code is broken code."); +}); + +test('remove', function() { + expect(4); + + el = $('#tabs1').tabs(); + + el.tabs('remove', 0); + equals(el.tabs('length'), 2, 'remove tab'); + equals($('li a[href$="fragment-1"]', el).length, 0, 'remove associated list item'); + equals($('#fragment-1').length, 0, 'remove associated panel'); + + // TODO delete tab -> focus tab to right + // TODO delete last tab -> focus tab to left + + el.tabs('select', 1); + el.tabs('remove', 1); + equals(el.tabs('option', 'selected'), 0, 'update selected property'); +}); + +test('select', function() { + expect(6); + + el = $('#tabs1').tabs(); + + el.tabs('select', 1); + equals(el.tabs('option', 'active'), 1, 'should select tab'); + + el.tabs('destroy'); + el.tabs({ collapsible: true }); + el.tabs('select', 0); + equals(el.tabs('option', 'active'), -1, 'should collapse tab passing in the already active tab'); + + el.tabs('destroy'); + el.tabs({ collapsible: true }); + el.tabs('select', -1); + equals(el.tabs('option', 'active'), -1, 'should collapse tab passing in -1'); + + el.tabs('destroy'); + el.tabs(); + el.tabs('select', 0); + equals(el.tabs('option', 'active'), 0, 'should not collapse tab if collapsible is not set to true'); + el.tabs('select', -1); + equals(el.tabs('option', 'active'), 0, 'should not collapse tab if collapsible is not set to true'); + + el.tabs('select', '#fragment-2'); + equals(el.tabs('option', 'active'), 1, 'should select tab by id'); +}); + + +test('#5069 - ui.tabs.add creates two tab panels when using a full URL', function() { + // http://dev.jqueryui.com/ticket/5069 + expect(2); + + el = $('#tabs2').tabs(); + equals(el.children('div').length, el.find('> ul > li').length, 'After creation, number of panels should be equal to number of tabs'); + el.tabs('add', '/ajax_html_echo', 'Test'); + equals(el.children('div').length, el.find('> ul > li').length, 'After add, number of panels should be equal to number of tabs'); +}); + +test('length', function() { + expect(1); + + el = $('#tabs1').tabs(); + equals(el.tabs('length'), $('ul a', el).length, ' should return length'); +}); + +test('url', function() { + el = $('#tabs2').tabs(); + var tab = el.find('a:eq(3)'), + url = tab.attr('href'); + + el.tabs('url', 3, "data/test2.html"); + equals(tab.attr('href'), 'data/test2.html', 'Url was updated'); + tab.attr('href', url ); +}); + +}( jQuery ) ); diff --git a/tests/unit/tabs/tabs_events.js b/tests/unit/tabs/tabs_events.js index 24fb62f9b..381326533 100644 --- a/tests/unit/tabs/tabs_events.js +++ b/tests/unit/tabs/tabs_events.js @@ -5,37 +5,59 @@ module("tabs: events"); -test('select', function() { +test('beforeActivate', function() { expect(7); - var eventObj; el = $('#tabs1').tabs({ - select: function(event, ui) { - ok(true, 'select triggered after initialization'); + beforeActivate: function(event, ui) { + ok(true, 'beforeActivate triggered after initialization'); equals(this, el[0], "context of callback"); - equals(event.type, 'tabsselect', 'event type in callback'); + equals(event.type, 'tabsbeforeactivate', 'event type in callback'); equals(ui.tab, el.find('a')[1], 'contain tab as DOM anchor element'); equals(ui.panel, el.find('div')[1], 'contain panel as DOM div element'); equals(ui.index, 1, 'contain index'); - evenObj = event; } }); - el.tabs('select', 1); + el.tabs('option', 'active', 1); + el.tabs('destroy'); + el.tabs({ + beforeActivate: function(event, ui) { + equals( event.originalEvent.type, "click", "beforeActivate triggered by click" ); + } + }); el.find( "li:eq(1) a" ).simulate( "click" ); - equals( evenObj.originalEvent.type, "click", "select triggered by click" ); +}); + +test('beforeload', function() { + expect( 5 ); + + el = $('#tabs2'); + + el.tabs({ + active: 2, + beforeload: function( event, ui ) { + ok( $.isFunction( ui.jqXHR.promise ), 'contain jqXHR object'); + equals( ui.settings.url, "data/test.html", 'contain ajax settings url'); + equals( ui.tab, el.find('a')[ 2 ], 'contain tab as DOM anchor element'); + equals( ui.panel, el.find('div')[ 2 ], 'contain panel as DOM div element'); + equals( ui.index, 2, 'contain index'); + event.preventDefault(); + } + }); + }); test('load', function() { ok(false, "missing test - untested code is broken code."); }); -test('show', function() { +test('activate', function() { expect(5); var uiObj, eventObj; el = $('#tabs1').tabs({ - show: function(event, ui) { + activate: function(event, ui) { uiObj = ui; eventObj = event; } @@ -50,56 +72,4 @@ test('show', function() { }); -test('add', function() { - - // TODO move to methods, not at all event related... - - var el = $('<div id="tabs"><ul></ul></div>').tabs(); - equals(el.tabs('option', 'selected'), -1, 'Initially empty, no selected tab'); - - el.tabs('add', '#test1', 'Test 1'); - equals(el.tabs('option', 'selected'), 0, 'First tab added should be auto selected'); - - el.tabs('add', '#test2', 'Test 2'); - equals(el.tabs('option', 'selected'), 0, 'Second tab added should not be auto selected'); - -}); - -test('remove', function() { - ok(false, "missing test - untested code is broken code."); -}); - -test('enable', function() { - expect(4); - - var uiObj; - el = $('#tabs1').tabs({ - disabled: [ 0, 1 ], - enable: function (event, ui) { - uiObj = ui; - } - }); - el.tabs('enable', 1); - ok(uiObj !== undefined, 'trigger callback'); - equals(uiObj.tab, $('a', el)[1], 'contain tab as DOM anchor element'); - equals(uiObj.panel, $('div', el)[1], 'contain panel as DOM div element'); - equals(uiObj.index, 1, 'contain index'); -}); - -test('disable', function() { - expect(4); - - var uiObj; - el = $('#tabs1').tabs({ - disable: function (event, ui) { - uiObj = ui; - } - }); - el.tabs('disable', 1); - ok(uiObj !== undefined, 'trigger callback'); - equals(uiObj.tab, $('a', el)[1], 'contain tab as DOM anchor element'); - equals(uiObj.panel, $('div', el)[1], 'contain panel as DOM div element'); - equals(uiObj.index, 1, 'contain index'); -}); - })(jQuery); diff --git a/tests/unit/tabs/tabs_methods.js b/tests/unit/tabs/tabs_methods.js index 0209af697..c33bebe65 100644 --- a/tests/unit/tabs/tabs_methods.js +++ b/tests/unit/tabs/tabs_methods.js @@ -14,11 +14,11 @@ test('init', function() { ok( el.is('.ui-tabs.ui-widget.ui-widget-content.ui-corner-all'), 'attach classes to container'); ok( $('ul', el).is('.ui-tabs-nav.ui-helper-reset.ui-helper-clearfix.ui-widget-header.ui-corner-all'), 'attach classes to list' ); ok( $('div:eq(0)', el).is('.ui-tabs-panel.ui-widget-content.ui-corner-bottom'), 'attach classes to panel' ); - ok( $('li:eq(0)', el).is('.ui-tabs-selected.ui-state-active.ui-corner-top'), 'attach classes to active li'); + ok( $('li:eq(0)', el).is('.ui-tabs-active.ui-state-active.ui-corner-top'), 'attach classes to active li'); ok( $('li:eq(1)', el).is('.ui-state-default.ui-corner-top'), 'attach classes to inactive li'); - equals( el.tabs('option', 'selected'), 0, 'selected option set' ); - equals( $('li', el).index( $('li.ui-tabs-selected', el) ), 0, 'second tab active'); - equals( $('div', el).index( $('div.ui-tabs-hide', '#tabs1') ), 1, 'second panel should be hidden' ); + equals( el.tabs('option', 'active'), 0, 'active option set' ); + equals( $('li', el).index( $('li.ui-tabs-active', el) ), 0, 'second tab active'); + equals( $('div', el).index( $('div:hidden', '#tabs1') ), 1, 'second panel should be hidden' ); }); test('init with hash', function() { @@ -30,13 +30,13 @@ test('init with hash', function() { //selection of tab with divs ordered differently than list el = $('#tabs1').tabs(); - equals(el.tabs('option', 'selected'), 1, 'second tab should be selected'); + equals(el.tabs('option', 'active'), 1, 'second tab should be active'); - ok(!$('#tabs1 ul li:eq(0)').is('.ui-tabs-selected.ui-state-active'), 'first tab should not be selected nor active'); - ok($('#tabs1 div:eq(0)').is('.ui-tabs-hide'), 'first div for first tab should be hidden'); + ok(!$('#tabs1 ul li:eq(0)').is('.ui-tabs-active.ui-state-active'), 'first tab should not be selected nor active'); + ok($('#tabs1 div:eq(0)').is(':hidden'), 'first div for first tab should be hidden'); - ok($('#tabs1 ul li:eq(1)').is('.ui-tabs-selected.ui-state-active'), 'second tab should be selected and active'); - ok(!$('#tabs1 div:eq(1)').is('.ui-tabs-hide'), 'second div for second tab should not be hidden'); + ok($('#tabs1 ul li:eq(1)').is('.ui-tabs-active.ui-state-active'), 'second tab should be selected and active'); + ok(!$('#tabs1 div:eq(1)').is(':hidden'), 'second div for second tab should not be hidden'); }); test('init mismatched order with hash', function() { @@ -48,13 +48,13 @@ test('init mismatched order with hash', function() { //selection of tab with divs ordered differently than list el = $('#tabs7').tabs(); - equals(el.tabs('option', 'selected'), 1, 'second tab should be selected'); + equals(el.tabs('option', 'active'), 1, 'second tab should be active'); - ok(!$('#tabs7-list li:eq(0)').is('.ui-tabs-selected.ui-state-active'), 'first tab should not be selected nor active'); - ok($('#tabs7 div:eq(1)').is('.ui-tabs-hide'), 'second div for first tab should be hidden'); + ok(!$('#tabs7-list li:eq(0)').is('.ui-tabs-active.ui-state-active'), 'first tab should not be selected nor active'); + ok($('#tabs7 div:eq(1)').is(':hidden'), 'second div for first tab should be hidden'); - ok($('#tabs7-list li:eq(1)').is('.ui-tabs-selected.ui-state-active'), 'second tab should be selected and active'); - ok(!$('#tabs7 div:eq(0)').is('.ui-tabs-hide'), 'first div for second tab should not be hidden'); + ok($('#tabs7-list li:eq(1)').is('.ui-tabs-active.ui-state-active'), 'second tab should be selected and active'); + ok(!$('#tabs7 div:eq(0)').is(':hidden'), 'first div for second tab should not be hidden'); }); test('destroy', function() { @@ -66,8 +66,8 @@ test('destroy', function() { ok( el.is(':not(.ui-tabs, .ui-widget, .ui-widget-content, .ui-corner-all, .ui-tabs-collapsible)'), 'remove classes from container'); ok( $('ul', el).is(':not(.ui-tabs-nav, .ui-helper-reset, .ui-helper-clearfix, .ui-widget-header, .ui-corner-all)'), 'remove classes from list' ); - ok( $('div:eq(1)', el).is(':not(.ui-tabs-panel, .ui-widget-content, .ui-corner-bottom, .ui-tabs-hide)'), 'remove classes to panel' ); - ok( $('li:eq(0)', el).is(':not(.ui-tabs-selected, .ui-state-active, .ui-corner-top)'), 'remove classes from active li'); + ok( $('div:eq(1)', el).is(':not(.ui-tabs-panel, .ui-widget-content, .ui-corner-bottom)'), 'remove classes to panel' ); + ok( $('li:eq(0)', el).is(':not(.ui-tabs-active, .ui-state-active, .ui-corner-top)'), 'remove classes from active li'); ok( $('li:eq(1)', el).is(':not(.ui-state-default, .ui-corner-top)'), 'remove classes from inactive li'); ok( $('li:eq(2)', el).is(':not(.ui-state-hover, .ui-state-focus)'), 'remove classes from mouseovered or focused li'); }); @@ -130,84 +130,36 @@ test('disable', function() { same(el.tabs('option', 'disabled'), true, 'set to true'); }); -test('add', function() { - expect(4); - - el = $('#tabs1').tabs(); - el.tabs('add', '#new', 'New'); - - var added = $('li:last', el).simulate('mouseover'); - ok(added.is('.ui-state-hover'), 'should add mouseover handler to added tab'); - added.simulate('mouseout'); - var other = $('li:first', el).simulate('mouseover'); - ok(other.is('.ui-state-hover'), 'should not remove mouseover handler from existing tab'); - other.simulate('mouseout'); - - equals($('a', added).attr('href'), '#new', 'should not expand href to full url of current page'); - - ok(false, "missing test - untested code is broken code."); -}); - -test('remove', function() { - expect(4); - - el = $('#tabs1').tabs(); - - el.tabs('remove', 0); - equals(el.tabs('length'), 2, 'remove tab'); - equals($('li a[href$="fragment-1"]', el).length, 0, 'remove associated list item'); - equals($('#fragment-1').length, 0, 'remove associated panel'); - - // TODO delete tab -> focus tab to right - // TODO delete last tab -> focus tab to left - - el.tabs('select', 1); - el.tabs('remove', 1); - equals(el.tabs('option', 'selected'), 0, 'update selected property'); -}); - -test('select', function() { - expect(6); +test('refresh', function() { + expect(5); - el = $('#tabs1').tabs(); + var el = $('<div id="tabs"><ul></ul></div>').tabs(), + ul = el.find('ul'); - el.tabs('select', 1); - equals(el.tabs('option', 'selected'), 1, 'should select tab'); + equals(el.tabs('option', 'active'), -1, 'Initially empty, no active tab'); - el.tabs('destroy'); - el.tabs({ collapsible: true }); - el.tabs('select', 0); - equals(el.tabs('option', 'selected'), -1, 'should collapse tab passing in the already selected tab'); + ul.append('<li><a href="data/test.html">Test 1</a></li>'); + el.tabs('refresh'); + equals( el.find('.ui-tabs-panel').length, 1, 'Panel created after refresh'); - el.tabs('destroy'); - el.tabs({ collapsible: true }); - el.tabs('select', -1); - equals(el.tabs('option', 'selected'), -1, 'should collapse tab passing in -1'); + ul.find('li').remove(); + el.tabs('refresh'); + equals( el.find('.ui-tabs-panel').length, 0, 'Panel removed after refresh'); - el.tabs('destroy'); - el.tabs(); - el.tabs('select', 0); - equals(el.tabs('option', 'selected'), 0, 'should not collapse tab if collapsible is not set to true'); - el.tabs('select', -1); - equals(el.tabs('option', 'selected'), 0, 'should not collapse tab if collapsible is not set to true'); + ul.append('<li><a href="#test1">Test 1</a></li>'); + $('<div id="test1">Test Panel 1</div>').insertAfter( ul ); + el.tabs('refresh'); + el.tabs('option', 'active', 0); + equals( el.tabs('option', 'active'), 0, 'First tab added should be auto active'); - el.tabs('select', '#fragment-2'); - equals(el.tabs('option', 'selected'), 1, 'should select tab by id'); + ul.append('<li><a href="#test2">Test 2</a></li>'); + $('<div id="test2">Test Panel 2</div>').insertAfter( ul ); + el.tabs('refresh'); + equals( el.tabs('option', 'active'), 0, 'Second tab added should not be auto active'); }); test('load', function() { ok(false, "missing test - untested code is broken code."); }); -test('url', function() { - ok(false, "missing test - untested code is broken code."); -}); - -test('length', function() { - expect(1); - - el = $('#tabs1').tabs(); - equals(el.tabs('length'), $('ul a', el).length, ' should return length'); -}); - })(jQuery); diff --git a/tests/unit/tabs/tabs_options.js b/tests/unit/tabs/tabs_options.js index 1c621ac28..98cd3b4ca 100644 --- a/tests/unit/tabs/tabs_options.js +++ b/tests/unit/tabs/tabs_options.js @@ -5,14 +5,6 @@ module("tabs: options"); -test('ajaxOptions', function() { - ok(false, "missing test - untested code is broken code."); -}); - -test('cache', function() { - ok(false, "missing test - untested code is broken code."); -}); - test('collapsible', function() { expect(4); @@ -21,45 +13,12 @@ test('collapsible', function() { el.tabs({ collapsible: true }); equals(el.tabs('option', 'collapsible'), true, 'option set'); ok(el.is('.ui-tabs-collapsible'), 'extra class "ui-tabs-collapsible" attached'); - el.tabs('select', 0); - equals($('div.ui-tabs-hide', '#tabs1').length, 3, 'all panels should be hidden'); - el.tabs('option', 'collapsible', false); - ok(el.is(':not(.ui-tabs-collapsible)'), 'extra class "ui-tabs-collapsible" not attached'); - -}); - -test('cookie', function() { - expect(6); - - el = $('#tabs1'); - var cookieName = 'tabs_test', cookieObj = { name: cookieName }; - $.cookie(cookieName, null); // blank state - var cookie = function() { - return parseInt($.cookie(cookieName), 10); - }; - - el.tabs({ cookie: cookieObj }); - equals(cookie(), 0, 'initial cookie value'); - el.tabs('destroy'); - el.tabs({ selected: 1, cookie: cookieObj }); - equals(cookie(), 1, 'initial cookie value, from selected property'); - - el.tabs('select', 2); - equals(cookie(), 2, 'cookie value updated after select'); - - el.tabs('destroy'); - $.cookie(cookieName, 1); - el.tabs({ cookie: cookieObj }); - equals(cookie(), 1, 'initial cookie value, from existing cookie'); + el.tabs('option', 'active', false); + equals($('div:hidden', '#tabs1').length, 3, 'all panels should be hidden'); - el.tabs('destroy'); - el.tabs({ cookie: cookieObj, collapsible: true }); - el.tabs('select', 0); - equals(cookie(), -1, 'cookie value for all tabs unselected'); - - el.tabs('destroy'); - ok($.cookie(cookieName) === null, 'erase cookie after destroy'); + el.tabs('option', 'collapsible', false); + ok(el.is(':not(.ui-tabs-collapsible)'), 'extra class "ui-tabs-collapsible" not attached'); }); @@ -87,50 +46,34 @@ test('fx', function() { ok(false, "missing test - untested code is broken code."); }); -test('idPrefix', function() { - ok(false, "missing test - untested code is broken code."); -}); - -test('panelTemplate', function() { - ok(false, "missing test - untested code is broken code."); -}); - -test('selected', function() { +test('active', function() { expect(8); el = $('#tabs1').tabs(); - equals(el.tabs('option', 'selected'), 0, 'should be 0 by default'); + equals(el.tabs('option', 'active'), 0, 'should be 0 by default'); el.tabs('destroy'); - el.tabs({ selected: -1 }); - equals(el.tabs('option', 'selected'), -1, 'should be -1 for all tabs unselected'); - equals( $('li.ui-tabs-selected', el).length, 0, 'no tab should be selected' ); - equals( $('div.ui-tabs-hide', '#tabs1').length, 3, 'all panels should be hidden' ); + el.tabs({ active: -1 }); + equals(el.tabs('option', 'active'), -1, 'should be -1 for all tabs deactive'); + equals( $('li.ui-tabs-selected', el).length, 0, 'no tab should be active' ); + equals( $('div:hidden', '#tabs1').length, 3, 'all panels should be hidden' ); el.tabs('destroy'); - el.tabs({ selected: null }); - equals(el.tabs('option', 'selected'), -1, 'should be -1 for all tabs unselected with value null (deprecated)'); + el.tabs({ active: null }); + equals(el.tabs('option', 'active'), -1, 'should be -1 for all tabs deactive with value null (deprecated)'); el.tabs('destroy'); - el.tabs({ selected: 1 }); - equals(el.tabs('option', 'selected'), 1, 'should be specified tab'); + el.tabs({ active: 1 }); + equals(el.tabs('option', 'active'), 1, 'should be specified tab'); el.tabs('destroy'); - el.tabs({ selected: 99 }); - equals(el.tabs('option', 'selected'), 0, 'selected should default to zero if given value is out of index'); + el.tabs({ active: 99 }); + equals(el.tabs('option', 'active'), 0, 'active should default to zero if given value is out of index'); el.tabs('destroy'); el.tabs({ collapsible: true }); - el.tabs('option', 'selected', 0); - equals(el.tabs('option', 'selected'), 0, 'should not collapse tab if value is same as selected'); -}); - -test('spinner', function() { - ok(false, "missing test - untested code is broken code."); -}); - -test('tabTemplate', function() { - ok(false, "missing test - untested code is broken code."); + el.tabs('option', 'active', 0); + equals(el.tabs('option', 'active'), 0, 'should not collapse tab if value is same as active'); }); })(jQuery); diff --git a/tests/unit/tabs/tabs_tickets.js b/tests/unit/tabs/tabs_tickets.js index 4a09d59e4..36ddd6e84 100644 --- a/tests/unit/tabs/tabs_tickets.js +++ b/tests/unit/tabs/tabs_tickets.js @@ -13,7 +13,7 @@ test('#2715 - id containing colon', function() { ok( $('div.ui-tabs-panel:eq(0)', '#tabs2').is(':visible'), 'first panel should be visible' ); ok( $('div.ui-tabs-panel:eq(1)', '#tabs2').is(':hidden'), 'second panel should be hidden' ); - el.tabs('select', 1).tabs('select', 0); + el.tabs('option', 'active', 1).tabs('option', 'active', 0); ok( $('div.ui-tabs-panel:eq(0)', '#tabs2').is(':visible'), 'first panel should be visible' ); ok( $('div.ui-tabs-panel:eq(1)', '#tabs2').is(':hidden'), 'second panel should be hidden' ); @@ -30,10 +30,10 @@ test('#???? - panel containing inline style', function() { el = $('#tabs2').tabs(); equals(inlineStyle('height'), expected, 'init should not remove inline style'); - el.tabs('select', 1); + el.tabs('option', 'active', 1); equals(inlineStyle('height'), expected, 'show tab should not remove inline style'); - el.tabs('select', 0); + el.tabs('option', 'active', 0); equals(inlineStyle('height'), expected, 'hide tab should not remove inline style'); }); @@ -42,32 +42,29 @@ test('#3627 - Ajax tab with url containing a fragment identifier fails to load', // http://dev.jqueryui.com/ticket/3627 expect(1); - el = $('#tabs2').tabs(); - - ok(/test.html$/.test( $('a:eq(2)', el).data('load.tabs') ), 'should ignore fragment identifier'); - + el = $('#tabs2').tabs({ + active: 2, + beforeload: function( event, ui ) { + event.preventDefault(); + ok(/test.html$/.test( ui.settings.url ), 'should ignore fragment identifier'); + } + }); }); test('#4033 - IE expands hash to full url and misinterprets tab as ajax', function() { // http://dev.jqueryui.com/ticket/4033 expect(1); - el = $('<div><ul><li><a href="#tab">Tab</a></li></ul><div id="tab"></div></div>') - .appendTo('#main').tabs(); - - equals($('a', el).data('load.tabs'), undefined, 'should not create ajax tab'); - -}); + el = $('<div><ul><li><a href="#tab">Tab</a></li></ul><div id="tab"></div></div>'); + el.appendTo('#main'); + el.tabs({ + beforeload: function( event, ui ) { + event.preventDefault(); + ok( false, 'should not be an ajax tab'); + } + }); -test('#5069 - ui.tabs.add creates two tab panels when using a full URL', function() { - // http://dev.jqueryui.com/ticket/5069 - expect(2); - - el = $('#tabs2').tabs(); - equals(el.children('div').length, el.find('> ul > li').length, 'After creation, number of panels should be equal to number of tabs'); - el.tabs('add', '/ajax_html_echo', 'Test'); - equals(el.children('div').length, el.find('> ul > li').length, 'After add, number of panels should be equal to number of tabs'); - + equals($('a', el).attr('aria-controls'), '#tab', 'aria-contorls attribute is correct'); }); test('#5893 - Sublist in the tab list are considered as tab', function() { @@ -75,23 +72,10 @@ test('#5893 - Sublist in the tab list are considered as tab', function() { expect(1); el = $('#tabs6').tabs(); - equals(el.tabs( "length" ), 2, 'should contain 2 tab'); + equals(el.data("tabs").anchors.length, 2, 'should contain 2 tab'); }); -asyncTest( "#4581 - title attribute for remote tabs does not support foreign languages", function() { - expect( 1 ); - - $( "#tabs2" ).tabs({ - selected: 3, - load: function( event, ui ) { - equal( ui.panel.id, "∫ßáö_Սե", "proper title" ); - start(); - } - }); -}); - - test('#6710 - selectors are global', function() { // http://bugs.jqueryui.com/ticket/6710 expect(1); @@ -108,7 +92,7 @@ test('#6710 - selectors are global', function() { </div>\ </div>'); container.find('#tabs_6710').tabs(); - ok( container.find('#tabs-2_6710').hasClass('ui-tabs-hide'), 'should find panels and add corresponding classes' ); + ok( container.find('#tabs-2_6710').is(':hidden'), 'should find panels and add corresponding classes' ); }); diff --git a/themes/base/jquery.ui.tabs.css b/themes/base/jquery.ui.tabs.css index 12666facd..d4c039f30 100644 --- a/themes/base/jquery.ui.tabs.css +++ b/themes/base/jquery.ui.tabs.css @@ -11,8 +11,7 @@ .ui-tabs .ui-tabs-nav { margin: 0; padding: .2em .2em 0; } .ui-tabs .ui-tabs-nav li { list-style: none; float: left; position: relative; top: 1px; margin: 0 .2em 1px 0; border-bottom: 0 !important; padding: 0; white-space: nowrap; } .ui-tabs .ui-tabs-nav li a { float: left; padding: .5em 1em; text-decoration: none; } -.ui-tabs .ui-tabs-nav li.ui-tabs-selected { margin-bottom: 0; padding-bottom: 1px; } -.ui-tabs .ui-tabs-nav li.ui-tabs-selected a, .ui-tabs .ui-tabs-nav li.ui-state-disabled 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-nav li.ui-tabs-active { margin-bottom: 0; padding-bottom: 1px; } +.ui-tabs .ui-tabs-nav li.ui-tabs-active a, .ui-tabs .ui-tabs-nav li.ui-state-disabled a, .ui-tabs .ui-tabs-nav li.ui-tabs-loading a { cursor: text; } +.ui-tabs .ui-tabs-nav li a, .ui-tabs.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active 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 { display: block; border-width: 0; padding: 1em 1.4em; background: none; } -.ui-tabs .ui-tabs-hide { display: none !important; } diff --git a/ui/jquery.ui.tabs.js b/ui/jquery.ui.tabs.js index fcdf22c0e..f9926ebef 100755 --- a/ui/jquery.ui.tabs.js +++ b/ui/jquery.ui.tabs.js @@ -26,56 +26,112 @@ function getNextListId() { $.widget( "ui.tabs", { options: { - add: null, - ajaxOptions: null, - cache: false, - cookie: null, // e.g. { expires: 7, path: '/', domain: 'jquery.com', secure: true } + activate: null, + beforeload: null, + beforeActivate: null, collapsible: false, - disable: null, disabled: false, - 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>" + load: null }, _create: function() { - this._tabify( true ); + var self = this, + o = this.options; + + this.running = false; + + this.element.addClass( "ui-tabs ui-widget ui-widget-content ui-corner-all" ); + + this._processTabs(); + + // Selected tab + // use "selected" option or try to retrieve: + // 1. from fragment identifier in url + // 2. from selected class attribute on <li> + if ( o.active === undefined ) { + if ( location.hash ) { + this.anchors.each(function( i, a ) { + if ( a.hash == location.hash ) { + o.active = i; + return false; + } + }); + } + if ( typeof o.active !== "number" && this.lis.filter( ".ui-tabs-active" ).length ) { + o.active = this.lis.index( this.lis.filter( ".ui-tabs-active" ) ); + } + o.active = o.active || ( this.lis.length ? 0 : -1 ); + } else if ( o.active === null ) { // usage of null is deprecated, TODO remove in next release + o.active = -1; + } + + // sanity check - default to first tab... + o.active = ( ( o.active >= 0 && this.anchors[ o.active ] ) || o.active < 0 ) + ? o.active + : 0; + + // Take disabling tabs via class attribute from HTML + // into account and update option properly. + if ( $.isArray( o.disabled ) ) { + o.disabled = $.unique( o.disabled.concat( + $.map( this.lis.filter( ".ui-state-disabled" ), function( n, i ) { + return self.lis.index( n ); + }) + ) ).sort(); + } + + this._setupFx( o.fx ); + + this._refresh(); + + // highlight selected tab + this.panels.hide(); + this.lis.removeClass( "ui-tabs-active ui-state-active" ); + // check for length avoids error when initializing empty list + if ( o.active >= 0 && this.anchors.length ) { + this.active = this._findActive( o.active ); + var panel = self.element.find( self._sanitizeSelector( this.active.attr( "aria-controls" ) ) ); + + panel.show(); + + this.lis.eq( o.active ).addClass( "ui-tabs-active ui-state-active" ); + + // seems to be expected behavior that the activate callback is fired + self.element.queue( "tabs", function() { + self._trigger( "activate", null, self._ui( self.active[ 0 ], panel[ 0 ] ) ); + }); + + this.load( o.active ); + } + + // clean up to avoid memory leaks in certain versions of IE 6 + $( window ).bind( "unload.tabs", function() { + self.lis.add( self.anchors ).unbind( ".tabs" ); + self.lis = self.anchors = self.panels = null; + }); }, _setOption: function( key, value ) { - if ( key == "selected" ) { - if (this.options.collapsible && value == this.options.selected ) { - return; - } - this.select( value ); + if ( key == "active" ) { + // _activate() will handle invalid values and update this.option + this._activate( value ); + return } else { this.options[ key ] = value; - this._tabify(); + this.refresh(); } }, _tabId: function( a ) { - return a.title && a.title.replace( /\s/g, "_" ).replace( /[^\w\u00c0-\uFFFF-]/g, "" ) || - this.options.idPrefix + getNextTabId(); + return ( $( a ).attr( "aria-controls" ) || "" ).replace( /^#/ , "" ) || + "ui-tabs-" + getNextTabId(); }, _sanitizeSelector: function( hash ) { // we need this because an id may contain a ":" - return hash.replace( /:/g, "\\:" ); - }, - - _cookie: function() { - var cookie = this.cookie || - ( this.cookie = this.options.cookie.name || "ui-tabs-" + getNextListId() ); - return $.cookie.apply( null, [ cookie ].concat( $.makeArray( arguments ) ) ); + return hash ? hash.replace( /:/g, "\\:" ) : ""; }, _ui: function( tab, panel ) { @@ -86,20 +142,59 @@ $.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" ) ).removeData( "label.tabs" ); - }); + refresh: function() { + var self = this; + + this._processTabs(); + + this._refresh(); + + // Remove panels that we created that are missing their tab + this.element.find(".ui-tabs-panel:data(destroy.tabs)").each( function( index, panel ) { + var anchor = self.anchors.filter( "[aria-controls='#" + panel.id + "']"); + if ( !anchor.length ) { + $( panel ).remove(); + } + }); }, - _tabify: function( init ) { + _refresh: function() { + var self = this, + o = this.options; + + this.element + .toggleClass( "ui-tabs-collapsible", o.collapsible ); + + 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" ); + + if ( !o.disabled.length ) { + o.disabled = false; + } + + // disable tabs + for ( var i = 0, li; ( li = this.lis[ i ] ); i++ ) { + $( li ).toggleClass( "ui-state-disabled", $.inArray( i, o.disabled ) != -1 ); + } + + this._setupEvents( o.event ); + + // remove all handlers, may run on existing tabs + this.lis.unbind( ".tabs" ); + + + this._focusable( this.lis ); + this._hoverable( this.lis ); + }, + + _processTabs: function() { var self = this, - o = this.options, fragmentId = /^#.+/; // Safari 2 reports '#' for an empty hash this.list = this.element.find( "ol,ul" ).eq( 0 ); @@ -110,14 +205,17 @@ $.widget( "ui.tabs", { this.panels = $( [] ); this.anchors.each(function( i, a ) { - var href = $( a ).attr( "href" ); + var href = $( a ).attr( "href" ), + hrefBase = href.split( "#" )[ 0 ], + selector, + panel, + baseEl; + // 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; @@ -126,281 +224,214 @@ $.widget( "ui.tabs", { // inline tab if ( fragmentId.test( href ) ) { - self.panels = self.panels.add( self.element.find( self._sanitizeSelector( href ) ) ); + selector = href; + panel = self.element.find( self._sanitizeSelector( selector ) ); // remote tab // prevent loading the page itself if href is just "#" } else if ( href && href !== "#" ) { - // required for restore on destroy - $.data( a, "href.tabs", href ); - - // TODO until #3808 is fixed strip fragment identifier from url - // (IE fails to load from such url) - $.data( a, "load.tabs", href.replace( /#.*$/, "" ) ); - var id = self._tabId( a ); - a.href = "#" + id; - var $panel = self.element.find( "#" + 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 ); + selector = "#" + id; + panel = self.element.find( selector ); + if ( !panel.length ) { + panel = self._createPanel( id ); + panel.insertAfter( self.panels[ i - 1 ] || self.list ); } - self.panels = self.panels.add( $panel ); // invalid tab href } else { - o.disabled.push( i ); + self.options.disabled.push( i ); } + + if ( panel.length) { + self.panels = self.panels.add( panel ); + } + $( a ).attr( "aria-controls", selector ); }); + }, - // 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; - } - }); - } - 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 || ( this.lis.length ? 0 : -1 ); - } 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. - if ( $.isArray( o.disabled ) ) { - o.disabled = $.unique( o.disabled.concat( - $.map( this.lis.filter( ".ui-state-disabled" ), function( n, i ) { - return self.lis.index( n ); - }) - ) ).sort(); - } - - // highlight selected tab - this.panels.addClass( "ui-tabs-hide" ); - this.lis.removeClass( "ui-tabs-selected ui-state-active" ); - // check for length avoids error when initializing empty list - if ( o.selected >= 0 && this.anchors.length ) { - self.element.find( self._sanitizeSelector( self.anchors[ o.selected ].hash ) ).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.element.find( self._sanitizeSelector( self.anchors[ o.selected ].hash ) )[ 0 ] ) ); - }); + _createPanel: function( id ) { + return $( "<div></div>" ) + .attr( "id", id ) + .addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" ) + .data( "destroy.tabs", true ); + }, - this.load( o.selected ); + _setupFx: function( fx ) { + // set up animations + if ( fx ) { + if ( $.isArray( fx ) ) { + this.hideFx = fx[ 0 ]; + this.showFx = fx[ 1 ]; + } else { + this.hideFx = this.showFx = fx; } - - // clean up to avoid memory leaks in certain versions of IE 6 - // TODO: namespace this event - $( 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" ) ); } + }, - if ( !o.disabled.length ) { - o.disabled = false; + // Reset certain styles left over from animation + // and prevent IE's ClearType bug... + _resetStyle: function ( $el, fx ) { + $el.css( "display", "" ); + if ( !$.support.opacity && fx.opacity ) { + $el[ 0 ].style.removeAttribute( "filter" ); } + }, - this.element.toggleClass( "ui-tabs-collapsible", o.collapsible ); + _showTab: function( clicked, show, event ) { + var self = this; - // set or update cookie after init and add/remove respectively - if ( o.cookie ) { - this._cookie( o.selected, o.cookie ); - } + $( clicked ).closest( "li" ).addClass( "ui-tabs-active ui-state-active" ); - // disable tabs - for ( var i = 0, li; ( li = this.lis[ i ] ); i++ ) { - $( li ).toggleClass( "ui-state-disabled", $.inArray( i, o.disabled ) != -1 ); - } - - // reset cache if switching from cached to not cached - if ( o.cache === false ) { - this.anchors.removeData( "cache.tabs" ); + if ( this.showFx ) { + self.running = true; + show.hide() + .animate( showFx, showFx.duration || "normal", function() { + self._resetStyle( show, showFx ); + self.running = false; + self._trigger( "activate", event, self._ui( clicked, show[ 0 ] ) ); + }); + } else { + show.show(); + self._trigger( "activate", event, self._ui( clicked, show[ 0 ] ) ); } + }, - // remove all handlers before, tabify may run on existing tabs after add or option change - this.lis.add( this.anchors ).unbind( ".tabs" ); + _hideTab: function( clicked, $hide ) { + var self = this; - this._focusable( this.lis ); - this._hoverable( this.lis ); - - // 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; - } + if ( this.hideFx ) { + self.running = true; + $hide.animate( hideFx, hideFx.duration || "normal", function() { + self.running = false; + self.lis.removeClass( "ui-tabs-active ui-state-active" ); + self._resetStyle( $hide, hideFx ); + self.element.dequeue( "tabs" ); + }); + } else { + self.lis.removeClass( "ui-tabs-active ui-state-active" ); + $hide.hide(); + self.element.dequeue( "tabs" ); } + }, - // 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" ); - } - } + _setupEvents: function( event ) { + // attach tab event handler, unbind to avoid duplicates from former tabifying... + this.anchors.unbind( ".tabs" ); - // Show a tab... - var showTab = showFx - ? function( clicked, $show, event ) { - $( clicked ).closest( "li" ).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", event, self._ui( clicked, $show[ 0 ] ) ); - }); - } - : function( clicked, $show, event ) { - $( clicked ).closest( "li" ).addClass( "ui-tabs-selected ui-state-active" ); - $show.removeClass( "ui-tabs-hide" ); - self._trigger( "show", event, 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" ); - $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" ); - $hide.addClass( "ui-tabs-hide" ); - self.element.dequeue( "tabs" ); - }; + if ( event ) { + this.anchors.bind( event.split( " " ).join( ".tabs " ) + ".tabs", + $.proxy( this, "_eventHandler" ) ); + } - // attach tab event handler, unbind to avoid duplicates from former tabifying... - this.anchors.bind( o.event + ".tabs", function( event ) { + // disable click in any case + this.anchors.bind( "click.tabs", function( event ){ event.preventDefault(); - var el = this, - $li = $(el).closest( "li" ), - $hide = self.panels.filter( ":not(.ui-tabs-hide)" ), - $show = self.element.find( self._sanitizeSelector( el.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.panels.filter( ":animated" ).length || - self._trigger( "select", event, self._ui( this, $show[ 0 ] ) ) === false ) { - this.blur(); - return; - } - - o.selected = self.anchors.index( this ); + }); + }, - self.abort(); + _eventHandler: function( event ) { + event.preventDefault(); + var self = this, + o = this.options, + clicked = $( event.currentTarget ), + $li = clicked.closest( "li" ), + $hide = self.element.find( self._sanitizeSelector( $( this.active ).attr( "aria-controls" ) ) ), + $show = self.element.find( self._sanitizeSelector( clicked.attr( "aria-controls" ) ) ); + + // tab is already selected, but not collapsible + if ( ( $li.hasClass( "ui-tabs-active" ) && !o.collapsible ) || + // can't switch durning an animation + self.running || + // tab is disabled + $li.hasClass( "ui-state-disabled" ) || + // tab is already loading + $li.hasClass( "ui-tabs-loading" ) || + // allow canceling by beforeActivate event + self._trigger( "beforeActivate", event, self._ui( clicked[ 0 ], $show[ 0 ] ) ) === false ) { + clicked[ 0 ].blur(); + return; + } - // if tab may be closed - if ( o.collapsible ) { - if ( $li.hasClass( "ui-tabs-selected" ) ) { - o.selected = -1; + o.active = self.anchors.index( clicked ); - if ( o.cookie ) { - self._cookie( o.selected, o.cookie ); - } + self.active = clicked; - self.element.queue( "tabs", function() { - hideTab( el, $hide ); - }).dequeue( "tabs" ); + if ( self.xhr ) { + self.xhr.abort(); + } - this.blur(); - return; - } else if ( !$hide.length ) { - if ( o.cookie ) { - self._cookie( o.selected, o.cookie ); - } + // if tab may be closed + if ( o.collapsible ) { + if ( $li.hasClass( "ui-tabs-active" ) ) { + o.active = -1; + self.active = null; - self.element.queue( "tabs", function() { - showTab( el, $show, event ); - }); + self.element.queue( "tabs", function() { + self._hideTab( clicked, $hide ); + }).dequeue( "tabs" ); - // TODO make passing in node possible, see also http://dev.jqueryui.com/ticket/3171 - self.load( self.anchors.index( this ) ); + clicked[ 0 ].blur(); + return; + } else if ( !$hide.length ) { + self.element.queue( "tabs", function() { + self._showTab( clicked, $show, event ); + }); - this.blur(); - return; - } - } + // TODO make passing in node possible, see also http://dev.jqueryui.com/ticket/3171 + self.load( self.anchors.index( clicked ) ); - if ( o.cookie ) { - self._cookie( o.selected, o.cookie ); + clicked[ 0 ].blur(); + return; } + } - // show new tab - if ( $show.length ) { - if ( $hide.length ) { - self.element.queue( "tabs", function() { - hideTab( el, $hide ); - }); - } + // show new tab + if ( $show.length ) { + if ( $hide.length ) { self.element.queue( "tabs", function() { - showTab( el, $show, event ); + self._hideTab( clicked, $hide ); }); - - self.load( self.anchors.index( this ) ); - } else { - throw "jQuery UI Tabs: Mismatching fragment identifier."; } + self.element.queue( "tabs", function() { + self._showTab( clicked, $show, event ); + }); - // 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 - if ( $.browser.msie ) { - this.blur(); - } - }); + self.load( self.anchors.index( clicked ) ); + } else { + throw "jQuery UI Tabs: Mismatching fragment identifier."; + } - // disable click in any case - this.anchors.bind( "click.tabs", function( event ){ - event.preventDefault(); + // 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 + if ( $.browser.msie ) { + clicked[ 0 ].blur(); + } + }, + + _activate: function( index ) { + var active = this._findActive( index )[ 0 ]; + + // trying to activate the already active panel + if ( this.active && active === this.active[ 0 ] ) { + return; + } + + // trying to collapse, simulate a click on the current active header + active = active || this.active; + + this._eventHandler({ + target: active, + currentTarget: active, + preventDefault: $.noop }); }, + _findActive: function( selector ) { + return typeof selector === "number" ? this.anchors.eq( selector ) : + typeof selector === "string" ? this.anchors.filter( "[href$='" + selector + "']" ) : $(); + }, + _getIndex: function( index ) { // meta-function to give users option to provide a href string instead of a numerical index. // also sanitizes numerical indexes to valid values. @@ -414,19 +445,17 @@ $.widget( "ui.tabs", { _destroy: function() { var o = this.options; - this.abort(); + if ( this.xhr ) { + this.xhr.abort(); + } this.element.removeClass( "ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible" ); 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 ) { + $.each( [ "href", "load" ], function( i, prefix ) { $this.removeData( prefix + ".tabs" ); }); }); @@ -438,97 +467,16 @@ $.widget( "ui.tabs", { $( this ).removeClass([ "ui-state-default", "ui-corner-top", - "ui-tabs-selected", + "ui-tabs-active", "ui-state-active", "ui-state-disabled", "ui-tabs-panel", "ui-widget-content", - "ui-corner-bottom", - "ui-tabs-hide" + "ui-corner-bottom" ].join( " " ) ); } }); - if ( o.cookie ) { - this._cookie( null, o.cookie ); - } - - return this; - }, - - add: function( url, label, index ) { - if ( index === undefined ) { - index = this.anchors.length; - } - - 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 = self.element.find( "#" + 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 ) { - o.selected = 0; - $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 ); - } - - this._trigger( "add", null, this._ui( this.anchors[ index ], this.panels[ index ] ) ); - return this; - }, - - remove: function( index ) { - index = this._getIndex( 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(); - - this._trigger( "remove", null, this._ui( $li.find( "a" )[ 0 ], $panel[ 0 ] ) ); return this; }, @@ -554,7 +502,6 @@ $.widget( "ui.tabs", { o.disabled = false; } - this._trigger( "enable", null, this._ui( this.anchors[ index ], this.panels[ index ] ) ); return this; }, @@ -578,120 +525,474 @@ $.widget( "ui.tabs", { o.disabled = true; } - this._trigger( "disable", null, this._ui( this.anchors[ index ], this.panels[ index ] ) ); } return this; }, - select: function( index ) { - index = this._getIndex( index ); - if ( index == -1 ) { - if ( this.options.collapsible && this.options.selected != -1 ) { - index = this.options.selected; - } else { - return this; - } - } - this.anchors.eq( index ).trigger( this.options.event + ".tabs" ); - return this; - }, - load: function( index ) { index = this._getIndex( index ); var self = this, o = this.options, a = this.anchors.eq( index )[ 0 ], - url = $.data( a, "load.tabs" ); + panel = self.element.find( self._sanitizeSelector( $( a ).attr( "aria-controls" ) ) ), + // TODO until #3808 is fixed strip fragment identifier from url + // (IE fails to load from such url) + url = $( a ).attr( "href" ).replace( /#.*$/, "" ), + eventData = self._ui( a, panel[ 0 ] ); - this.abort(); + if ( this.xhr ) { + this.xhr.abort(); + } - // not remote or from cache - if ( !url || this.element.queue( "tabs" ).length !== 0 && $.data( a, "cache.tabs" ) ) { + // not remote + if ( !url ) { this.element.dequeue( "tabs" ); return; } - // load remote from here on - this.lis.eq( index ).addClass( "ui-state-processing" ); + this.xhr = $.ajax({ + url: url, + beforeSend: function( jqXHR, settings ) { + return self._trigger( "beforeload", null, + $.extend( { jqXHR: jqXHR, settings: settings }, eventData ) ); + } + }); + + if ( this.xhr ) { + // load remote from here on + this.lis.eq( index ).addClass( "ui-tabs-loading" ); + + this.xhr + .success( function( response ) { + panel.html( response ); + }) + .complete( function( jqXHR, status ) { + if ( status === "abort" ) { + // stop possibly running animations + self.element.queue( [] ); + self.panels.stop( false, true ); + + // "tabs" queue must not contain more than two elements, + // which are the callbacks for the latest clicked tab... + self.element.queue( "tabs", self.element.queue( "tabs" ).splice( -2, 2 ) ); + + delete this.xhr; + } - if ( o.spinner ) { - var span = $( "span", a ); - span.data( "label.tabs", span.html() ).html( o.spinner ); + self.lis.eq( index ).removeClass( "ui-tabs-loading" ); + + self._trigger( "load", null, eventData ); + }); } - this.xhr = $.ajax( $.extend( {}, o.ajaxOptions, { - url: url, - success: function( r, s ) { - self.element.find( self._sanitizeSelector( a.hash ) ).html( r ); + // last, so that load event is fired before show... + self.element.dequeue( "tabs" ); - // take care of tab labels - self._cleanup(); + return this; + } - if ( o.cache ) { - $.data( a, "cache.tabs", true ); - } +}); + +$.extend( $.ui.tabs, { + version: "@VERSION" +}); + +// DEPRECATED +if ( $.uiBackCompat !== false ) { + + // ajaxOptions and cache options + (function( $, prototype ) { + $.extend( prototype.options, { + ajaxOptions: null, + cache: false + }); - self._trigger( "load", null, self._ui( self.anchors[ index ], self.panels[ index ] ) ); - try { - o.ajaxOptions.success( r, s ); + var _create = prototype._create, + _setOption = prototype._setOption, + _destroy = prototype._destroy, + oldurl = prototype._url; + + $.extend( prototype, { + _create: function() { + _create.call( this ); + + var self = this; + + this.element.bind( "tabsbeforeload", function( event, ui ) { + // tab is already cached + if ( $.data( ui.tab, "cache.tabs" ) ) { + event.preventDefault(); + return; + } + + $.extend( ui.settings, self.options.ajaxOptions, { + error: function( xhr, s, e ) { + try { + // Passing index avoid a race condition when this method is + // called after the user has selected another tab. + // Pass the anchor that initiated this request allows + // loadError to manipulate the tab content panel via $(a.hash) + self.options.ajaxOptions.error( xhr, s, ui.index, ui.tab ); + } + catch ( e ) {} + } + }); + + ui.jqXHR.success( function() { + if ( self.options.cache ) { + $.data( ui.tab, "cache.tabs", true ); + } + }); + }); + }, + + _setOption: function( key, value ) { + // reset cache if switching from cached to not cached + if ( key === "cache" && value === false ) { + this.anchors.removeData( "cache.tabs" ); } - catch ( e ) {} + _setOption.apply( this, arguments ); + }, + + _destroy: function() { + this.anchors.removeData( "cache.tabs" ); + _destroy.call( this ); }, - error: function( xhr, s, e ) { - // take care of tab labels - self._cleanup(); - - self._trigger( "load", null, self._ui( self.anchors[ index ], self.panels[ index ] ) ); - try { - // Passing index avoid a race condition when this method is - // called after the user has selected another tab. - // Pass the anchor that initiated this request allows - // loadError to manipulate the tab content panel via $(a.hash) - o.ajaxOptions.error( xhr, s, index, a ); + + url: function( index, url ){ + this.anchors.eq( index ).removeData( "cache.tabs" ); + oldurl.apply( this, arguments ); + } + }); + }( jQuery, jQuery.ui.tabs.prototype ) ); + + // abort method + (function( $, prototype ) { + prototype.abort = function() { + if ( this.xhr ) { + this.xhr.abort(); + } + }; + }( jQuery, jQuery.ui.tabs.prototype ) ); + + // spinner + (function( $, prototype ) { + $.extend( prototype.options, { + spinner: "<em>Loading…</em>" + }); + + var _create = prototype._create; + prototype._create = function() { + _create.call( this ); + var self = this; + + this.element.bind( "tabsbeforeload", function( event, ui ) { + if ( self.options.spinner ) { + var span = $( "span", ui.tab ); + if ( span.length ) { + span.data( "label.tabs", span.html() ).html( self.options.spinner ); + } } - catch ( e ) {} + ui.jqXHR.complete( function() { + if ( self.options.spinner ) { + var span = $( "span", ui.tab ); + if ( span.length ) { + span.html( span.data( "label.tabs" ) ).removeData( "label.tabs" ); + } + } + }); + }); + }; + }( jQuery, jQuery.ui.tabs.prototype ) ); + + // enable/disable events + (function( $, prototype ) { + $.extend( prototype.options, { + enable: null, + disable: null + }); + + var enable = prototype.enable, + disable = prototype.disable; + + prototype.enable = function( index ) { + var o = this.options, + trigger; + + if ( index && o.disabled || ($.isArray( o.disabled ) && $.inArray( index, o.disabled ) !== -1 ) ) { + trigger = true; } - } ) ); - // last, so that load event is fired before show... - self.element.dequeue( "tabs" ); + enable.apply( this, arguments ); - return this; - }, + if ( trigger ) { + this._trigger( "enable", null, this._ui( this.anchors[ index ], this.panels[ index ] ) ); + } + }; - abort: function() { - // stop possibly running animations - this.element.queue( [] ); - this.panels.stop( false, true ); + prototype.disable = function( index ) { + var o = this.options, + trigger; - // "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 ) ); + if ( index && !o.disabled || ($.isArray( o.disabled ) && $.inArray( index, o.disabled ) == -1 ) ) { + trigger = true; + } - // terminate pending requests from other tabs - if ( this.xhr ) { - this.xhr.abort(); - delete this.xhr; - } + disable.apply( this, arguments ); - // take care of tab labels - this._cleanup(); - return this; - }, + if ( trigger ) { + this._trigger( "disable", null, this._ui( this.anchors[ index ], this.panels[ index ] ) ); + } + }; + }( jQuery, jQuery.ui.tabs.prototype ) ); + + // add/remove methods and events + (function( $, prototype ) { + $.extend( prototype.options, { + add: null, + remove: null, + tabTemplate: "<li><a href='#{href}'><span>#{label}</span></a></li>" + }); - url: function( index, url ) { - this.anchors.eq( index ).removeData( "cache.tabs" ).data( "load.tabs", url ); - return this; - }, + prototype.add = function( url, label, index ) { + if ( index === undefined ) { + index = this.anchors.length; + } - length: function() { - return this.anchors.length; - } -}); + 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 ] ); -$.extend( $.ui.tabs, { - version: "@VERSION" -}); + $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 = self.element.find( "#" + id ); + if ( !$panel.length ) { + $panel = self._createPanel( id ); + } + $panel.addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" ).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.refresh(); + + if ( this.anchors.length == 1 ) { + o.active = o.selected = 0; + $li.addClass( "ui-tabs-active ui-state-active" ); + $panel.show(); + this.element.queue( "tabs", function() { + self._trigger( "activate", null, self._ui( self.anchors[ 0 ], self.panels[ 0 ] ) ); + }); + + this.load( 0 ); + } + + this._trigger( "add", null, this._ui( this.anchors[ index ], this.panels[ index ] ) ); + return this; + }; + + prototype.remove = function( index ) { + index = this._getIndex( 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-active" ) && this.anchors.length > 1) { + this._activate( 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.refresh(); + + this._trigger( "remove", null, this._ui( $li.find( "a" )[ 0 ], $panel[ 0 ] ) ); + return this; + }; + }( jQuery, jQuery.ui.tabs.prototype ) ); + + // length method + (function( $, prototype ) { + prototype.length = function() { + return this.anchors.length; + }; + }( jQuery, jQuery.ui.tabs.prototype ) ); + + // url method + (function( $, prototype ) { + prototype.url = function( index, url ) { + this.anchors.eq( index ).attr( "href", url ); + }; + }( jQuery, jQuery.ui.tabs.prototype ) ); + + // _tabId method + (function( $, prototype ) { + $.extend( prototype.options, { + idPrefix: "ui-tabs-" + }); + + var _tabId = prototype._tabId; + prototype._tabId = function( a ) { + return ( $( a ).attr( "aria-controls" ) || "" ).replace( /^#/ , "" ) || + a.title && a.title.replace( /\s/g, "_" ).replace( /[^\w\u00c0-\uFFFF-]/g, "" ) || + this.options.idPrefix + getNextTabId(); + }; + }( jQuery, jQuery.ui.tabs.prototype ) ); + + // _tabId method + (function( $, prototype ) { + $.extend( prototype.options, { + panelTemplate: "<div></div>" + }); + + var _createPanel = prototype._createPanel; + prototype._createPanel = function( id ) { + return $( this.options.panelTemplate ) + .attr( "id", id ) + .addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" ) + .data( "destroy.tabs", true ); + }; + }( jQuery, jQuery.ui.tabs.prototype ) ); + + // selected option + (function( $, prototype ) { + var _create = prototype._create, + _setOption = prototype._setOption, + _eventHandler = prototype._eventHandler; + + prototype._create = function() { + var options = this.options; + if ( options.active === undefined && options.selected !== undefined ) { + options.active = options.selected; + } + _create.call( this ); + options.selected = options.active; + }; + + prototype._setOption = function( key, value ) { + var options = this.options; + if ( key === "selected" ) { + key = "active"; + } + _setOption.apply( this, arguments ); + if ( key === "active" ) { + options.selected = options.active ; + } + }; + + prototype._eventHandler = function( event ) { + _eventHandler.apply( this, arguments ); + this.options.selected = this.options.active ; + }; + }( jQuery, jQuery.ui.tabs.prototype ) ); + + // show and select event + (function( $, prototype ) { + $.extend( prototype.options, { + show: null, + select: null + }); + var _trigger = prototype._trigger; + + prototype._trigger = function( type, event, data ) { + var ret = _trigger.apply( this, arguments ); + if ( !ret ) { + return false; + } + if ( type === "beforeActivate" ) { + ret = _trigger.call( this, "select", event, data ); + } else if ( type === "activate" ) { + ret = _trigger.call( this, "show", event, data ); + } + }; + }( jQuery, jQuery.ui.tabs.prototype ) ); + + // select method + (function( $, prototype ) { + prototype.select = function( index ) { + index = this._getIndex( index ); + if ( index == -1 ) { + if ( this.options.collapsible && this.options.selected != -1 ) { + index = this.options.selected; + } else { + return; + } + } + this.anchors.eq( index ).trigger( this.options.event + ".tabs" ); + }; + }( jQuery, jQuery.ui.tabs.prototype ) ); + + // cookie option + (function( $, prototype ) { + $.extend( prototype.options, { + cookie: null // e.g. { expires: 7, path: '/', domain: 'jquery.com', secure: true } + }); + + var _create = prototype._create, + _refresh = prototype._refresh, + _eventHandler = prototype._eventHandler, + _destroy = prototype._destroy; + + prototype._create = function() { + var o = this.options; + if ( o.active === undefined ) { + if ( typeof o.active !== "number" && o.cookie ) { + o.active = parseInt( this._cookie(), 10 ); + } + } + _create.call( this ); + }; + + prototype._cookie = function() { + var cookie = this.cookie || + ( this.cookie = this.options.cookie.name || "ui-tabs-" + getNextListId() ); + return $.cookie.apply( null, [ cookie ].concat( $.makeArray( arguments ) ) ); + }; + + prototype._refresh = function() { + _refresh.call( this ); + + // set or update cookie after init and add/remove respectively + if ( this.options.cookie ) { + this._cookie( this.options.active, this.options.cookie ); + } + }; + + prototype._eventHandler = function( event ) { + _eventHandler.apply( this, arguments ); + + if ( this.options.cookie ) { + this._cookie( this.options.active, this.options.cookie ); + } + }; + + prototype._destroy = function() { + _destroy.call( this ); + + if ( this.options.cookie ) { + this._cookie( null, this.options.cookie ); + } + }; + }( jQuery, jQuery.ui.tabs.prototype ) ); +} })( jQuery ); |