2 * jQuery UI Accordion 1.8.24
4 * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
5 * Dual licensed under the MIT or GPL Version 2 licenses.
6 * http://jquery.org/license
8 * http://docs.jquery.com/UI/Accordion
14 (function( $, undefined ) {
16 $.widget( "ui.accordion", {
25 header: "> li > :first-child,> :not(li):even",
27 header: "ui-icon-triangle-1-e",
28 headerSelected: "ui-icon-triangle-1-s"
31 navigationFilter: function() {
32 return this.href.toLowerCase() === location.href.toLowerCase();
38 options = self.options;
43 .addClass( "ui-accordion ui-widget ui-helper-reset" )
44 // in lack of child-selectors in CSS
45 // we need to mark top-LIs in a UL-accordion for some IE-fix
47 .addClass( "ui-accordion-li-fix" );
49 self.headers = self.element.find( options.header )
50 .addClass( "ui-accordion-header ui-helper-reset ui-state-default ui-corner-all" )
51 .bind( "mouseenter.accordion", function() {
52 if ( options.disabled ) {
55 $( this ).addClass( "ui-state-hover" );
57 .bind( "mouseleave.accordion", function() {
58 if ( options.disabled ) {
61 $( this ).removeClass( "ui-state-hover" );
63 .bind( "focus.accordion", function() {
64 if ( options.disabled ) {
67 $( this ).addClass( "ui-state-focus" );
69 .bind( "blur.accordion", function() {
70 if ( options.disabled ) {
73 $( this ).removeClass( "ui-state-focus" );
77 .addClass( "ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom" );
79 if ( options.navigation ) {
80 var current = self.element.find( "a" ).filter( options.navigationFilter ).eq( 0 );
81 if ( current.length ) {
82 var header = current.closest( ".ui-accordion-header" );
83 if ( header.length ) {
84 // anchor within header
87 // anchor within content
88 self.active = current.closest( ".ui-accordion-content" ).prev();
93 self.active = self._findActive( self.active || options.active )
94 .addClass( "ui-state-default ui-state-active" )
95 .toggleClass( "ui-corner-all" )
96 .toggleClass( "ui-corner-top" );
97 self.active.next().addClass( "ui-accordion-content-active" );
103 self.element.attr( "role", "tablist" );
106 .attr( "role", "tab" )
107 .bind( "keydown.accordion", function( event ) {
108 return self._keydown( event );
111 .attr( "role", "tabpanel" );
114 .not( self.active || "" )
116 "aria-expanded": "false",
117 "aria-selected": "false",
123 // make sure at least one header is in the tab order
124 if ( !self.active.length ) {
125 self.headers.eq( 0 ).attr( "tabIndex", 0 );
129 "aria-expanded": "true",
130 "aria-selected": "true",
135 // only need links in tab order for Safari
136 if ( !$.browser.safari ) {
137 self.headers.find( "a" ).attr( "tabIndex", -1 );
140 if ( options.event ) {
141 self.headers.bind( options.event.split(" ").join(".accordion ") + ".accordion", function(event) {
142 self._clickHandler.call( self, event, this );
143 event.preventDefault();
148 _createIcons: function() {
149 var options = this.options;
150 if ( options.icons ) {
152 .addClass( "ui-icon " + options.icons.header )
153 .prependTo( this.headers );
154 this.active.children( ".ui-icon" )
155 .toggleClass(options.icons.header)
156 .toggleClass(options.icons.headerSelected);
157 this.element.addClass( "ui-accordion-icons" );
161 _destroyIcons: function() {
162 this.headers.children( ".ui-icon" ).remove();
163 this.element.removeClass( "ui-accordion-icons" );
166 destroy: function() {
167 var options = this.options;
170 .removeClass( "ui-accordion ui-widget ui-helper-reset" )
171 .removeAttr( "role" );
174 .unbind( ".accordion" )
175 .removeClass( "ui-accordion-header ui-accordion-disabled ui-helper-reset ui-state-default ui-corner-all ui-state-active ui-state-disabled ui-corner-top" )
176 .removeAttr( "role" )
177 .removeAttr( "aria-expanded" )
178 .removeAttr( "aria-selected" )
179 .removeAttr( "tabIndex" );
181 this.headers.find( "a" ).removeAttr( "tabIndex" );
182 this._destroyIcons();
183 var contents = this.headers.next()
184 .css( "display", "" )
185 .removeAttr( "role" )
186 .removeClass( "ui-helper-reset ui-widget-content ui-corner-bottom ui-accordion-content ui-accordion-content-active ui-accordion-disabled ui-state-disabled" );
187 if ( options.autoHeight || options.fillHeight ) {
188 contents.css( "height", "" );
191 return $.Widget.prototype.destroy.call( this );
194 _setOption: function( key, value ) {
195 $.Widget.prototype._setOption.apply( this, arguments );
197 if ( key == "active" ) {
198 this.activate( value );
200 if ( key == "icons" ) {
201 this._destroyIcons();
206 // #5332 - opacity doesn't cascade to positioned elements in IE
207 // so we need to add the disabled class to the headers and panels
208 if ( key == "disabled" ) {
209 this.headers.add(this.headers.next())
210 [ value ? "addClass" : "removeClass" ](
211 "ui-accordion-disabled ui-state-disabled" );
215 _keydown: function( event ) {
216 if ( this.options.disabled || event.altKey || event.ctrlKey ) {
220 var keyCode = $.ui.keyCode,
221 length = this.headers.length,
222 currentIndex = this.headers.index( event.target ),
225 switch ( event.keyCode ) {
228 toFocus = this.headers[ ( currentIndex + 1 ) % length ];
232 toFocus = this.headers[ ( currentIndex - 1 + length ) % length ];
236 this._clickHandler( { target: event.target }, event.target );
237 event.preventDefault();
241 $( event.target ).attr( "tabIndex", -1 );
242 $( toFocus ).attr( "tabIndex", 0 );
251 var options = this.options,
254 if ( options.fillSpace ) {
255 if ( $.browser.msie ) {
256 var defOverflow = this.element.parent().css( "overflow" );
257 this.element.parent().css( "overflow", "hidden");
259 maxHeight = this.element.parent().height();
260 if ($.browser.msie) {
261 this.element.parent().css( "overflow", defOverflow );
264 this.headers.each(function() {
265 maxHeight -= $( this ).outerHeight( true );
270 $( this ).height( Math.max( 0, maxHeight -
271 $( this ).innerHeight() + $( this ).height() ) );
273 .css( "overflow", "auto" );
274 } else if ( options.autoHeight ) {
278 maxHeight = Math.max( maxHeight, $( this ).height( "" ).height() );
280 .height( maxHeight );
286 activate: function( index ) {
287 // TODO this gets called on init, changing the option without an explicit call for that
288 this.options.active = index;
289 // call clickHandler with custom event
290 var active = this._findActive( index )[ 0 ];
291 this._clickHandler( { target: active }, active );
296 _findActive: function( selector ) {
298 ? typeof selector === "number"
299 ? this.headers.filter( ":eq(" + selector + ")" )
300 : this.headers.not( this.headers.not( selector ) )
303 : this.headers.filter( ":eq(0)" );
306 // TODO isn't event.target enough? why the separate target argument?
307 _clickHandler: function( event, target ) {
308 var options = this.options;
309 if ( options.disabled ) {
313 // called only when using activate(false) to close all parts programmatically
314 if ( !event.target ) {
315 if ( !options.collapsible ) {
319 .removeClass( "ui-state-active ui-corner-top" )
320 .addClass( "ui-state-default ui-corner-all" )
321 .children( ".ui-icon" )
322 .removeClass( options.icons.headerSelected )
323 .addClass( options.icons.header );
324 this.active.next().addClass( "ui-accordion-content-active" );
325 var toHide = this.active.next(),
329 oldHeader: options.active,
333 toShow = ( this.active = $( [] ) );
334 this._toggle( toShow, toHide, data );
338 // get the click target
339 var clicked = $( event.currentTarget || target ),
340 clickedIsActive = clicked[0] === this.active[0];
342 // TODO the option is changed, is that correct?
343 // TODO if it is correct, shouldn't that happen after determining that the click is valid?
344 options.active = options.collapsible && clickedIsActive ?
346 this.headers.index( clicked );
348 // if animations are still active, or the active header is the target, ignore click
349 if ( this.running || ( !options.collapsible && clickedIsActive ) ) {
353 // find elements to show and hide
354 var active = this.active,
355 toShow = clicked.next(),
356 toHide = this.active.next(),
359 newHeader: clickedIsActive && options.collapsible ? $([]) : clicked,
360 oldHeader: this.active,
361 newContent: clickedIsActive && options.collapsible ? $([]) : toShow,
364 down = this.headers.index( this.active[0] ) > this.headers.index( clicked[0] );
366 // when the call to ._toggle() comes after the class changes
367 // it causes a very odd bug in IE 8 (see #6720)
368 this.active = clickedIsActive ? $([]) : clicked;
369 this._toggle( toShow, toHide, data, clickedIsActive, down );
373 .removeClass( "ui-state-active ui-corner-top" )
374 .addClass( "ui-state-default ui-corner-all" )
375 .children( ".ui-icon" )
376 .removeClass( options.icons.headerSelected )
377 .addClass( options.icons.header );
378 if ( !clickedIsActive ) {
380 .removeClass( "ui-state-default ui-corner-all" )
381 .addClass( "ui-state-active ui-corner-top" )
382 .children( ".ui-icon" )
383 .removeClass( options.icons.header )
384 .addClass( options.icons.headerSelected );
387 .addClass( "ui-accordion-content-active" );
393 _toggle: function( toShow, toHide, data, clickedIsActive, down ) {
395 options = self.options;
397 self.toShow = toShow;
398 self.toHide = toHide;
401 var complete = function() {
405 return self._completed.apply( self, arguments );
408 // trigger changestart event
409 self._trigger( "changestart", null, self.data );
411 // count elements to animate
412 self.running = toHide.size() === 0 ? toShow.size() : toHide.size();
414 if ( options.animated ) {
415 var animOptions = {};
417 if ( options.collapsible && clickedIsActive ) {
423 autoHeight: options.autoHeight || options.fillSpace
431 autoHeight: options.autoHeight || options.fillSpace
435 if ( !options.proxied ) {
436 options.proxied = options.animated;
439 if ( !options.proxiedDuration ) {
440 options.proxiedDuration = options.duration;
443 options.animated = $.isFunction( options.proxied ) ?
444 options.proxied( animOptions ) :
447 options.duration = $.isFunction( options.proxiedDuration ) ?
448 options.proxiedDuration( animOptions ) :
449 options.proxiedDuration;
451 var animations = $.ui.accordion.animations,
452 duration = options.duration,
453 easing = options.animated;
455 if ( easing && !animations[ easing ] && !$.easing[ easing ] ) {
458 if ( !animations[ easing ] ) {
459 animations[ easing ] = function( options ) {
460 this.slide( options, {
462 duration: duration || 700
467 animations[ easing ]( animOptions );
469 if ( options.collapsible && clickedIsActive ) {
479 // TODO assert that the blur and focus triggers are really necessary, remove otherwise
482 "aria-expanded": "false",
483 "aria-selected": "false",
489 "aria-expanded": "true",
490 "aria-selected": "true",
496 _completed: function( cancel ) {
497 this.running = cancel ? 0 : --this.running;
498 if ( this.running ) {
502 if ( this.options.clearStyle ) {
503 this.toShow.add( this.toHide ).css({
509 // other classes are removed before the animation; this one needs to stay until completed
510 this.toHide.removeClass( "ui-accordion-content-active" );
511 // Work around for rendering bug in IE (#5421)
512 if ( this.toHide.length ) {
513 this.toHide.parent()[0].className = this.toHide.parent()[0].className;
516 this._trigger( "change", null, this.data );
520 $.extend( $.ui.accordion, {
523 slide: function( options, additions ) {
527 }, options, additions );
528 if ( !options.toHide.size() ) {
529 options.toShow.animate({
532 paddingBottom: "show"
536 if ( !options.toShow.size() ) {
537 options.toHide.animate({
540 paddingBottom: "hide"
544 var overflow = options.toShow.css( "overflow" ),
548 fxAttrs = [ "height", "paddingTop", "paddingBottom" ],
550 // fix width before calculating height of hidden element
551 var s = options.toShow;
552 originalWidth = s[0].style.width;
553 s.width( s.parent().width()
554 - parseFloat( s.css( "paddingLeft" ) )
555 - parseFloat( s.css( "paddingRight" ) )
556 - ( parseFloat( s.css( "borderLeftWidth" ) ) || 0 )
557 - ( parseFloat( s.css( "borderRightWidth" ) ) || 0 ) );
559 $.each( fxAttrs, function( i, prop ) {
560 hideProps[ prop ] = "hide";
562 var parts = ( "" + $.css( options.toShow[0], prop ) ).match( /^([\d+-.]+)(.*)$/ );
563 showProps[ prop ] = {
565 unit: parts[ 2 ] || "px"
568 options.toShow.css({ height: 0, overflow: "hidden" }).show();
571 .each( options.complete )
573 .filter( ":visible" )
574 .animate( hideProps, {
575 step: function( now, settings ) {
576 // only calculate the percent when animating height
577 // IE gets very inconsistent results when animating elements
578 // with small values, which is common for padding
579 if ( settings.prop == "height" ) {
580 percentDone = ( settings.end - settings.start === 0 ) ? 0 :
581 ( settings.now - settings.start ) / ( settings.end - settings.start );
584 options.toShow[ 0 ].style[ settings.prop ] =
585 ( percentDone * showProps[ settings.prop ].value )
586 + showProps[ settings.prop ].unit;
588 duration: options.duration,
589 easing: options.easing,
590 complete: function() {
591 if ( !options.autoHeight ) {
592 options.toShow.css( "height", "" );
595 width: originalWidth,
602 bounceslide: function( options ) {
603 this.slide( options, {
604 easing: options.down ? "easeOutBounce" : "swing",
605 duration: options.down ? 1000 : 200