ui.tabs.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. /*
  2. * Tabs 3 - New Wave Tabs
  3. *
  4. * Copyright (c) 2007 Klaus Hartl (stilbuero.de)
  5. * Dual licensed under the MIT (MIT-LICENSE.txt)
  6. * and GPL (GPL-LICENSE.txt) licenses.
  7. */
  8. (function($) {
  9. // if the UI scope is not availalable, add it
  10. $.ui = $.ui || {};
  11. // tabs initialization
  12. $.fn.tabs = function(initial, options) {
  13. if (initial && initial.constructor == Object) { // shift arguments
  14. options = initial;
  15. initial = null;
  16. }
  17. options = options || {};
  18. initial = initial && initial.constructor == Number && --initial || 0;
  19. return this.each(function() {
  20. new $.ui.tabs(this, $.extend(options, { initial: initial }));
  21. });
  22. };
  23. // other chainable tabs methods
  24. $.each(['Add', 'Remove', 'Enable', 'Disable', 'Click', 'Load'], function(i, method) {
  25. $.fn['tabs' + method] = function() {
  26. var args = arguments;
  27. return this.each(function() {
  28. var instance = $.ui.tabs.getInstance(this);
  29. instance[method.toLowerCase()].apply(instance, args);
  30. });
  31. };
  32. });
  33. $.fn.tabsSelected = function() {
  34. var selected = -1;
  35. if (this[0]) {
  36. var instance = $.ui.tabs.getInstance(this[0]),
  37. $lis = $('li', this);
  38. selected = $lis.index( $lis.filter('.' + instance.options.selectedClass)[0] );
  39. }
  40. return selected >= 0 ? ++selected : -1;
  41. };
  42. // tabs class
  43. $.ui.tabs = function(el, options) {
  44. this.source = el;
  45. this.options = $.extend({
  46. // basic setup
  47. initial: 0,
  48. event: 'click',
  49. disabled: [],
  50. // TODO bookmarkable: $.ajaxHistory ? true : false,
  51. unselected: false,
  52. unselect: options.unselected ? true : false,
  53. // Ajax
  54. spinner: 'Loading…',
  55. cache: false,
  56. idPrefix: 'tab-',
  57. // animations
  58. /*fxFade: null,
  59. fxSlide: null,
  60. fxShow: null,
  61. fxHide: null,*/
  62. fxSpeed: 'normal',
  63. /*fxShowSpeed: null,
  64. fxHideSpeed: null,*/
  65. // callbacks
  66. add: function() {},
  67. remove: function() {},
  68. enable: function() {},
  69. disable: function() {},
  70. click: function() {},
  71. hide: function() {},
  72. show: function() {},
  73. load: function() {},
  74. // CSS classes
  75. navClass: 'ui-tabs-nav',
  76. selectedClass: 'ui-tabs-selected',
  77. disabledClass: 'ui-tabs-disabled',
  78. containerClass: 'ui-tabs-container',
  79. hideClass: 'ui-tabs-hide',
  80. loadingClass: 'ui-tabs-loading'
  81. }, options);
  82. this.tabify(true);
  83. // save instance for later
  84. var uuid = 'tabs' + $.ui.tabs.prototype.count++;
  85. $.ui.tabs.instances[uuid] = this;
  86. $.data(el, 'tabsUUID', uuid);
  87. };
  88. // static
  89. $.ui.tabs.instances = {};
  90. $.ui.tabs.getInstance = function(el) {
  91. return $.ui.tabs.instances[$.data(el, 'tabsUUID')];
  92. };
  93. // instance methods
  94. $.extend($.ui.tabs.prototype, {
  95. count: 0,
  96. tabify: function(init) {
  97. this.$tabs = $('a:first-child', this.source);
  98. this.$containers = $([]);
  99. var self = this, o = this.options;
  100. this.$tabs.each(function(i, a) {
  101. // inline tab
  102. if (a.hash && a.hash.replace('#', '')) { // safari 2 reports '#' for an empty hash
  103. self.$containers = self.$containers.add(a.hash);
  104. }
  105. // remote tab
  106. else {
  107. $.data(a, 'href', a.href);
  108. var id = a.title && a.title.replace(/\s/g, '_') || o.idPrefix + (self.count + 1) + '-' + (i + 1);
  109. a.href = '#' + id;
  110. self.$containers = self.$containers.add(
  111. $('#' + id)[0] || $('<div id="' + id + '" class="' + o.containerClass + '"></div>')
  112. .insertAfter( self.$containers[i - 1] || self.source )
  113. );
  114. }
  115. });
  116. if (init) {
  117. // Try to retrieve initial tab from fragment identifier in url if present,
  118. // otherwise try to find selected class attribute on <li>.
  119. this.$tabs.each(function(i, a) {
  120. if (location.hash) {
  121. if (a.hash == location.hash) {
  122. o.initial = i;
  123. // prevent page scroll to fragment
  124. //if (($.browser.msie || $.browser.opera) && !o.remote) {
  125. if ($.browser.msie || $.browser.opera) {
  126. var $toShow = $(location.hash), toShowId = $toShow.attr('id');
  127. $toShow.attr('id', '');
  128. setTimeout(function() {
  129. $toShow.attr('id', toShowId); // restore id
  130. }, 500);
  131. }
  132. scrollTo(0, 0);
  133. return false; // break
  134. }
  135. } else if ( $(a).parents('li:eq(0)').is('li.' + o.selectedClass) ) {
  136. o.initial = i;
  137. return false; // break
  138. }
  139. });
  140. // attach necessary classes for styling if not present
  141. $(this.source).is('.' + o.navClass) || $(this.source).addClass(o.navClass);
  142. this.$containers.each(function() {
  143. var $this = $(this);
  144. $this.is('.' + o.containerClass) || $this.addClass(o.containerClass);
  145. });
  146. // highlight tab
  147. var $lis = $('li', this.source);
  148. this.$containers.addClass(o.hideClass);
  149. $lis.removeClass(o.selectedClass);
  150. if (!o.unselected) {
  151. this.$containers.slice(o.initial, o.initial + 1).show();
  152. $lis.slice(o.initial, o.initial + 1).addClass(o.selectedClass);
  153. }
  154. // load if remote tab
  155. if ($.data(this.$tabs[o.initial], 'href')) {
  156. this.load(o.initial + 1, $.data(this.$tabs[o.initial], 'href'));
  157. if (o.cache) {
  158. $.removeData(this.$tabs[o.initial], 'href'); // if loaded once do not load them again
  159. }
  160. }
  161. // disabled tabs
  162. for (var i = 0, position; position = o.disabled[i]; i++) {
  163. this.disable(position);
  164. }
  165. }
  166. // setup animations
  167. var showAnim = {}, showSpeed = o.fxShowSpeed || o.fxSpeed,
  168. hideAnim = {}, hideSpeed = o.fxHideSpeed || o.fxSpeed;
  169. if (o.fxSlide || o.fxFade) {
  170. if (o.fxSlide) {
  171. showAnim['height'] = 'show';
  172. hideAnim['height'] = 'hide';
  173. }
  174. if (o.fxFade) {
  175. showAnim['opacity'] = 'show';
  176. hideAnim['opacity'] = 'hide';
  177. }
  178. } else {
  179. if (o.fxShow) {
  180. showAnim = o.fxShow;
  181. } else { // use some kind of animation to prevent browser scrolling to the tab
  182. showAnim['min-width'] = 0; // avoid opacity, causes flicker in Firefox
  183. showSpeed = 1; // as little as 1 is sufficient
  184. }
  185. if (o.fxHide) {
  186. hideAnim = o.fxHide;
  187. } else { // use some kind of animation to prevent browser scrolling to the tab
  188. hideAnim['min-width'] = 0; // avoid opacity, causes flicker in Firefox
  189. hideSpeed = 1; // as little as 1 is sufficient
  190. }
  191. }
  192. // reset some styles to maintain print style sheets etc.
  193. var resetCSS = { display: '', overflow: '', height: '' };
  194. if (!$.browser.msie) { // not in IE to prevent ClearType font issue
  195. resetCSS['opacity'] = '';
  196. }
  197. // Hide a tab, animation prevents browser scrolling to fragment,
  198. // $show is optional.
  199. function hideTab(clicked, $hide, $show) {
  200. $hide.animate(hideAnim, hideSpeed, function() { //
  201. $hide.addClass(o.hideClass).css(resetCSS); // maintain flexible height and accessibility in print etc.
  202. if ($.browser.msie) {
  203. $hide[0].style.filter = '';
  204. }
  205. o.hide(clicked, $hide[0], $show && $show[0] || null);
  206. if ($show) {
  207. showTab(clicked, $show, $hide);
  208. }
  209. });
  210. }
  211. // Show a tab, animation prevents browser scrolling to fragment,
  212. // $hide is optional
  213. function showTab(clicked, $show, $hide) {
  214. if (!(o.fxSlide || o.fxFade || o.fxShow)) {
  215. $show.css('display', 'block'); // prevent occasionally occuring flicker in Firefox cause by gap between showing and hiding the tab containers
  216. }
  217. $show.animate(showAnim, showSpeed, function() {
  218. $show.removeClass(o.hideClass).css(resetCSS); // maintain flexible height and accessibility in print etc.
  219. if ($.browser.msie) {
  220. $show[0].style.filter = '';
  221. }
  222. o.show(clicked, $show[0], $hide && $hide[0] || null);
  223. });
  224. }
  225. // switch a tab
  226. function switchTab(clicked, $hide, $show) {
  227. /*if (o.bookmarkable && trueClick) { // add to history only if true click occured, not a triggered click
  228. $.ajaxHistory.update(clicked.hash);
  229. }*/
  230. $(clicked).parents('li:eq(0)').addClass(o.selectedClass)
  231. .siblings().removeClass(o.selectedClass);
  232. hideTab(clicked, $hide, $show);
  233. }
  234. // tab click handler
  235. function tabClick(e) {
  236. //var trueClick = e.clientX; // add to history only if true click occured, not a triggered click
  237. var $li = $(this).parents('li:eq(0)'),
  238. $hide = self.$containers.filter(':visible'),
  239. $show = $(this.hash);
  240. // If tab is already selected and not unselectable or tab disabled or click callback returns false stop here.
  241. // Check if click handler returns false last so that it is not executed for a disabled tab!
  242. if (($li.is('.' + o.selectedClass) && !o.unselect) || $li.is('.' + o.disabledClass)
  243. || o.click(this, $show[0], $hide[0]) === false) {
  244. this.blur();
  245. return false;
  246. }
  247. // if tab may be closed
  248. if (o.unselect) {
  249. if ($li.is('.' + o.selectedClass)) {
  250. $li.removeClass(o.selectedClass);
  251. self.$containers.stop();
  252. hideTab(this, $hide);
  253. this.blur();
  254. return false;
  255. } else if (!$hide.length) {
  256. $li.addClass(o.selectedClass);
  257. self.$containers.stop();
  258. showTab(this, $show);
  259. this.blur();
  260. return false;
  261. }
  262. }
  263. // stop possibly running animations
  264. self.$containers.stop();
  265. // show new tab
  266. if ($show.length) {
  267. // prevent scrollbar scrolling to 0 and than back in IE7, happens only if bookmarking/history is enabled
  268. /*if ($.browser.msie && o.bookmarkable) {
  269. var showId = this.hash.replace('#', '');
  270. $show.attr('id', '');
  271. setTimeout(function() {
  272. $show.attr('id', showId); // restore id
  273. }, 0);
  274. }*/
  275. if ($.data(this, 'href')) { // remote tab
  276. var a = this;
  277. self.load(self.$tabs.index(this) + 1, $.data(this, 'href'), function() {
  278. switchTab(a, $hide, $show);
  279. });
  280. if (o.cache) {
  281. $.removeData(this, 'href'); // if loaded once do not load them again
  282. }
  283. } else {
  284. switchTab(this, $hide, $show);
  285. }
  286. // Set scrollbar to saved position - need to use timeout with 0 to prevent browser scroll to target of hash
  287. /*var scrollX = window.pageXOffset || document.documentElement && document.documentElement.scrollLeft || document.body.scrollLeft || 0;
  288. var scrollY = window.pageYOffset || document.documentElement && document.documentElement.scrollTop || document.body.scrollTop || 0;
  289. setTimeout(function() {
  290. scrollTo(scrollX, scrollY);
  291. }, 0);*/
  292. } else {
  293. throw 'jQuery UI Tabs: Mismatching fragment identifier.';
  294. }
  295. this.blur(); // prevent IE from keeping other link focussed when using the back button
  296. //return o.bookmarkable && !!trueClick; // convert trueClick == undefined to Boolean required in IE
  297. return false;
  298. }
  299. // attach click event, avoid duplicates from former tabifying
  300. this.$tabs.unbind(o.event, tabClick).bind(o.event, tabClick);
  301. },
  302. add: function(url, text, position) {
  303. if (url && text) {
  304. var o = this.options;
  305. position = position || this.$tabs.length; // append by default
  306. if (position >= this.$tabs.length) {
  307. var method = 'insertAfter';
  308. position = this.$tabs.length;
  309. } else {
  310. var method = 'insertBefore';
  311. }
  312. if (url.indexOf('#') == 0) { // ajax container is created by tabify automatically
  313. var $container = $(url);
  314. // try to find an existing element before creating a new one
  315. ($container.length && $container || $('<div id="' + url.replace('#', '') + '" class="' + o.containerClass + ' ' + o.hideClass + '"></div>'))
  316. [method](this.$containers[position - 1]);
  317. }
  318. $('<li><a href="' + url + '"><span>' + text + '</span></a></li>')
  319. [method](this.$tabs.slice(position - 1, position).parents('li:eq(0)'));
  320. this.tabify();
  321. o.add(this.$tabs[position - 1], this.$containers[position - 1]); // callback
  322. } else {
  323. throw 'jQuery UI Tabs: Not enough arguments to add tab.';
  324. }
  325. },
  326. remove: function(position) {
  327. if (position && position.constructor == Number) {
  328. var $removedTab = this.$tabs.slice(position - 1, position).parents('li:eq(0)').remove();
  329. var $removedContainer = this.$containers.slice(position - 1, position).remove();
  330. this.tabify();
  331. this.options.remove($removedTab[0], $removedContainer[0]); // callback
  332. }
  333. },
  334. enable: function(position) {
  335. var $li = this.$tabs.slice(position - 1, position).parents('li:eq(0)'), o = this.options;
  336. $li.removeClass(o.disabledClass);
  337. if ($.browser.safari) { // fix disappearing tab after enabling in Safari... TODO check Safari 3
  338. $li.animate({ opacity: 1 }, 1, function() {
  339. $li.css({ opacity: '' });
  340. });
  341. }
  342. o.enable(this.$tabs[position - 1], this.$containers[position - 1]); // callback
  343. },
  344. disable: function(position) {
  345. var $li = this.$tabs.slice(position - 1, position).parents('li:eq(0)'), o = this.options;
  346. if ($.browser.safari) { // fix opacity of tab after disabling in Safari... TODO check Safari 3
  347. $li.animate({ opacity: 0 }, 1, function() {
  348. $li.css({ opacity: '' });
  349. });
  350. }
  351. $li.addClass(this.options.disabledClass);
  352. o.disable(this.$tabs[position - 1], this.$containers[position - 1]); // callback
  353. },
  354. click: function(position) {
  355. this.$tabs.slice(position - 1, position).trigger('click');
  356. },
  357. load: function(position, url, callback) {
  358. var self = this,
  359. o = this.options,
  360. $a = this.$tabs.slice(position - 1, position).addClass(o.loadingClass),
  361. $span = $('span', $a),
  362. text = $span.html();
  363. // shift arguments
  364. if (url && url.constructor == Function) {
  365. callback = url;
  366. }
  367. // set new URL
  368. if (url) {
  369. $.data($a[0], 'href', url);
  370. }
  371. // load
  372. if (o.spinner) {
  373. $span.html('<em>' + o.spinner + '</em>');
  374. }
  375. setTimeout(function() { // timeout is again required in IE, "wait" for id being restored
  376. $($a[0].hash).load(url, function() {
  377. if (o.spinner) {
  378. $span.html(text);
  379. }
  380. $a.removeClass(o.loadingClass);
  381. // This callback is required because the switch has to take place after loading
  382. // has completed.
  383. if (callback && callback.constructor == Function) {
  384. callback();
  385. }
  386. o.load(self.$tabs[position - 1], self.$containers[position - 1]); // callback
  387. });
  388. }, 0);
  389. }
  390. });
  391. })(jQuery);