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