/* * jQuery UI Accordion @VERSION * * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) * Dual licensed under the MIT or GPL Version 2 licenses. * http://jquery.org/license * * http://docs.jquery.com/UI/Accordion * * Depends: * jquery.ui.core.js * jquery.ui.widget.js */ (function( $, undefined ) { $.widget( "ui.accordion", { options: { active: 0, animated: "slide", collapsible: false, event: "click", header: "> li > :first-child,> :not(li):even", // TODO: set to "auto" in 2.0 (#5868, #5872) heightStyle: null, // "auto" icons: { header: "ui-icon-triangle-1-e", // TODO: set to "ui-icon-triangle-1-s" in 2.0 (#6835) activeHeader: null // "ui-icon-triangle-1-s" } }, _create: function() { var self = this, options = self.options; self.running = 0; self.element .addClass( "ui-accordion ui-widget ui-helper-reset" ) // in lack of child-selectors in CSS // we need to mark top-LIs in a UL-accordion for some IE-fix .children( "li" ) .addClass( "ui-accordion-li-fix" ); self.headers = self.element.find( options.header ) .addClass( "ui-accordion-header ui-helper-reset ui-state-default ui-corner-all" ) .bind( "mouseenter.accordion", function() { if ( options.disabled ) { return; } $( this ).addClass( "ui-state-hover" ); }) .bind( "mouseleave.accordion", function() { if ( options.disabled ) { return; } $( this ).removeClass( "ui-state-hover" ); }) .bind( "focus.accordion", function() { if ( options.disabled ) { return; } $( this ).addClass( "ui-state-focus" ); }) .bind( "blur.accordion", function() { if ( options.disabled ) { return; } $( this ).removeClass( "ui-state-focus" ); }); self.headers.next() .addClass( "ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom" ); self.headers.find( ":first-child" ).addClass( "ui-accordion-heading" ); self.active = self._findActive( options.active ) .addClass( "ui-state-default ui-state-active" ) .toggleClass( "ui-corner-all" ) .toggleClass( "ui-corner-top" ); self.active.next().addClass( "ui-accordion-content-active" ); self._createIcons(); self.resize(); // ARIA self.element.attr( "role", "tablist" ); self.headers .attr( "role", "tab" ) .bind( "keydown.accordion", function( event ) { return self._keydown( event ); }) .next() .attr( "role", "tabpanel" ); self.headers .not( self.active || "" ) .attr({ "aria-expanded": "false", tabIndex: -1 }) .next() .hide(); // make sure at least one header is in the tab order if ( !self.active.length ) { self.headers.eq( 0 ).attr( "tabIndex", 0 ); } else { self.active .attr({ "aria-expanded": "true", tabIndex: 0 }); } // only need links in tab order for Safari if ( !$.browser.safari ) { self.headers.find( "a" ).attr( "tabIndex", -1 ); } if ( options.event ) { self.headers.bind( options.event.split(" ").join(".accordion ") + ".accordion", function(event) { self._eventHandler( event ); event.preventDefault(); }); } }, _createIcons: function() { var options = this.options; if ( options.icons ) { $( "" ) .addClass( "ui-icon " + options.icons.header ) .prependTo( this.headers ); this.active.children( ".ui-icon" ) .toggleClass(options.icons.header) .toggleClass(options.icons.activeHeader); this.element.addClass( "ui-accordion-icons" ); } }, _destroyIcons: function() { this.headers.children( ".ui-icon" ).remove(); this.element.removeClass( "ui-accordion-icons" ); }, destroy: function() { var options = this.options; this.element .removeClass( "ui-accordion ui-widget ui-helper-reset" ) .removeAttr( "role" ); this.headers .unbind( ".accordion" ) .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" ) .removeAttr( "role" ) .removeAttr( "aria-expanded" ) .removeAttr( "tabIndex" ); this.headers.find( "a" ).removeAttr( "tabIndex" ); this._destroyIcons(); this.headers.find( "a:first-child" ).removeClass( "ui-accordion-heading" ); var contents = this.headers.next() .css( "display", "" ) .removeAttr( "role" ) .removeClass( "ui-helper-reset ui-widget-content ui-corner-bottom ui-accordion-content ui-accordion-content-active ui-accordion-disabled ui-state-disabled" ); if ( options.heightStyle !== "content" ) { contents.css( "height", "" ); } return $.Widget.prototype.destroy.call( this ); }, _setOption: function( key, value ) { if ( key == "active" ) { // _activate() will handle invalid values and update this.options this._activate( value ); return; } $.Widget.prototype._setOption.apply( this, arguments ); if ( key == "icons" ) { this._destroyIcons(); if ( value ) { this._createIcons(); } } // #5332 - opacity doesn't cascade to positioned elements in IE // so we need to add the disabled class to the headers and panels if ( key == "disabled" ) { this.headers.add(this.headers.next()) .toggleClass( "ui-accordion-disabled ui-state-disabled", !!value ); } }, _keydown: function( event ) { if ( this.options.disabled || event.altKey || event.ctrlKey ) { return; } var keyCode = $.ui.keyCode, length = this.headers.length, currentIndex = this.headers.index( event.target ), toFocus = false; switch ( event.keyCode ) { case keyCode.RIGHT: case keyCode.DOWN: toFocus = this.headers[ ( currentIndex + 1 ) % length ]; break; case keyCode.LEFT: case keyCode.UP: toFocus = this.headers[ ( currentIndex - 1 + length ) % length ]; break; case keyCode.SPACE: case keyCode.ENTER: this._eventHandler( event ); event.preventDefault(); } if ( toFocus ) { $( event.target ).attr( "tabIndex", -1 ); $( toFocus ).attr( "tabIndex", 0 ); toFocus.focus(); return false; } return true; }, resize: function() { var options = this.options, maxHeight; if ( options.heightStyle === "fill" ) { if ( $.browser.msie ) { var defOverflow = this.element.parent().css( "overflow" ); this.element.parent().css( "overflow", "hidden"); } maxHeight = this.element.parent().height(); this.element.siblings( ":visible" ).each(function() { var elem = $( this ), position = elem.css( "position" ); if ( position === "absolute" || position === "fixed" ) { return; } maxHeight -= elem.outerHeight( true ); }); if ($.browser.msie) { this.element.parent().css( "overflow", defOverflow ); } this.headers.each(function() { maxHeight -= $( this ).outerHeight( true ); }); this.headers.next() .each(function() { $( this ).height( Math.max( 0, maxHeight - $( this ).innerHeight() + $( this ).height() ) ); }) .css( "overflow", "auto" ); } else if ( options.heightStyle === "auto" ) { maxHeight = 0; this.headers.next() .each(function() { maxHeight = Math.max( maxHeight, $( this ).height( "" ).height() ); }) .height( maxHeight ); } return this; }, _activate: function( index ) { var active = this._findActive( index )[ 0 ]; // we found a header to activate, just delegate to the event handler if ( active ) { if ( active !== this.active[ 0 ] ) { this._eventHandler( { target: active, currentTarget: active } ); } return; } // no header to activate, check if we can collapse if ( !this.options.collapsible ) { return; } this.active .removeClass( "ui-state-active ui-corner-top" ) .addClass( "ui-state-default ui-corner-all" ) .children( ".ui-icon" ) .removeClass( this.options.icons.activeHeader ) .addClass( this.options.icons.header ); this.active.next().addClass( "ui-accordion-content-active" ); var toHide = this.active.next(), data = { options: this.options, newHeader: $( [] ), oldHeader: this.active, newContent: $( [] ), oldContent: toHide }, toShow = ( this.active = $( [] ) ); this._toggle( toShow, toHide, data ); }, // TODO: add tests/docs for negative values in 2.0 (#6854) _findActive: function( selector ) { return typeof selector === "number" ? this.headers.eq( selector ) : $( [] ); }, _eventHandler: function( event ) { var options = this.options, clicked = $( event.currentTarget ), clickedIsActive = clicked[0] === this.active[0]; if ( options.disabled ) { return; } // if animations are still active, or the active header is the target, ignore click if ( this.running || ( !options.collapsible && clickedIsActive ) ) { return; } options.active = options.collapsible && clickedIsActive ? false : this.headers.index( clicked ); // find elements to show and hide var active = this.active, toShow = clicked.next(), toHide = this.active.next(), data = { options: options, newHeader: clickedIsActive && options.collapsible ? $([]) : clicked, oldHeader: this.active, newContent: clickedIsActive && options.collapsible ? $([]) : toShow, oldContent: toHide }, down = this.headers.index( this.active[0] ) > this.headers.index( clicked[0] ); // when the call to ._toggle() comes after the class changes // it causes a very odd bug in IE 8 (see #6720) this.active = clickedIsActive ? $([]) : clicked; this._toggle( toShow, toHide, data, clickedIsActive, down ); // switch classes active .removeClass( "ui-state-active ui-corner-top" ) .addClass( "ui-state-default ui-corner-all" ) .children( ".ui-icon" ) .removeClass( options.icons.activeHeader ) .addClass( options.icons.header ); if ( !clickedIsActive ) { clicked .removeClass( "ui-state-default ui-corner-all" ) .addClass( "ui-state-active ui-corner-top" ) .children( ".ui-icon" ) .removeClass( options.icons.header ) .addClass( options.icons.activeHeader ); clicked .next() .addClass( "ui-accordion-content-active" ); } }, _toggle: function( toShow, toHide, data, clickedIsActive, down ) { var self = this, options = self.options; self.toShow = toShow; self.toHide = toHide; self.data = data; var complete = function() { if ( !self ) { return; } return self._completed.apply( self, arguments ); }; // trigger changestart event self._trigger( "changestart", null, self.data ); // count elements to animate self.running = toHide.size() === 0 ? toShow.size() : toHide.size(); if ( options.animated ) { var animOptions = {}; if ( options.collapsible && clickedIsActive ) { animOptions = { toShow: $( [] ), toHide: toHide, complete: complete, down: down, autoHeight: options.heightStyle !== "content" }; } else { animOptions = { toShow: toShow, toHide: toHide, complete: complete, down: down, autoHeight: options.heightStyle !== "content" }; } if ( !options.proxied ) { options.proxied = options.animated; } if ( !options.proxiedDuration ) { options.proxiedDuration = options.duration; } options.animated = $.isFunction( options.proxied ) ? options.proxied( animOptions ) : options.proxied; options.duration = $.isFunction( options.proxiedDuration ) ? options.proxiedDuration( animOptions ) : options.proxiedDuration; var animations = $.ui.accordion.animations, duration = options.duration, easing = options.animated; if ( easing && !animations[ easing ] && !$.easing[ easing ] ) { easing = "slide"; } if ( !animations[ easing ] ) { animations[ easing ] = function( options ) { this.slide( options, { easing: easing, duration: duration || 700 }); }; } animations[ easing ]( animOptions ); } else { if ( options.collapsible && clickedIsActive ) { toShow.toggle(); } else { toHide.hide(); toShow.show(); } complete( true ); } // TODO assert that the blur and focus triggers are really necessary, remove otherwise toHide.prev() .attr({ "aria-expanded": "false", tabIndex: -1 }) .blur(); toShow.prev() .attr({ "aria-expanded": "true", tabIndex: 0 }) .focus(); }, _completed: function( cancel ) { this.running = cancel ? 0 : --this.running; if ( this.running ) { return; } if ( this.options.heightStyle === "content" ) { this.toShow.add( this.toHide ).css({ height: "", overflow: "" }); } // other classes are removed before the animation; this one needs to stay until completed this.toHide.removeClass( "ui-accordion-content-active" ); // Work around for rendering bug in IE (#5421) this.toHide.parent()[0].className = this.toHide.parent()[0].className; this._trigger( "change", null, this.data ); } }); $.extend( $.ui.accordion, { version: "@VERSION", animations: { slide: function( options, additions ) { options = $.extend({ easing: "swing", duration: 300 }, options, additions ); if ( !options.toHide.size() ) { options.toShow.animate({ height: "show", paddingTop: "show", paddingBottom: "show" }, options ); return; } if ( !options.toShow.size() ) { options.toHide.animate({ height: "hide", paddingTop: "hide", paddingBottom: "hide" }, options ); return; } var overflow = options.toShow.css( "overflow" ), percentDone = 0, showProps = {}, hideProps = {}, fxAttrs = [ "height", "paddingTop", "paddingBottom" ], originalWidth; // fix width before calculating height of hidden element var s = options.toShow; originalWidth = s[0].style.width; s.width( parseInt( s.parent().width(), 10 ) - parseInt( s.css( "paddingLeft" ), 10 ) - parseInt( s.css( "paddingRight" ), 10 ) - ( parseInt( s.css( "borderLeftWidth" ), 10 ) || 0 ) - ( parseInt( s.css( "borderRightWidth" ), 10) || 0 ) ); $.each( fxAttrs, function( i, prop ) { hideProps[ prop ] = "hide"; var parts = ( "" + $.css( options.toShow[0], prop ) ).match( /^([\d+-.]+)(.*)$/ ); showProps[ prop ] = { value: parts[ 1 ], unit: parts[ 2 ] || "px" }; }); options.toShow.css({ height: 0, overflow: "hidden" }).show(); options.toHide .filter( ":hidden" ) .each( options.complete ) .end() .filter( ":visible" ) .animate( hideProps, { step: function( now, settings ) { // only calculate the percent when animating height // IE gets very inconsistent results when animating elements // with small values, which is common for padding if ( settings.prop == "height" ) { percentDone = ( settings.end - settings.start === 0 ) ? 0 : ( settings.now - settings.start ) / ( settings.end - settings.start ); } options.toShow[ 0 ].style[ settings.prop ] = ( percentDone * showProps[ settings.prop ].value ) + showProps[ settings.prop ].unit; }, duration: options.duration, easing: options.easing, complete: function() { if ( !options.autoHeight ) { options.toShow.css( "height", "" ); } options.toShow.css({ width: originalWidth, overflow: overflow }); options.complete(); } }); }, bounceslide: function( options ) { this.slide( options, { easing: options.down ? "easeOutBounce" : "swing", duration: options.down ? 1000 : 200 }); } } }); // DEPRECATED // navigation options (function( $, prototype ) { $.extend( prototype.options, { navigation: false, navigationFilter: function() { return this.href.toLowerCase() === location.href.toLowerCase(); } }); var _create = prototype._create; prototype._create = function() { if ( this.options.navigation ) { var self = this, headers = this.element.find( this.options.header ), content = headers.next(); current = headers.add( content ) .find( "a" ) .filter( this.options.navigationFilter ) [ 0 ]; if ( current ) { headers.add( content ).each( function( index ) { if ( $.contains( this, current ) ) { self.options.active = Math.floor( index / 2 ); return false; } }); } } _create.call( this ); }; }( jQuery, jQuery.ui.accordion.prototype ) ); // height options (function( $, prototype ) { $.extend( prototype.options, { autoHeight: true, // use heightStyle: "auto" clearStyle: false, // use heightStyle: "content" fillSpace: false // use heightStyle: "fill" }); var _create = prototype._create, _setOption = prototype._setOption; $.extend( prototype, { _create: function() { this.options.heightStyle = this.options.heightStyle || this._mergeHeightStyle(); _create.call( this ); }, _setOption: function( key, value ) { if ( key === "autoHeight" || key === "clearStyle" || key === "fillSpace" ) { this.options.heightStyle = this._mergeHeightStyle(); } _setOption.apply( this, arguments ); }, _mergeHeightStyle: function() { var options = this.options; if ( options.fillSpace ) { return "fill"; } if ( options.clearStyle ) { return "content"; } if ( options.autoHeight ) { return "auto"; } } }); }( jQuery, jQuery.ui.accordion.prototype ) ); // icon options (function( $, prototype ) { prototype.options.icons.headerSelected = "ui-icon-triangle-1-s"; var _createIcons = prototype._createIcons; prototype._createIcons = function() { this.options.icons.activeHeader = this.options.icons.activeHeader || this.options.icons.headerSelected; _createIcons.call( this ); }; }( jQuery, jQuery.ui.accordion.prototype ) ); // expanded active option, activate method (function( $, prototype ) { prototype.activate = prototype._activate; var _findActive = prototype._findActive; prototype._findActive = function( index ) { if ( index === -1 ) { index = false; } if ( index && typeof index !== "number" ) { index = this.headers.index( this.headers.filter( index ) ); if ( index === -1 ) { index = false; } } return _findActive.call( this, index ); }; }( jQuery, jQuery.ui.accordion.prototype ) ); })( jQuery );