ui.tabs.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685
  1. /*
  2. * jQuery UI Tabs 1.7.2
  3. *
  4. * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
  5. * Dual licensed under the MIT (MIT-LICENSE.txt)
  6. * and GPL (GPL-LICENSE.txt) licenses.
  7. *
  8. * http://docs.jquery.com/UI/Tabs
  9. *
  10. * Depends:
  11. * ui.core.js
  12. */
  13. (function($) {
  14. $.widget("ui.tabs", {
  15. _init: function() {
  16. if (this.options.deselectable !== undefined) {
  17. this.options.collapsible = this.options.deselectable;
  18. }
  19. this._tabify(true);
  20. },
  21. _setData: function(key, value) {
  22. if (key == 'selected') {
  23. if (this.options.collapsible && value == this.options.selected) {
  24. return;
  25. }
  26. this.select(value);
  27. }
  28. else {
  29. this.options[key] = value;
  30. if (key == 'deselectable') {
  31. this.options.collapsible = value;
  32. }
  33. this._tabify();
  34. }
  35. },
  36. _tabId: function(a) {
  37. return a.title && a.title.replace(/\s/g, '_').replace(/[^A-Za-z0-9\-_:\.]/g, '') ||
  38. this.options.idPrefix + $.data(a);
  39. },
  40. _sanitizeSelector: function(hash) {
  41. return hash.replace(/:/g, '\\:'); // we need this because an id may contain a ":"
  42. },
  43. _cookie: function() {
  44. var cookie = this.cookie || (this.cookie = this.options.cookie.name || 'ui-tabs-' + $.data(this.list[0]));
  45. return $.cookie.apply(null, [cookie].concat($.makeArray(arguments)));
  46. },
  47. _ui: function(tab, panel) {
  48. return {
  49. tab: tab,
  50. panel: panel,
  51. index: this.anchors.index(tab)
  52. };
  53. },
  54. _cleanup: function() {
  55. // restore all former loading tabs labels
  56. this.lis.filter('.ui-state-processing').removeClass('ui-state-processing')
  57. .find('span:data(label.tabs)')
  58. .each(function() {
  59. var el = $(this);
  60. el.html(el.data('label.tabs')).removeData('label.tabs');
  61. });
  62. },
  63. _tabify: function(init) {
  64. this.list = this.element.children('ul:first');
  65. this.lis = $('li:has(a[href])', this.list);
  66. this.anchors = this.lis.map(function() { return $('a', this)[0]; });
  67. this.panels = $([]);
  68. var self = this, o = this.options;
  69. var fragmentId = /^#.+/; // Safari 2 reports '#' for an empty hash
  70. this.anchors.each(function(i, a) {
  71. var href = $(a).attr('href');
  72. // For dynamically created HTML that contains a hash as href IE < 8 expands
  73. // such href to the full page url with hash and then misinterprets tab as ajax.
  74. // Same consideration applies for an added tab with a fragment identifier
  75. // since a[href=#fragment-identifier] does unexpectedly not match.
  76. // Thus normalize href attribute...
  77. var hrefBase = href.split('#')[0], baseEl;
  78. if (hrefBase && (hrefBase === location.toString().split('#')[0] ||
  79. (baseEl = $('base')[0]) && hrefBase === baseEl.href)) {
  80. href = a.hash;
  81. a.href = href;
  82. }
  83. // inline tab
  84. if (fragmentId.test(href)) {
  85. self.panels = self.panels.add(self._sanitizeSelector(href));
  86. }
  87. // remote tab
  88. else if (href != '#') { // prevent loading the page itself if href is just "#"
  89. $.data(a, 'href.tabs', href); // required for restore on destroy
  90. // TODO until #3808 is fixed strip fragment identifier from url
  91. // (IE fails to load from such url)
  92. $.data(a, 'load.tabs', href.replace(/#.*$/, '')); // mutable data
  93. var id = self._tabId(a);
  94. a.href = '#' + id;
  95. var $panel = $('#' + id);
  96. if (!$panel.length) {
  97. $panel = $(o.panelTemplate).attr('id', id).addClass('ui-tabs-panel ui-widget-content ui-corner-bottom')
  98. .insertAfter(self.panels[i - 1] || self.list);
  99. $panel.data('destroy.tabs', true);
  100. }
  101. self.panels = self.panels.add($panel);
  102. }
  103. // invalid tab href
  104. else {
  105. o.disabled.push(i);
  106. }
  107. });
  108. // initialization from scratch
  109. if (init) {
  110. // attach necessary classes for styling
  111. this.element.addClass('ui-tabs ui-widget ui-widget-content ui-corner-all');
  112. this.list.addClass('ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all');
  113. this.lis.addClass('ui-state-default ui-corner-top');
  114. this.panels.addClass('ui-tabs-panel ui-widget-content ui-corner-bottom');
  115. // Selected tab
  116. // use "selected" option or try to retrieve:
  117. // 1. from fragment identifier in url
  118. // 2. from cookie
  119. // 3. from selected class attribute on <li>
  120. if (o.selected === undefined) {
  121. if (location.hash) {
  122. this.anchors.each(function(i, a) {
  123. if (a.hash == location.hash) {
  124. o.selected = i;
  125. return false; // break
  126. }
  127. });
  128. }
  129. if (typeof o.selected != 'number' && o.cookie) {
  130. o.selected = parseInt(self._cookie(), 10);
  131. }
  132. if (typeof o.selected != 'number' && this.lis.filter('.ui-tabs-selected').length) {
  133. o.selected = this.lis.index(this.lis.filter('.ui-tabs-selected'));
  134. }
  135. o.selected = o.selected || 0;
  136. }
  137. else if (o.selected === null) { // usage of null is deprecated, TODO remove in next release
  138. o.selected = -1;
  139. }
  140. // sanity check - default to first tab...
  141. o.selected = ((o.selected >= 0 && this.anchors[o.selected]) || o.selected < 0) ? o.selected : 0;
  142. // Take disabling tabs via class attribute from HTML
  143. // into account and update option properly.
  144. // A selected tab cannot become disabled.
  145. o.disabled = $.unique(o.disabled.concat(
  146. $.map(this.lis.filter('.ui-state-disabled'),
  147. function(n, i) { return self.lis.index(n); } )
  148. )).sort();
  149. if ($.inArray(o.selected, o.disabled) != -1) {
  150. o.disabled.splice($.inArray(o.selected, o.disabled), 1);
  151. }
  152. // highlight selected tab
  153. this.panels.addClass('ui-tabs-hide');
  154. this.lis.removeClass('ui-tabs-selected ui-state-active');
  155. if (o.selected >= 0 && this.anchors.length) { // check for length avoids error when initializing empty list
  156. this.panels.eq(o.selected).removeClass('ui-tabs-hide');
  157. this.lis.eq(o.selected).addClass('ui-tabs-selected ui-state-active');
  158. // seems to be expected behavior that the show callback is fired
  159. self.element.queue("tabs", function() {
  160. self._trigger('show', null, self._ui(self.anchors[o.selected], self.panels[o.selected]));
  161. });
  162. this.load(o.selected);
  163. }
  164. // clean up to avoid memory leaks in certain versions of IE 6
  165. $(window).bind('unload', function() {
  166. self.lis.add(self.anchors).unbind('.tabs');
  167. self.lis = self.anchors = self.panels = null;
  168. });
  169. }
  170. // update selected after add/remove
  171. else {
  172. o.selected = this.lis.index(this.lis.filter('.ui-tabs-selected'));
  173. }
  174. // update collapsible
  175. this.element[o.collapsible ? 'addClass' : 'removeClass']('ui-tabs-collapsible');
  176. // set or update cookie after init and add/remove respectively
  177. if (o.cookie) {
  178. this._cookie(o.selected, o.cookie);
  179. }
  180. // disable tabs
  181. for (var i = 0, li; (li = this.lis[i]); i++) {
  182. $(li)[$.inArray(i, o.disabled) != -1 &&
  183. !$(li).hasClass('ui-tabs-selected') ? 'addClass' : 'removeClass']('ui-state-disabled');
  184. }
  185. // reset cache if switching from cached to not cached
  186. if (o.cache === false) {
  187. this.anchors.removeData('cache.tabs');
  188. }
  189. // remove all handlers before, tabify may run on existing tabs after add or option change
  190. this.lis.add(this.anchors).unbind('.tabs');
  191. if (o.event != 'mouseover') {
  192. var addState = function(state, el) {
  193. if (el.is(':not(.ui-state-disabled)')) {
  194. el.addClass('ui-state-' + state);
  195. }
  196. };
  197. var removeState = function(state, el) {
  198. el.removeClass('ui-state-' + state);
  199. };
  200. this.lis.bind('mouseover.tabs', function() {
  201. addState('hover', $(this));
  202. });
  203. this.lis.bind('mouseout.tabs', function() {
  204. removeState('hover', $(this));
  205. });
  206. this.anchors.bind('focus.tabs', function() {
  207. addState('focus', $(this).closest('li'));
  208. });
  209. this.anchors.bind('blur.tabs', function() {
  210. removeState('focus', $(this).closest('li'));
  211. });
  212. }
  213. // set up animations
  214. var hideFx, showFx;
  215. if (o.fx) {
  216. if ($.isArray(o.fx)) {
  217. hideFx = o.fx[0];
  218. showFx = o.fx[1];
  219. }
  220. else {
  221. hideFx = showFx = o.fx;
  222. }
  223. }
  224. // Reset certain styles left over from animation
  225. // and prevent IE's ClearType bug...
  226. function resetStyle($el, fx) {
  227. $el.css({ display: '' });
  228. if ($.browser.msie && fx.opacity) {
  229. $el[0].style.removeAttribute('filter');
  230. }
  231. }
  232. // Show a tab...
  233. var showTab = showFx ?
  234. function(clicked, $show) {
  235. $(clicked).closest('li').removeClass('ui-state-default').addClass('ui-tabs-selected ui-state-active');
  236. $show.hide().removeClass('ui-tabs-hide') // avoid flicker that way
  237. .animate(showFx, showFx.duration || 'normal', function() {
  238. resetStyle($show, showFx);
  239. self._trigger('show', null, self._ui(clicked, $show[0]));
  240. });
  241. } :
  242. function(clicked, $show) {
  243. $(clicked).closest('li').removeClass('ui-state-default').addClass('ui-tabs-selected ui-state-active');
  244. $show.removeClass('ui-tabs-hide');
  245. self._trigger('show', null, self._ui(clicked, $show[0]));
  246. };
  247. // Hide a tab, $show is optional...
  248. var hideTab = hideFx ?
  249. function(clicked, $hide) {
  250. $hide.animate(hideFx, hideFx.duration || 'normal', function() {
  251. self.lis.removeClass('ui-tabs-selected ui-state-active').addClass('ui-state-default');
  252. $hide.addClass('ui-tabs-hide');
  253. resetStyle($hide, hideFx);
  254. self.element.dequeue("tabs");
  255. });
  256. } :
  257. function(clicked, $hide, $show) {
  258. self.lis.removeClass('ui-tabs-selected ui-state-active').addClass('ui-state-default');
  259. $hide.addClass('ui-tabs-hide');
  260. self.element.dequeue("tabs");
  261. };
  262. // attach tab event handler, unbind to avoid duplicates from former tabifying...
  263. this.anchors.bind(o.event + '.tabs', function() {
  264. var el = this, $li = $(this).closest('li'), $hide = self.panels.filter(':not(.ui-tabs-hide)'),
  265. $show = $(self._sanitizeSelector(this.hash));
  266. // If tab is already selected and not collapsible or tab disabled or
  267. // or is already loading or click callback returns false stop here.
  268. // Check if click handler returns false last so that it is not executed
  269. // for a disabled or loading tab!
  270. if (($li.hasClass('ui-tabs-selected') && !o.collapsible) ||
  271. $li.hasClass('ui-state-disabled') ||
  272. $li.hasClass('ui-state-processing') ||
  273. self._trigger('select', null, self._ui(this, $show[0])) === false) {
  274. this.blur();
  275. return false;
  276. }
  277. o.selected = self.anchors.index(this);
  278. self.abort();
  279. // if tab may be closed
  280. if (o.collapsible) {
  281. if ($li.hasClass('ui-tabs-selected')) {
  282. o.selected = -1;
  283. if (o.cookie) {
  284. self._cookie(o.selected, o.cookie);
  285. }
  286. self.element.queue("tabs", function() {
  287. hideTab(el, $hide);
  288. }).dequeue("tabs");
  289. this.blur();
  290. return false;
  291. }
  292. else if (!$hide.length) {
  293. if (o.cookie) {
  294. self._cookie(o.selected, o.cookie);
  295. }
  296. self.element.queue("tabs", function() {
  297. showTab(el, $show);
  298. });
  299. self.load(self.anchors.index(this)); // TODO make passing in node possible, see also http://dev.jqueryui.com/ticket/3171
  300. this.blur();
  301. return false;
  302. }
  303. }
  304. if (o.cookie) {
  305. self._cookie(o.selected, o.cookie);
  306. }
  307. // show new tab
  308. if ($show.length) {
  309. if ($hide.length) {
  310. self.element.queue("tabs", function() {
  311. hideTab(el, $hide);
  312. });
  313. }
  314. self.element.queue("tabs", function() {
  315. showTab(el, $show);
  316. });
  317. self.load(self.anchors.index(this));
  318. }
  319. else {
  320. throw 'jQuery UI Tabs: Mismatching fragment identifier.';
  321. }
  322. // Prevent IE from keeping other link focussed when using the back button
  323. // and remove dotted border from clicked link. This is controlled via CSS
  324. // in modern browsers; blur() removes focus from address bar in Firefox
  325. // which can become a usability and annoying problem with tabs('rotate').
  326. if ($.browser.msie) {
  327. this.blur();
  328. }
  329. });
  330. // disable click in any case
  331. this.anchors.bind('click.tabs', function(){return false;});
  332. },
  333. destroy: function() {
  334. var o = this.options;
  335. this.abort();
  336. this.element.unbind('.tabs')
  337. .removeClass('ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible')
  338. .removeData('tabs');
  339. this.list.removeClass('ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all');
  340. this.anchors.each(function() {
  341. var href = $.data(this, 'href.tabs');
  342. if (href) {
  343. this.href = href;
  344. }
  345. var $this = $(this).unbind('.tabs');
  346. $.each(['href', 'load', 'cache'], function(i, prefix) {
  347. $this.removeData(prefix + '.tabs');
  348. });
  349. });
  350. this.lis.unbind('.tabs').add(this.panels).each(function() {
  351. if ($.data(this, 'destroy.tabs')) {
  352. $(this).remove();
  353. }
  354. else {
  355. $(this).removeClass([
  356. 'ui-state-default',
  357. 'ui-corner-top',
  358. 'ui-tabs-selected',
  359. 'ui-state-active',
  360. 'ui-state-hover',
  361. 'ui-state-focus',
  362. 'ui-state-disabled',
  363. 'ui-tabs-panel',
  364. 'ui-widget-content',
  365. 'ui-corner-bottom',
  366. 'ui-tabs-hide'
  367. ].join(' '));
  368. }
  369. });
  370. if (o.cookie) {
  371. this._cookie(null, o.cookie);
  372. }
  373. },
  374. add: function(url, label, index) {
  375. if (index === undefined) {
  376. index = this.anchors.length; // append by default
  377. }
  378. var self = this, o = this.options,
  379. $li = $(o.tabTemplate.replace(/#\{href\}/g, url).replace(/#\{label\}/g, label)),
  380. id = !url.indexOf('#') ? url.replace('#', '') : this._tabId($('a', $li)[0]);
  381. $li.addClass('ui-state-default ui-corner-top').data('destroy.tabs', true);
  382. // try to find an existing element before creating a new one
  383. var $panel = $('#' + id);
  384. if (!$panel.length) {
  385. $panel = $(o.panelTemplate).attr('id', id).data('destroy.tabs', true);
  386. }
  387. $panel.addClass('ui-tabs-panel ui-widget-content ui-corner-bottom ui-tabs-hide');
  388. if (index >= this.lis.length) {
  389. $li.appendTo(this.list);
  390. $panel.appendTo(this.list[0].parentNode);
  391. }
  392. else {
  393. $li.insertBefore(this.lis[index]);
  394. $panel.insertBefore(this.panels[index]);
  395. }
  396. o.disabled = $.map(o.disabled,
  397. function(n, i) { return n >= index ? ++n : n; });
  398. this._tabify();
  399. if (this.anchors.length == 1) { // after tabify
  400. $li.addClass('ui-tabs-selected ui-state-active');
  401. $panel.removeClass('ui-tabs-hide');
  402. this.element.queue("tabs", function() {
  403. self._trigger('show', null, self._ui(self.anchors[0], self.panels[0]));
  404. });
  405. this.load(0);
  406. }
  407. // callback
  408. this._trigger('add', null, this._ui(this.anchors[index], this.panels[index]));
  409. },
  410. remove: function(index) {
  411. var o = this.options, $li = this.lis.eq(index).remove(),
  412. $panel = this.panels.eq(index).remove();
  413. // If selected tab was removed focus tab to the right or
  414. // in case the last tab was removed the tab to the left.
  415. if ($li.hasClass('ui-tabs-selected') && this.anchors.length > 1) {
  416. this.select(index + (index + 1 < this.anchors.length ? 1 : -1));
  417. }
  418. o.disabled = $.map($.grep(o.disabled, function(n, i) { return n != index; }),
  419. function(n, i) { return n >= index ? --n : n; });
  420. this._tabify();
  421. // callback
  422. this._trigger('remove', null, this._ui($li.find('a')[0], $panel[0]));
  423. },
  424. enable: function(index) {
  425. var o = this.options;
  426. if ($.inArray(index, o.disabled) == -1) {
  427. return;
  428. }
  429. this.lis.eq(index).removeClass('ui-state-disabled');
  430. o.disabled = $.grep(o.disabled, function(n, i) { return n != index; });
  431. // callback
  432. this._trigger('enable', null, this._ui(this.anchors[index], this.panels[index]));
  433. },
  434. disable: function(index) {
  435. var self = this, o = this.options;
  436. if (index != o.selected) { // cannot disable already selected tab
  437. this.lis.eq(index).addClass('ui-state-disabled');
  438. o.disabled.push(index);
  439. o.disabled.sort();
  440. // callback
  441. this._trigger('disable', null, this._ui(this.anchors[index], this.panels[index]));
  442. }
  443. },
  444. select: function(index) {
  445. if (typeof index == 'string') {
  446. index = this.anchors.index(this.anchors.filter('[href$=' + index + ']'));
  447. }
  448. else if (index === null) { // usage of null is deprecated, TODO remove in next release
  449. index = -1;
  450. }
  451. if (index == -1 && this.options.collapsible) {
  452. index = this.options.selected;
  453. }
  454. this.anchors.eq(index).trigger(this.options.event + '.tabs');
  455. },
  456. load: function(index) {
  457. var self = this, o = this.options, a = this.anchors.eq(index)[0], url = $.data(a, 'load.tabs');
  458. this.abort();
  459. // not remote or from cache
  460. if (!url || this.element.queue("tabs").length !== 0 && $.data(a, 'cache.tabs')) {
  461. this.element.dequeue("tabs");
  462. return;
  463. }
  464. // load remote from here on
  465. this.lis.eq(index).addClass('ui-state-processing');
  466. if (o.spinner) {
  467. var span = $('span', a);
  468. span.data('label.tabs', span.html()).html(o.spinner);
  469. }
  470. this.xhr = $.ajax($.extend({}, o.ajaxOptions, {
  471. url: url,
  472. success: function(r, s) {
  473. $(self._sanitizeSelector(a.hash)).html(r);
  474. // take care of tab labels
  475. self._cleanup();
  476. if (o.cache) {
  477. $.data(a, 'cache.tabs', true); // if loaded once do not load them again
  478. }
  479. // callbacks
  480. self._trigger('load', null, self._ui(self.anchors[index], self.panels[index]));
  481. try {
  482. o.ajaxOptions.success(r, s);
  483. }
  484. catch (e) {}
  485. // last, so that load event is fired before show...
  486. self.element.dequeue("tabs");
  487. }
  488. }));
  489. },
  490. abort: function() {
  491. // stop possibly running animations
  492. this.element.queue([]);
  493. this.panels.stop(false, true);
  494. // terminate pending requests from other tabs
  495. if (this.xhr) {
  496. this.xhr.abort();
  497. delete this.xhr;
  498. }
  499. // take care of tab labels
  500. this._cleanup();
  501. },
  502. url: function(index, url) {
  503. this.anchors.eq(index).removeData('cache.tabs').data('load.tabs', url);
  504. },
  505. length: function() {
  506. return this.anchors.length;
  507. }
  508. });
  509. $.extend($.ui.tabs, {
  510. version: '1.7.2',
  511. getter: 'length',
  512. defaults: {
  513. ajaxOptions: null,
  514. cache: false,
  515. cookie: null, // e.g. { expires: 7, path: '/', domain: 'jquery.com', secure: true }
  516. collapsible: false,
  517. disabled: [],
  518. event: 'click',
  519. fx: null, // e.g. { height: 'toggle', opacity: 'toggle', duration: 200 }
  520. idPrefix: 'ui-tabs-',
  521. panelTemplate: '<div></div>',
  522. spinner: '<em>Loading&#8230;</em>',
  523. tabTemplate: '<li><a href="#{href}"><span>#{label}</span></a></li>'
  524. }
  525. });
  526. /*
  527. * Tabs Extensions
  528. */
  529. /*
  530. * Rotate
  531. */
  532. $.extend($.ui.tabs.prototype, {
  533. rotation: null,
  534. rotate: function(ms, continuing) {
  535. var self = this, o = this.options;
  536. var rotate = self._rotate || (self._rotate = function(e) {
  537. clearTimeout(self.rotation);
  538. self.rotation = setTimeout(function() {
  539. var t = o.selected;
  540. self.select( ++t < self.anchors.length ? t : 0 );
  541. }, ms);
  542. if (e) {
  543. e.stopPropagation();
  544. }
  545. });
  546. var stop = self._unrotate || (self._unrotate = !continuing ?
  547. function(e) {
  548. if (e.clientX) { // in case of a true click
  549. self.rotate(null);
  550. }
  551. } :
  552. function(e) {
  553. t = o.selected;
  554. rotate();
  555. });
  556. // start rotation
  557. if (ms) {
  558. this.element.bind('tabsshow', rotate);
  559. this.anchors.bind(o.event + '.tabs', stop);
  560. rotate();
  561. }
  562. // stop rotation
  563. else {
  564. clearTimeout(self.rotation);
  565. this.element.unbind('tabsshow', rotate);
  566. this.anchors.unbind(o.event + '.tabs', stop);
  567. delete this._rotate;
  568. delete this._unrotate;
  569. }
  570. }
  571. });
  572. })(jQuery);