Localisation updates from http://translatewiki.net.
[mediawiki.git] / resources / jquery.ui / jquery.ui.accordion.js
blobdc1ba60aa4ef6aba59e76bb67d30348dd5dba721
1 /*!
2  * jQuery UI Accordion 1.8.24
3  *
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
7  *
8  * http://docs.jquery.com/UI/Accordion
9  *
10  * Depends:
11  *      jquery.ui.core.js
12  *      jquery.ui.widget.js
13  */
14 (function( $, undefined ) {
16 $.widget( "ui.accordion", {
17         options: {
18                 active: 0,
19                 animated: "slide",
20                 autoHeight: true,
21                 clearStyle: false,
22                 collapsible: false,
23                 event: "click",
24                 fillSpace: false,
25                 header: "> li > :first-child,> :not(li):even",
26                 icons: {
27                         header: "ui-icon-triangle-1-e",
28                         headerSelected: "ui-icon-triangle-1-s"
29                 },
30                 navigation: false,
31                 navigationFilter: function() {
32                         return this.href.toLowerCase() === location.href.toLowerCase();
33                 }
34         },
36         _create: function() {
37                 var self = this,
38                         options = self.options;
40                 self.running = 0;
42                 self.element
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
46                         .children( "li" )
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 ) {
53                                         return;
54                                 }
55                                 $( this ).addClass( "ui-state-hover" );
56                         })
57                         .bind( "mouseleave.accordion", function() {
58                                 if ( options.disabled ) {
59                                         return;
60                                 }
61                                 $( this ).removeClass( "ui-state-hover" );
62                         })
63                         .bind( "focus.accordion", function() {
64                                 if ( options.disabled ) {
65                                         return;
66                                 }
67                                 $( this ).addClass( "ui-state-focus" );
68                         })
69                         .bind( "blur.accordion", function() {
70                                 if ( options.disabled ) {
71                                         return;
72                                 }
73                                 $( this ).removeClass( "ui-state-focus" );
74                         });
76                 self.headers.next()
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
85                                         self.active = header;
86                                 } else {
87                                         // anchor within content
88                                         self.active = current.closest( ".ui-accordion-content" ).prev();
89                                 }
90                         }
91                 }
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" );
99                 self._createIcons();
100                 self.resize();
101                 
102                 // ARIA
103                 self.element.attr( "role", "tablist" );
105                 self.headers
106                         .attr( "role", "tab" )
107                         .bind( "keydown.accordion", function( event ) {
108                                 return self._keydown( event );
109                         })
110                         .next()
111                                 .attr( "role", "tabpanel" );
113                 self.headers
114                         .not( self.active || "" )
115                         .attr({
116                                 "aria-expanded": "false",
117                                 "aria-selected": "false",
118                                 tabIndex: -1
119                         })
120                         .next()
121                                 .hide();
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 );
126                 } else {
127                         self.active
128                                 .attr({
129                                         "aria-expanded": "true",
130                                         "aria-selected": "true",
131                                         tabIndex: 0
132                                 });
133                 }
135                 // only need links in tab order for Safari
136                 if ( !$.browser.safari ) {
137                         self.headers.find( "a" ).attr( "tabIndex", -1 );
138                 }
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();
144                         });
145                 }
146         },
148         _createIcons: function() {
149                 var options = this.options;
150                 if ( options.icons ) {
151                         $( "<span></span>" )
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" );
158                 }
159         },
161         _destroyIcons: function() {
162                 this.headers.children( ".ui-icon" ).remove();
163                 this.element.removeClass( "ui-accordion-icons" );
164         },
166         destroy: function() {
167                 var options = this.options;
169                 this.element
170                         .removeClass( "ui-accordion ui-widget ui-helper-reset" )
171                         .removeAttr( "role" );
173                 this.headers
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", "" );
189                 }
191                 return $.Widget.prototype.destroy.call( this );
192         },
194         _setOption: function( key, value ) {
195                 $.Widget.prototype._setOption.apply( this, arguments );
196                         
197                 if ( key == "active" ) {
198                         this.activate( value );
199                 }
200                 if ( key == "icons" ) {
201                         this._destroyIcons();
202                         if ( value ) {
203                                 this._createIcons();
204                         }
205                 }
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" );
212                 }
213         },
215         _keydown: function( event ) {
216                 if ( this.options.disabled || event.altKey || event.ctrlKey ) {
217                         return;
218                 }
220                 var keyCode = $.ui.keyCode,
221                         length = this.headers.length,
222                         currentIndex = this.headers.index( event.target ),
223                         toFocus = false;
225                 switch ( event.keyCode ) {
226                         case keyCode.RIGHT:
227                         case keyCode.DOWN:
228                                 toFocus = this.headers[ ( currentIndex + 1 ) % length ];
229                                 break;
230                         case keyCode.LEFT:
231                         case keyCode.UP:
232                                 toFocus = this.headers[ ( currentIndex - 1 + length ) % length ];
233                                 break;
234                         case keyCode.SPACE:
235                         case keyCode.ENTER:
236                                 this._clickHandler( { target: event.target }, event.target );
237                                 event.preventDefault();
238                 }
240                 if ( toFocus ) {
241                         $( event.target ).attr( "tabIndex", -1 );
242                         $( toFocus ).attr( "tabIndex", 0 );
243                         toFocus.focus();
244                         return false;
245                 }
247                 return true;
248         },
250         resize: function() {
251                 var options = this.options,
252                         maxHeight;
254                 if ( options.fillSpace ) {
255                         if ( $.browser.msie ) {
256                                 var defOverflow = this.element.parent().css( "overflow" );
257                                 this.element.parent().css( "overflow", "hidden");
258                         }
259                         maxHeight = this.element.parent().height();
260                         if ($.browser.msie) {
261                                 this.element.parent().css( "overflow", defOverflow );
262                         }
264                         this.headers.each(function() {
265                                 maxHeight -= $( this ).outerHeight( true );
266                         });
268                         this.headers.next()
269                                 .each(function() {
270                                         $( this ).height( Math.max( 0, maxHeight -
271                                                 $( this ).innerHeight() + $( this ).height() ) );
272                                 })
273                                 .css( "overflow", "auto" );
274                 } else if ( options.autoHeight ) {
275                         maxHeight = 0;
276                         this.headers.next()
277                                 .each(function() {
278                                         maxHeight = Math.max( maxHeight, $( this ).height( "" ).height() );
279                                 })
280                                 .height( maxHeight );
281                 }
283                 return this;
284         },
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 );
293                 return this;
294         },
296         _findActive: function( selector ) {
297                 return selector
298                         ? typeof selector === "number"
299                                 ? this.headers.filter( ":eq(" + selector + ")" )
300                                 : this.headers.not( this.headers.not( selector ) )
301                         : selector === false
302                                 ? $( [] )
303                                 : this.headers.filter( ":eq(0)" );
304         },
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 ) {
310                         return;
311                 }
313                 // called only when using activate(false) to close all parts programmatically
314                 if ( !event.target ) {
315                         if ( !options.collapsible ) {
316                                 return;
317                         }
318                         this.active
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(),
326                                 data = {
327                                         options: options,
328                                         newHeader: $( [] ),
329                                         oldHeader: options.active,
330                                         newContent: $( [] ),
331                                         oldContent: toHide
332                                 },
333                                 toShow = ( this.active = $( [] ) );
334                         this._toggle( toShow, toHide, data );
335                         return;
336                 }
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 ?
345                         false :
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 ) ) {
350                         return;
351                 }
353                 // find elements to show and hide
354                 var active = this.active,
355                         toShow = clicked.next(),
356                         toHide = this.active.next(),
357                         data = {
358                                 options: options,
359                                 newHeader: clickedIsActive && options.collapsible ? $([]) : clicked,
360                                 oldHeader: this.active,
361                                 newContent: clickedIsActive && options.collapsible ? $([]) : toShow,
362                                 oldContent: toHide
363                         },
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 );
371                 // switch classes
372                 active
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 ) {
379                         clicked
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 );
385                         clicked
386                                 .next()
387                                 .addClass( "ui-accordion-content-active" );
388                 }
390                 return;
391         },
393         _toggle: function( toShow, toHide, data, clickedIsActive, down ) {
394                 var self = this,
395                         options = self.options;
397                 self.toShow = toShow;
398                 self.toHide = toHide;
399                 self.data = data;
401                 var complete = function() {
402                         if ( !self ) {
403                                 return;
404                         }
405                         return self._completed.apply( self, arguments );
406                 };
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 ) {
418                                 animOptions = {
419                                         toShow: $( [] ),
420                                         toHide: toHide,
421                                         complete: complete,
422                                         down: down,
423                                         autoHeight: options.autoHeight || options.fillSpace
424                                 };
425                         } else {
426                                 animOptions = {
427                                         toShow: toShow,
428                                         toHide: toHide,
429                                         complete: complete,
430                                         down: down,
431                                         autoHeight: options.autoHeight || options.fillSpace
432                                 };
433                         }
435                         if ( !options.proxied ) {
436                                 options.proxied = options.animated;
437                         }
439                         if ( !options.proxiedDuration ) {
440                                 options.proxiedDuration = options.duration;
441                         }
443                         options.animated = $.isFunction( options.proxied ) ?
444                                 options.proxied( animOptions ) :
445                                 options.proxied;
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 ] ) {
456                                 easing = "slide";
457                         }
458                         if ( !animations[ easing ] ) {
459                                 animations[ easing ] = function( options ) {
460                                         this.slide( options, {
461                                                 easing: easing,
462                                                 duration: duration || 700
463                                         });
464                                 };
465                         }
467                         animations[ easing ]( animOptions );
468                 } else {
469                         if ( options.collapsible && clickedIsActive ) {
470                                 toShow.toggle();
471                         } else {
472                                 toHide.hide();
473                                 toShow.show();
474                         }
476                         complete( true );
477                 }
479                 // TODO assert that the blur and focus triggers are really necessary, remove otherwise
480                 toHide.prev()
481                         .attr({
482                                 "aria-expanded": "false",
483                                 "aria-selected": "false",
484                                 tabIndex: -1
485                         })
486                         .blur();
487                 toShow.prev()
488                         .attr({
489                                 "aria-expanded": "true",
490                                 "aria-selected": "true",
491                                 tabIndex: 0
492                         })
493                         .focus();
494         },
496         _completed: function( cancel ) {
497                 this.running = cancel ? 0 : --this.running;
498                 if ( this.running ) {
499                         return;
500                 }
502                 if ( this.options.clearStyle ) {
503                         this.toShow.add( this.toHide ).css({
504                                 height: "",
505                                 overflow: ""
506                         });
507                 }
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;
514                 }
516                 this._trigger( "change", null, this.data );
517         }
520 $.extend( $.ui.accordion, {
521         version: "1.8.24",
522         animations: {
523                 slide: function( options, additions ) {
524                         options = $.extend({
525                                 easing: "swing",
526                                 duration: 300
527                         }, options, additions );
528                         if ( !options.toHide.size() ) {
529                                 options.toShow.animate({
530                                         height: "show",
531                                         paddingTop: "show",
532                                         paddingBottom: "show"
533                                 }, options );
534                                 return;
535                         }
536                         if ( !options.toShow.size() ) {
537                                 options.toHide.animate({
538                                         height: "hide",
539                                         paddingTop: "hide",
540                                         paddingBottom: "hide"
541                                 }, options );
542                                 return;
543                         }
544                         var overflow = options.toShow.css( "overflow" ),
545                                 percentDone = 0,
546                                 showProps = {},
547                                 hideProps = {},
548                                 fxAttrs = [ "height", "paddingTop", "paddingBottom" ],
549                                 originalWidth;
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 ] = {
564                                         value: parts[ 1 ],
565                                         unit: parts[ 2 ] || "px"
566                                 };
567                         });
568                         options.toShow.css({ height: 0, overflow: "hidden" }).show();
569                         options.toHide
570                                 .filter( ":hidden" )
571                                         .each( options.complete )
572                                 .end()
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 );
582                                         }
584                                         options.toShow[ 0 ].style[ settings.prop ] =
585                                                 ( percentDone * showProps[ settings.prop ].value )
586                                                 + showProps[ settings.prop ].unit;
587                                 },
588                                 duration: options.duration,
589                                 easing: options.easing,
590                                 complete: function() {
591                                         if ( !options.autoHeight ) {
592                                                 options.toShow.css( "height", "" );
593                                         }
594                                         options.toShow.css({
595                                                 width: originalWidth,
596                                                 overflow: overflow
597                                         });
598                                         options.complete();
599                                 }
600                         });
601                 },
602                 bounceslide: function( options ) {
603                         this.slide( options, {
604                                 easing: options.down ? "easeOutBounce" : "swing",
605                                 duration: options.down ? 1000 : 200
606                         });
607                 }
608         }
611 })( jQuery );