jquery-ui/ui/jquery.ui.accordion.js
2010-07-22 19:10:14 -04:00

597 lines
15 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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",
autoHeight: true,
clearStyle: false,
collapsible: false,
event: "click",
fillSpace: false,
header: "> li > :first-child,> :not(li):even",
icons: {
header: "ui-icon-triangle-1-e",
headerSelected: "ui-icon-triangle-1-s"
},
navigation: false,
navigationFilter: function() {
return this.href.toLowerCase() === location.href.toLowerCase();
}
},
_create: function() {
var o = this.options, self = this;
this.running = 0;
this.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
this.element.children( "li" ).addClass( "ui-accordion-li-fix" );
this.headers = this.element.find( o.header )
.addClass( "ui-accordion-header ui-helper-reset ui-state-default ui-corner-all" )
.bind( "mouseenter.accordion", function() {
if ( o.disabled ) {
return;
}
$( this ).addClass( "ui-state-hover" );
})
.bind( "mouseleave.accordion", function() {
if ( o.disabled ) {
return;
}
$( this ).removeClass( "ui-state-hover" );
})
.bind( "focus.accordion", function() {
if ( o.disabled ) {
return;
}
$( this ).addClass( "ui-state-focus" );
})
.bind( "blur.accordion", function() {
if ( o.disabled ) {
return;
}
$( this ).removeClass( "ui-state-focus" );
});
this.headers.next()
.addClass( "ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom" );
if ( o.navigation ) {
var current = this.element.find( "a" ).filter( o.navigationFilter ).eq( 0 );
if ( current.length ) {
var header = current.closest( ".ui-accordion-header" );
if ( header.length ) {
// anchor within header
this.active = header;
} else {
// anchor within content
this.active = current.closest( ".ui-accordion-content" ).prev();
}
}
}
this.active = this._findActive( this.active || o.active )
.addClass( "ui-state-default ui-state-active" )
.toggleClass( "ui-corner-all ui-corner-top" );
this.active.next().addClass( "ui-accordion-content-active" );
this._createIcons();
this.resize();
// ARIA
this.element.attr( "role", "tablist" );
this.headers
.attr( "role", "tab" )
.bind( "keydown.accordion", function( event ) {
return self._keydown( event );
})
.next()
.attr( "role", "tabpanel" );
this.headers
.not( this.active || "" )
.attr({
"aria-expanded": "false",
tabIndex: -1
})
.next()
.hide();
// make sure at least one header is in the tab order
if ( !this.active.length ) {
this.headers.eq( 0 ).attr( "tabIndex", 0 );
} else {
this.active
.attr({
"aria-expanded": "true",
tabIndex: 0
});
}
// only need links in tab order for Safari
if ( !$.browser.safari ) {
this.headers.find( "a" ).attr( "tabIndex", -1 );
}
if ( o.event ) {
this.headers.bind( o.event.split(" ").join(".accordion ") + ".accordion", function(event) {
self._clickHandler.call( self, event, this );
event.preventDefault();
});
}
},
_createIcons: function() {
var o = this.options;
if ( o.icons ) {
$( "<span></span>" )
.addClass( "ui-icon " + o.icons.header )
.prependTo( this.headers );
this.active.children( ".ui-icon" )
.toggleClass(o.icons.header)
.toggleClass(o.icons.headerSelected);
this.element.addClass( "ui-accordion-icons" );
}
},
_destroyIcons: function() {
this.headers.children( ".ui-icon" ).remove();
this.element.removeClass( "ui-accordion-icons" );
},
destroy: function() {
var o = 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();
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 ( o.autoHeight || o.fillHeight ) {
contents.css( "height", "" );
}
return $.Widget.prototype.destroy.call( this );
},
_setOption: function( key, value ) {
$.Widget.prototype._setOption.apply( this, arguments );
if ( key == "active" ) {
this.activate( value );
}
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())
[ value ? "addClass" : "removeClass" ](
"ui-accordion-disabled ui-state-disabled" );
}
},
_keydown: function( event ) {
var o = this.options, keyCode = $.ui.keyCode;
if ( o.disabled || event.altKey || event.ctrlKey ) {
return;
}
var 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._clickHandler( { target: event.target }, event.target );
event.preventDefault();
}
if ( toFocus ) {
$( event.target ).attr( "tabIndex", -1 );
$( toFocus ).attr( "tabIndex", 0 );
toFocus.focus();
return false;
}
return true;
},
resize: function() {
var o = this.options, maxHeight;
if ( o.fillSpace ) {
if ( $.browser.msie ) {
var defOverflow = this.element.parent().css( "overflow" );
this.element.parent().css( "overflow", "hidden");
}
maxHeight = this.element.parent().height();
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 ( o.autoHeight ) {
maxHeight = 0;
this.headers.next()
.each(function() {
maxHeight = Math.max( maxHeight, $( this ).height( "" ).height() );
})
.height( maxHeight );
}
return this;
},
activate: function( index ) {
// TODO this gets called on init, changing the option without an explicit call for that
this.options.active = index;
// call clickHandler with custom event
var active = this._findActive( index )[ 0 ];
this._clickHandler( { target: active }, active );
return this;
},
_findActive: function(selector) {
return selector
? typeof selector === "number"
? this.headers.filter( ":eq(" + selector + ")" )
: this.headers.not( this.headers.not( selector ) )
: selector === false
? $( [] )
: this.headers.filter( ":eq(0)" );
},
// TODO isn't event.target enough? why the separate target argument?
_clickHandler: function( event, target ) {
var o = this.options;
if ( o.disabled ) {
return;
}
// called only when using activate(false) to close all parts programmatically
if ( !event.target ) {
if ( !o.collapsible ) {
return;
}
this.active
.removeClass( "ui-state-active ui-corner-top" )
.addClass( "ui-state-default ui-corner-all" )
.children( ".ui-icon" )
.removeClass( o.icons.headerSelected )
.addClass( o.icons.header );
this.active.next().addClass( "ui-accordion-content-active" );
var toHide = this.active.next(),
data = {
options: o,
newHeader: $( [] ),
oldHeader: o.active,
newContent: $( [] ),
oldContent: toHide
},
toShow = ( this.active = $( [] ) );
this._toggle( toShow, toHide, data );
return;
}
// get the click target
var clicked = $( event.currentTarget || target ),
clickedIsActive = clicked[0] === this.active[0];
// TODO the option is changed, is that correct?
// TODO if it is correct, shouldn't that happen after determining that the click is valid?
o.active = o.collapsible && clickedIsActive ?
false :
this.headers.index( clicked );
// if animations are still active, or the active header is the target, ignore click
if ( this.running || ( !o.collapsible && clickedIsActive ) ) {
return;
}
// switch classes
this.active
.removeClass( "ui-state-active ui-corner-top" )
.addClass( "ui-state-default ui-corner-all" )
.children( ".ui-icon" )
.removeClass( o.icons.headerSelected )
.addClass( o.icons.header );
if ( !clickedIsActive ) {
clicked
.removeClass( "ui-state-default ui-corner-all" )
.addClass( "ui-state-active ui-corner-top" )
.children( ".ui-icon" )
.removeClass( o.icons.header )
.addClass( o.icons.headerSelected );
clicked
.next()
.addClass( "ui-accordion-content-active" );
}
// find elements to show and hide
var toShow = clicked.next(),
toHide = this.active.next(),
data = {
options: o,
newHeader: clickedIsActive && o.collapsible ? $([]) : clicked,
oldHeader: this.active,
newContent: clickedIsActive && o.collapsible ? $([]) : toShow,
oldContent: toHide
},
down = this.headers.index( this.active[0] ) > this.headers.index( clicked[0] );
this.active = clickedIsActive ? $([]) : clicked;
this._toggle( toShow, toHide, data, clickedIsActive, down );
return;
},
_toggle: function( toShow, toHide, data, clickedIsActive, down ) {
var o = this.options, self = this;
this.toShow = toShow;
this.toHide = toHide;
this.data = data;
var complete = function() {
if ( !self ) {
return;
}
return self._completed.apply( self, arguments );
};
// trigger changestart event
this._trigger( "changestart", null, this.data );
// count elements to animate
this.running = toHide.size() === 0 ? toShow.size() : toHide.size();
if ( o.animated ) {
var animOptions = {};
if ( o.collapsible && clickedIsActive ) {
animOptions = {
toShow: $([]),
toHide: toHide,
complete: complete,
down: down,
autoHeight: o.autoHeight || o.fillSpace
};
} else {
animOptions = {
toShow: toShow,
toHide: toHide,
complete: complete,
down: down,
autoHeight: o.autoHeight || o.fillSpace
};
}
if ( !o.proxied ) {
o.proxied = o.animated;
}
if ( !o.proxiedDuration ) {
o.proxiedDuration = o.duration;
}
o.animated = $.isFunction( o.proxied ) ?
o.proxied( animOptions ) :
o.proxied;
o.duration = $.isFunction( o.proxiedDuration ) ?
o.proxiedDuration( animOptions ) :
o.proxiedDuration;
var animations = $.ui.accordion.animations,
duration = o.duration,
easing = o.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 ( o.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 ) {
var o = this.options;
this.running = cancel ? 0 : --this.running;
if ( this.running ) {
return;
}
if ( o.clearStyle ) {
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" );
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
});
}
}
});
})( jQuery );