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