ui.tabs.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594
  1. /*
  2. * jQuery UI Tabs
  3. *
  4. * Copyright (c) 2007, 2008 Klaus Hartl (stilbuero.de)
  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. * Revision: $Id: ui.tabs.js 5547 2008-05-10 08:33:38Z klaus.hartl $
  14. */
  15. ;(function($) {
  16. $.widget("ui.tabs", {
  17. init: function() {
  18. this.options.event += '.tabs'; // namespace event
  19. // create tabs
  20. this.tabify(true);
  21. },
  22. setData: function(key, value) {
  23. if ((/^selected/).test(key))
  24. this.select(value);
  25. else {
  26. this.options[key] = value;
  27. this.tabify();
  28. }
  29. },
  30. length: function() {
  31. return this.$tabs.length;
  32. },
  33. tabId: function(a) {
  34. return a.title && a.title.replace(/\s/g, '_').replace(/[^A-Za-z0-9\-_:\.]/g, '')
  35. || this.options.idPrefix + $.data(a);
  36. },
  37. ui: function(tab, panel) {
  38. return {
  39. instance: this,
  40. options: this.options,
  41. tab: tab,
  42. panel: panel
  43. };
  44. },
  45. tabify: function(init) {
  46. this.$lis = $('li:has(a[href])', this.element);
  47. this.$tabs = this.$lis.map(function() { return $('a', this)[0]; });
  48. this.$panels = $([]);
  49. var self = this, o = this.options;
  50. this.$tabs.each(function(i, a) {
  51. // inline tab
  52. if (a.hash && a.hash.replace('#', '')) // Safari 2 reports '#' for an empty hash
  53. self.$panels = self.$panels.add(a.hash);
  54. // remote tab
  55. else if ($(a).attr('href') != '#') { // prevent loading the page itself if href is just "#"
  56. $.data(a, 'href.tabs', a.href); // required for restore on destroy
  57. $.data(a, 'load.tabs', a.href); // mutable
  58. var id = self.tabId(a);
  59. a.href = '#' + id;
  60. var $panel = $('#' + id);
  61. if (!$panel.length) {
  62. $panel = $(o.panelTemplate).attr('id', id).addClass(o.panelClass)
  63. .insertAfter( self.$panels[i - 1] || self.element );
  64. $panel.data('destroy.tabs', true);
  65. }
  66. self.$panels = self.$panels.add( $panel );
  67. }
  68. // invalid tab href
  69. else
  70. o.disabled.push(i + 1);
  71. });
  72. if (init) {
  73. // attach necessary classes for styling if not present
  74. this.element.hasClass(o.navClass) || this.element.addClass(o.navClass);
  75. this.$panels.each(function() {
  76. var $this = $(this);
  77. $this.hasClass(o.panelClass) || $this.addClass(o.panelClass);
  78. });
  79. // Selected tab
  80. // use "selected" option or try to retrieve:
  81. // 1. from fragment identifier in url
  82. // 2. from cookie
  83. // 3. from selected class attribute on <li>
  84. if (o.selected === undefined) {
  85. if (location.hash) {
  86. this.$tabs.each(function(i, a) {
  87. if (a.hash == location.hash) {
  88. o.selected = i;
  89. // prevent page scroll to fragment
  90. if ($.browser.msie || $.browser.opera) { // && !o.remote
  91. var $toShow = $(location.hash), toShowId = $toShow.attr('id');
  92. $toShow.attr('id', '');
  93. setTimeout(function() {
  94. $toShow.attr('id', toShowId); // restore id
  95. }, 500);
  96. }
  97. scrollTo(0, 0);
  98. return false; // break
  99. }
  100. });
  101. }
  102. else if (o.cookie) {
  103. var index = parseInt($.cookie('ui-tabs' + $.data(self.element)),10);
  104. if (index && self.$tabs[index])
  105. o.selected = index;
  106. }
  107. else if (self.$lis.filter('.' + o.selectedClass).length)
  108. o.selected = self.$lis.index( self.$lis.filter('.' + o.selectedClass)[0] );
  109. }
  110. o.selected = o.selected === null || o.selected !== undefined ? o.selected : 0; // first tab selected by default
  111. // Take disabling tabs via class attribute from HTML
  112. // into account and update option properly.
  113. // A selected tab cannot become disabled.
  114. o.disabled = $.unique(o.disabled.concat(
  115. $.map(this.$lis.filter('.' + o.disabledClass),
  116. function(n, i) { return self.$lis.index(n); } )
  117. )).sort();
  118. if ($.inArray(o.selected, o.disabled) != -1)
  119. o.disabled.splice($.inArray(o.selected, o.disabled), 1);
  120. // highlight selected tab
  121. this.$panels.addClass(o.hideClass);
  122. this.$lis.removeClass(o.selectedClass);
  123. if (o.selected !== null) {
  124. this.$panels.eq(o.selected).show().removeClass(o.hideClass); // use show and remove class to show in any case no matter how it has been hidden before
  125. this.$lis.eq(o.selected).addClass(o.selectedClass);
  126. // seems to be expected behavior that the show callback is fired
  127. var onShow = function() {
  128. $(self.element).triggerHandler('tabsshow',
  129. [self.ui(self.$tabs[o.selected], self.$panels[o.selected])], o.show);
  130. };
  131. // load if remote tab
  132. if ($.data(this.$tabs[o.selected], 'load.tabs'))
  133. this.load(o.selected, onShow);
  134. // just trigger show event
  135. else
  136. onShow();
  137. }
  138. // clean up to avoid memory leaks in certain versions of IE 6
  139. $(window).bind('unload', function() {
  140. self.$tabs.unbind('.tabs');
  141. self.$lis = self.$tabs = self.$panels = null;
  142. });
  143. }
  144. // disable tabs
  145. for (var i = 0, li; li = this.$lis[i]; i++)
  146. $(li)[$.inArray(i, o.disabled) != -1 && !$(li).hasClass(o.selectedClass) ? 'addClass' : 'removeClass'](o.disabledClass);
  147. // reset cache if switching from cached to not cached
  148. if (o.cache === false)
  149. this.$tabs.removeData('cache.tabs');
  150. // set up animations
  151. var hideFx, showFx, baseFx = { 'min-width': 0, duration: 1 }, baseDuration = 'normal';
  152. if (o.fx && o.fx.constructor == Array)
  153. hideFx = o.fx[0] || baseFx, showFx = o.fx[1] || baseFx;
  154. else
  155. hideFx = showFx = o.fx || baseFx;
  156. // reset some styles to maintain print style sheets etc.
  157. var resetCSS = { display: '', overflow: '', height: '' };
  158. if (!$.browser.msie) // not in IE to prevent ClearType font issue
  159. resetCSS.opacity = '';
  160. // Hide a tab, animation prevents browser scrolling to fragment,
  161. // $show is optional.
  162. function hideTab(clicked, $hide, $show) {
  163. $hide.animate(hideFx, hideFx.duration || baseDuration, function() { //
  164. $hide.addClass(o.hideClass).css(resetCSS); // maintain flexible height and accessibility in print etc.
  165. if ($.browser.msie && hideFx.opacity)
  166. $hide[0].style.filter = '';
  167. if ($show)
  168. showTab(clicked, $show, $hide);
  169. });
  170. }
  171. // Show a tab, animation prevents browser scrolling to fragment,
  172. // $hide is optional.
  173. function showTab(clicked, $show, $hide) {
  174. if (showFx === baseFx)
  175. $show.css('display', 'block'); // prevent occasionally occuring flicker in Firefox cause by gap between showing and hiding the tab panels
  176. $show.animate(showFx, showFx.duration || baseDuration, function() {
  177. $show.removeClass(o.hideClass).css(resetCSS); // maintain flexible height and accessibility in print etc.
  178. if ($.browser.msie && showFx.opacity)
  179. $show[0].style.filter = '';
  180. // callback
  181. $(self.element).triggerHandler('tabsshow',
  182. [self.ui(clicked, $show[0])], o.show);
  183. });
  184. }
  185. // switch a tab
  186. function switchTab(clicked, $li, $hide, $show) {
  187. /*if (o.bookmarkable && trueClick) { // add to history only if true click occured, not a triggered click
  188. $.ajaxHistory.update(clicked.hash);
  189. }*/
  190. $li.addClass(o.selectedClass)
  191. .siblings().removeClass(o.selectedClass);
  192. hideTab(clicked, $hide, $show);
  193. }
  194. // attach tab event handler, unbind to avoid duplicates from former tabifying...
  195. this.$tabs.unbind('.tabs').bind(o.event, function() {
  196. //var trueClick = e.clientX; // add to history only if true click occured, not a triggered click
  197. var $li = $(this).parents('li:eq(0)'),
  198. $hide = self.$panels.filter(':visible'),
  199. $show = $(this.hash);
  200. // If tab is already selected and not unselectable or tab disabled or
  201. // or is already loading or click callback returns false stop here.
  202. // Check if click handler returns false last so that it is not executed
  203. // for a disabled or loading tab!
  204. if (($li.hasClass(o.selectedClass) && !o.unselect)
  205. || $li.hasClass(o.disabledClass)
  206. || $(this).hasClass(o.loadingClass)
  207. || $(self.element).triggerHandler('tabsselect', [self.ui(this, $show[0])], o.select) === false
  208. ) {
  209. this.blur();
  210. return false;
  211. }
  212. self.options.selected = self.$tabs.index(this);
  213. // if tab may be closed
  214. if (o.unselect) {
  215. if ($li.hasClass(o.selectedClass)) {
  216. self.options.selected = null;
  217. $li.removeClass(o.selectedClass);
  218. self.$panels.stop();
  219. hideTab(this, $hide);
  220. this.blur();
  221. return false;
  222. } else if (!$hide.length) {
  223. self.$panels.stop();
  224. var a = this;
  225. self.load(self.$tabs.index(this), function() {
  226. $li.addClass(o.selectedClass).addClass(o.unselectClass);
  227. showTab(a, $show);
  228. });
  229. this.blur();
  230. return false;
  231. }
  232. }
  233. if (o.cookie)
  234. $.cookie('ui-tabs' + $.data(self.element), self.options.selected, o.cookie);
  235. // stop possibly running animations
  236. self.$panels.stop();
  237. // show new tab
  238. if ($show.length) {
  239. // prevent scrollbar scrolling to 0 and than back in IE7, happens only if bookmarking/history is enabled
  240. /*if ($.browser.msie && o.bookmarkable) {
  241. var showId = this.hash.replace('#', '');
  242. $show.attr('id', '');
  243. setTimeout(function() {
  244. $show.attr('id', showId); // restore id
  245. }, 0);
  246. }*/
  247. var a = this;
  248. self.load(self.$tabs.index(this), $hide.length ?
  249. function() {
  250. switchTab(a, $li, $hide, $show);
  251. } :
  252. function() {
  253. $li.addClass(o.selectedClass);
  254. showTab(a, $show);
  255. }
  256. );
  257. // Set scrollbar to saved position - need to use timeout with 0 to prevent browser scroll to target of hash
  258. /*var scrollX = window.pageXOffset || document.documentElement && document.documentElement.scrollLeft || document.body.scrollLeft || 0;
  259. var scrollY = window.pageYOffset || document.documentElement && document.documentElement.scrollTop || document.body.scrollTop || 0;
  260. setTimeout(function() {
  261. scrollTo(scrollX, scrollY);
  262. }, 0);*/
  263. } else
  264. throw 'jQuery UI Tabs: Mismatching fragment identifier.';
  265. // Prevent IE from keeping other link focussed when using the back button
  266. // and remove dotted border from clicked link. This is controlled in modern
  267. // browsers via CSS, also blur removes focus from address bar in Firefox
  268. // which can become a usability and annoying problem with tabsRotate.
  269. if ($.browser.msie)
  270. this.blur();
  271. //return o.bookmarkable && !!trueClick; // convert trueClick == undefined to Boolean required in IE
  272. return false;
  273. });
  274. // disable click if event is configured to something else
  275. if (!(/^click/).test(o.event))
  276. this.$tabs.bind('click.tabs', function() { return false; });
  277. },
  278. add: function(url, label, index) {
  279. if (index == undefined)
  280. index = this.$tabs.length; // append by default
  281. var o = this.options;
  282. var $li = $(o.tabTemplate.replace(/#\{href\}/, url).replace(/#\{label\}/, label));
  283. $li.data('destroy.tabs', true);
  284. var id = url.indexOf('#') == 0 ? url.replace('#', '') : this.tabId( $('a:first-child', $li)[0] );
  285. // try to find an existing element before creating a new one
  286. var $panel = $('#' + id);
  287. if (!$panel.length) {
  288. $panel = $(o.panelTemplate).attr('id', id)
  289. .addClass(o.panelClass).addClass(o.hideClass);
  290. $panel.data('destroy.tabs', true);
  291. }
  292. if (index >= this.$lis.length) {
  293. $li.appendTo(this.element);
  294. $panel.appendTo(this.element[0].parentNode);
  295. } else {
  296. $li.insertBefore(this.$lis[index]);
  297. $panel.insertBefore(this.$panels[index]);
  298. }
  299. o.disabled = $.map(o.disabled,
  300. function(n, i) { return n >= index ? ++n : n });
  301. this.tabify();
  302. if (this.$tabs.length == 1) {
  303. $li.addClass(o.selectedClass);
  304. $panel.removeClass(o.hideClass);
  305. var href = $.data(this.$tabs[0], 'load.tabs');
  306. if (href)
  307. this.load(index, href);
  308. }
  309. // callback
  310. this.element.triggerHandler('tabsadd',
  311. [this.ui(this.$tabs[index], this.$panels[index])], o.add
  312. );
  313. },
  314. remove: function(index) {
  315. var o = this.options, $li = this.$lis.eq(index).remove(),
  316. $panel = this.$panels.eq(index).remove();
  317. // If selected tab was removed focus tab to the right or
  318. // in case the last tab was removed the tab to the left.
  319. if ($li.hasClass(o.selectedClass) && this.$tabs.length > 1)
  320. this.select(index + (index + 1 < this.$tabs.length ? 1 : -1));
  321. o.disabled = $.map($.grep(o.disabled, function(n, i) { return n != index; }),
  322. function(n, i) { return n >= index ? --n : n });
  323. this.tabify();
  324. // callback
  325. this.element.triggerHandler('tabsremove',
  326. [this.ui($li.find('a')[0], $panel[0])], o.remove
  327. );
  328. },
  329. enable: function(index) {
  330. var o = this.options;
  331. if ($.inArray(index, o.disabled) == -1)
  332. return;
  333. var $li = this.$lis.eq(index).removeClass(o.disabledClass);
  334. if ($.browser.safari) { // fix disappearing tab (that used opacity indicating disabling) after enabling in Safari 2...
  335. $li.css('display', 'inline-block');
  336. setTimeout(function() {
  337. $li.css('display', 'block');
  338. }, 0);
  339. }
  340. o.disabled = $.grep(o.disabled, function(n, i) { return n != index; });
  341. // callback
  342. this.element.triggerHandler('tabsenable',
  343. [this.ui(this.$tabs[index], this.$panels[index])], o.enable
  344. );
  345. },
  346. disable: function(index) {
  347. var self = this, o = this.options;
  348. if (index != o.selected) { // cannot disable already selected tab
  349. this.$lis.eq(index).addClass(o.disabledClass);
  350. o.disabled.push(index);
  351. o.disabled.sort();
  352. // callback
  353. this.element.triggerHandler('tabsdisable',
  354. [this.ui(this.$tabs[index], this.$panels[index])], o.disable
  355. );
  356. }
  357. },
  358. select: function(index) {
  359. if (typeof index == 'string')
  360. index = this.$tabs.index( this.$tabs.filter('[href$=' + index + ']')[0] );
  361. this.$tabs.eq(index).trigger(this.options.event);
  362. },
  363. load: function(index, callback) { // callback is for internal usage only
  364. var self = this, o = this.options, $a = this.$tabs.eq(index), a = $a[0],
  365. bypassCache = callback == undefined || callback === false, url = $a.data('load.tabs');
  366. callback = callback || function() {};
  367. // no remote or from cache - just finish with callback
  368. if (!url || !bypassCache && $.data(a, 'cache.tabs')) {
  369. callback();
  370. return;
  371. }
  372. // load remote from here on
  373. var inner = function(parent) {
  374. var $parent = $(parent), $inner = $parent.find('*:last');
  375. return $inner.length && $inner || $parent;
  376. };
  377. var cleanup = function() {
  378. self.$tabs.filter('.' + o.loadingClass).removeClass(o.loadingClass)
  379. .each(function() {
  380. if (o.spinner)
  381. inner(this).parent().html(inner(this).data('label.tabs'));
  382. });
  383. self.xhr = null;
  384. };
  385. if (o.spinner) {
  386. var label = inner(a).html();
  387. inner(a).wrapInner('<em></em>')
  388. .find('em').data('label.tabs', label).html(o.spinner);
  389. }
  390. var ajaxOptions = $.extend({}, o.ajaxOptions, {
  391. url: url,
  392. success: function(r, s) {
  393. $(a.hash).html(r);
  394. cleanup();
  395. if (o.cache)
  396. $.data(a, 'cache.tabs', true); // if loaded once do not load them again
  397. // callbacks
  398. $(self.element).triggerHandler('tabsload',
  399. [self.ui(self.$tabs[index], self.$panels[index])], o.load
  400. );
  401. o.ajaxOptions.success && o.ajaxOptions.success(r, s);
  402. // This callback is required because the switch has to take
  403. // place after loading has completed. Call last in order to
  404. // fire load before show callback...
  405. callback();
  406. }
  407. });
  408. if (this.xhr) {
  409. // terminate pending requests from other tabs and restore tab label
  410. this.xhr.abort();
  411. cleanup();
  412. }
  413. $a.addClass(o.loadingClass);
  414. setTimeout(function() { // timeout is again required in IE, "wait" for id being restored
  415. self.xhr = $.ajax(ajaxOptions);
  416. }, 0);
  417. },
  418. url: function(index, url) {
  419. this.$tabs.eq(index).removeData('cache.tabs').data('load.tabs', url);
  420. },
  421. destroy: function() {
  422. var o = this.options;
  423. this.element.unbind('.tabs')
  424. .removeClass(o.navClass).removeData('tabs');
  425. this.$tabs.each(function() {
  426. var href = $.data(this, 'href.tabs');
  427. if (href)
  428. this.href = href;
  429. var $this = $(this).unbind('.tabs');
  430. $.each(['href', 'load', 'cache'], function(i, prefix) {
  431. $this.removeData(prefix + '.tabs');
  432. });
  433. });
  434. this.$lis.add(this.$panels).each(function() {
  435. if ($.data(this, 'destroy.tabs'))
  436. $(this).remove();
  437. else
  438. $(this).removeClass([o.selectedClass, o.unselectClass,
  439. o.disabledClass, o.panelClass, o.hideClass].join(' '));
  440. });
  441. }
  442. });
  443. $.ui.tabs.defaults = {
  444. // basic setup
  445. unselect: false,
  446. event: 'click',
  447. disabled: [],
  448. cookie: null, // e.g. { expires: 7, path: '/', domain: 'jquery.com', secure: true }
  449. // TODO history: false,
  450. // Ajax
  451. spinner: 'Loading&#8230;',
  452. cache: false,
  453. idPrefix: 'ui-tabs-',
  454. ajaxOptions: {},
  455. // animations
  456. fx: null, // e.g. { height: 'toggle', opacity: 'toggle', duration: 200 }
  457. // templates
  458. tabTemplate: '<li><a href="#{href}"><span>#{label}</span></a></li>',
  459. panelTemplate: '<div></div>',
  460. // CSS classes
  461. navClass: 'ui-tabs-nav',
  462. selectedClass: 'ui-tabs-selected',
  463. unselectClass: 'ui-tabs-unselect',
  464. disabledClass: 'ui-tabs-disabled',
  465. panelClass: 'ui-tabs-panel',
  466. hideClass: 'ui-tabs-hide',
  467. loadingClass: 'ui-tabs-loading'
  468. };
  469. $.ui.tabs.getter = "length";
  470. /*
  471. * Tabs Extensions
  472. */
  473. /*
  474. * Rotate
  475. */
  476. $.extend($.ui.tabs.prototype, {
  477. rotation: null,
  478. rotate: function(ms, continuing) {
  479. continuing = continuing || false;
  480. var self = this, t = this.options.selected;
  481. function start() {
  482. self.rotation = setInterval(function() {
  483. t = ++t < self.$tabs.length ? t : 0;
  484. self.select(t);
  485. }, ms);
  486. }
  487. function stop(e) {
  488. if (!e || e.clientX) { // only in case of a true click
  489. clearInterval(self.rotation);
  490. }
  491. }
  492. // start interval
  493. if (ms) {
  494. start();
  495. if (!continuing)
  496. this.$tabs.bind(this.options.event, stop);
  497. else
  498. this.$tabs.bind(this.options.event, function() {
  499. stop();
  500. t = self.options.selected;
  501. start();
  502. });
  503. }
  504. // stop interval
  505. else {
  506. stop();
  507. this.$tabs.unbind(this.options.event, stop);
  508. }
  509. }
  510. });
  511. })(jQuery);