Localisation updates for core and extension messages from translatewiki.net (2010...
[mediawiki.git] / resources / jquery.ui / jquery.ui.accordion.js
blob7d926e0960ff9a76c83ebb1afafb6a86def2a2e9
1 /*
2  * jQuery UI Accordion 1.8.2
3  *
4  * Copyright (c) 2010 AUTHORS.txt (http://jqueryui.com/about)
5  * Dual licensed under the MIT (MIT-LICENSE.txt)
6  * and GPL (GPL-LICENSE.txt) licenses.
7  *
8  * http://docs.jquery.com/UI/Accordion
9  *
10  * Depends:
11  *      jquery.ui.core.js
12  *      jquery.ui.widget.js
13  */
14 (function($) {
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         },
35         _create: function() {
37                 var o = this.options, self = this;
38                 this.running = 0;
40                 this.element.addClass("ui-accordion ui-widget ui-helper-reset");
41                 
42                 // in lack of child-selectors in CSS we need to mark top-LIs in a UL-accordion for some IE-fix
43                 this.element.children("li").addClass("ui-accordion-li-fix");
45                 this.headers = this.element.find(o.header).addClass("ui-accordion-header ui-helper-reset ui-state-default ui-corner-all")
46                         .bind("mouseenter.accordion", function(){ $(this).addClass('ui-state-hover'); })
47                         .bind("mouseleave.accordion", function(){ $(this).removeClass('ui-state-hover'); })
48                         .bind("focus.accordion", function(){ $(this).addClass('ui-state-focus'); })
49                         .bind("blur.accordion", function(){ $(this).removeClass('ui-state-focus'); });
51                 this.headers
52                         .next()
53                                 .addClass("ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom");
55                 if ( o.navigation ) {
56                         var current = this.element.find("a").filter(o.navigationFilter);
57                         if ( current.length ) {
58                                 var header = current.closest(".ui-accordion-header");
59                                 if ( header.length ) {
60                                         // anchor within header
61                                         this.active = header;
62                                 } else {
63                                         // anchor within content
64                                         this.active = current.closest(".ui-accordion-content").prev();
65                                 }
66                         }
67                 }
69                 this.active = this._findActive(this.active || o.active).toggleClass("ui-state-default").toggleClass("ui-state-active").toggleClass("ui-corner-all").toggleClass("ui-corner-top");
70                 this.active.next().addClass('ui-accordion-content-active');
72                 //Append icon elements
73                 this._createIcons();
75                 this.resize();
77                 //ARIA
78                 this.element.attr('role','tablist');
80                 this.headers
81                         .attr('role','tab')
82                         .bind('keydown', function(event) { return self._keydown(event); })
83                         .next()
84                         .attr('role','tabpanel');
86                 this.headers
87                         .not(this.active || "")
88                         .attr('aria-expanded','false')
89                         .attr("tabIndex", "-1")
90                         .next()
91                         .hide();
93                 // make sure at least one header is in the tab order
94                 if (!this.active.length) {
95                         this.headers.eq(0).attr('tabIndex','0');
96                 } else {
97                         this.active
98                                 .attr('aria-expanded','true')
99                                 .attr('tabIndex', '0');
100                 }
102                 // only need links in taborder for Safari
103                 if (!$.browser.safari)
104                         this.headers.find('a').attr('tabIndex','-1');
106                 if (o.event) {
107                         this.headers.bind((o.event) + ".accordion", function(event) {
108                                 self._clickHandler.call(self, event, this);
109                                 event.preventDefault();
110                         });
111                 }
113         },
114         
115         _createIcons: function() {
116                 var o = this.options;
117                 if (o.icons) {
118                         $("<span/>").addClass("ui-icon " + o.icons.header).prependTo(this.headers);
119                         this.active.find(".ui-icon").toggleClass(o.icons.header).toggleClass(o.icons.headerSelected);
120                         this.element.addClass("ui-accordion-icons");
121                 }
122         },
123         
124         _destroyIcons: function() {
125                 this.headers.children(".ui-icon").remove();
126                 this.element.removeClass("ui-accordion-icons");
127         },
129         destroy: function() {
130                 var o = this.options;
132                 this.element
133                         .removeClass("ui-accordion ui-widget ui-helper-reset")
134                         .removeAttr("role")
135                         .unbind('.accordion')
136                         .removeData('accordion');
138                 this.headers
139                         .unbind(".accordion")
140                         .removeClass("ui-accordion-header ui-helper-reset ui-state-default ui-corner-all ui-state-active ui-corner-top")
141                         .removeAttr("role").removeAttr("aria-expanded").removeAttr("tabIndex");
143                 this.headers.find("a").removeAttr("tabIndex");
144                 this._destroyIcons();
145                 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");
146                 if (o.autoHeight || o.fillHeight) {
147                         contents.css("height", "");
148                 }
150                 return this;
151         },
152         
153         _setOption: function(key, value) {
154                 $.Widget.prototype._setOption.apply(this, arguments);
155                         
156                 if (key == "active") {
157                         this.activate(value);
158                 }
159                 if (key == "icons") {
160                         this._destroyIcons();
161                         if (value) {
162                                 this._createIcons();
163                         }
164                 }
165                 
166         },
168         _keydown: function(event) {
170                 var o = this.options, keyCode = $.ui.keyCode;
172                 if (o.disabled || event.altKey || event.ctrlKey)
173                         return;
175                 var length = this.headers.length;
176                 var currentIndex = this.headers.index(event.target);
177                 var toFocus = false;
179                 switch(event.keyCode) {
180                         case keyCode.RIGHT:
181                         case keyCode.DOWN:
182                                 toFocus = this.headers[(currentIndex + 1) % length];
183                                 break;
184                         case keyCode.LEFT:
185                         case keyCode.UP:
186                                 toFocus = this.headers[(currentIndex - 1 + length) % length];
187                                 break;
188                         case keyCode.SPACE:
189                         case keyCode.ENTER:
190                                 this._clickHandler({ target: event.target }, event.target);
191                                 event.preventDefault();
192                 }
194                 if (toFocus) {
195                         $(event.target).attr('tabIndex','-1');
196                         $(toFocus).attr('tabIndex','0');
197                         toFocus.focus();
198                         return false;
199                 }
201                 return true;
203         },
205         resize: function() {
207                 var o = this.options, maxHeight;
209                 if (o.fillSpace) {
210                         
211                         if($.browser.msie) { var defOverflow = this.element.parent().css('overflow'); this.element.parent().css('overflow', 'hidden'); }
212                         maxHeight = this.element.parent().height();
213                         if($.browser.msie) { this.element.parent().css('overflow', defOverflow); }
214         
215                         this.headers.each(function() {
216                                 maxHeight -= $(this).outerHeight(true);
217                         });
219                         this.headers.next().each(function() {
220                    $(this).height(Math.max(0, maxHeight - $(this).innerHeight() + $(this).height()));
221                         }).css('overflow', 'auto');
223                 } else if ( o.autoHeight ) {
224                         maxHeight = 0;
225                         this.headers.next().each(function() {
226                                 maxHeight = Math.max(maxHeight, $(this).height());
227                         }).height(maxHeight);
228                 }
230                 return this;
231         },
233         activate: function(index) {
234                 // TODO this gets called on init, changing the option without an explicit call for that
235                 this.options.active = index;
236                 // call clickHandler with custom event
237                 var active = this._findActive(index)[0];
238                 this._clickHandler({ target: active }, active);
240                 return this;
241         },
243         _findActive: function(selector) {
244                 return selector
245                         ? typeof selector == "number"
246                                 ? this.headers.filter(":eq(" + selector + ")")
247                                 : this.headers.not(this.headers.not(selector))
248                         : selector === false
249                                 ? $([])
250                                 : this.headers.filter(":eq(0)");
251         },
253         // TODO isn't event.target enough? why the seperate target argument?
254         _clickHandler: function(event, target) {
256                 var o = this.options;
257                 if (o.disabled)
258                         return;
260                 // called only when using activate(false) to close all parts programmatically
261                 if (!event.target) {
262                         if (!o.collapsible)
263                                 return;
264                         this.active.removeClass("ui-state-active ui-corner-top").addClass("ui-state-default ui-corner-all")
265                                 .find(".ui-icon").removeClass(o.icons.headerSelected).addClass(o.icons.header);
266                         this.active.next().addClass('ui-accordion-content-active');
267                         var toHide = this.active.next(),
268                                 data = {
269                                         options: o,
270                                         newHeader: $([]),
271                                         oldHeader: o.active,
272                                         newContent: $([]),
273                                         oldContent: toHide
274                                 },
275                                 toShow = (this.active = $([]));
276                         this._toggle(toShow, toHide, data);
277                         return;
278                 }
280                 // get the click target
281                 var clicked = $(event.currentTarget || target);
282                 var clickedIsActive = clicked[0] == this.active[0];
283                 
284                 // TODO the option is changed, is that correct?
285                 // TODO if it is correct, shouldn't that happen after determining that the click is valid?
286                 o.active = o.collapsible && clickedIsActive ? false : $('.ui-accordion-header', this.element).index(clicked);
288                 // if animations are still active, or the active header is the target, ignore click
289                 if (this.running || (!o.collapsible && clickedIsActive)) {
290                         return;
291                 }
293                 // switch classes
294                 this.active.removeClass("ui-state-active ui-corner-top").addClass("ui-state-default ui-corner-all")
295                         .find(".ui-icon").removeClass(o.icons.headerSelected).addClass(o.icons.header);
296                 if (!clickedIsActive) {
297                         clicked.removeClass("ui-state-default ui-corner-all").addClass("ui-state-active ui-corner-top")
298                                 .find(".ui-icon").removeClass(o.icons.header).addClass(o.icons.headerSelected);
299                         clicked.next().addClass('ui-accordion-content-active');
300                 }
302                 // find elements to show and hide
303                 var toShow = clicked.next(),
304                         toHide = this.active.next(),
305                         data = {
306                                 options: o,
307                                 newHeader: clickedIsActive && o.collapsible ? $([]) : clicked,
308                                 oldHeader: this.active,
309                                 newContent: clickedIsActive && o.collapsible ? $([]) : toShow,
310                                 oldContent: toHide
311                         },
312                         down = this.headers.index( this.active[0] ) > this.headers.index( clicked[0] );
314                 this.active = clickedIsActive ? $([]) : clicked;
315                 this._toggle(toShow, toHide, data, clickedIsActive, down);
317                 return;
319         },
321         _toggle: function(toShow, toHide, data, clickedIsActive, down) {
323                 var o = this.options, self = this;
325                 this.toShow = toShow;
326                 this.toHide = toHide;
327                 this.data = data;
329                 var complete = function() { if(!self) return; return self._completed.apply(self, arguments); };
331                 // trigger changestart event
332                 this._trigger("changestart", null, this.data);
334                 // count elements to animate
335                 this.running = toHide.size() === 0 ? toShow.size() : toHide.size();
337                 if (o.animated) {
339                         var animOptions = {};
341                         if ( o.collapsible && clickedIsActive ) {
342                                 animOptions = {
343                                         toShow: $([]),
344                                         toHide: toHide,
345                                         complete: complete,
346                                         down: down,
347                                         autoHeight: o.autoHeight || o.fillSpace
348                                 };
349                         } else {
350                                 animOptions = {
351                                         toShow: toShow,
352                                         toHide: toHide,
353                                         complete: complete,
354                                         down: down,
355                                         autoHeight: o.autoHeight || o.fillSpace
356                                 };
357                         }
359                         if (!o.proxied) {
360                                 o.proxied = o.animated;
361                         }
363                         if (!o.proxiedDuration) {
364                                 o.proxiedDuration = o.duration;
365                         }
367                         o.animated = $.isFunction(o.proxied) ?
368                                 o.proxied(animOptions) : o.proxied;
370                         o.duration = $.isFunction(o.proxiedDuration) ?
371                                 o.proxiedDuration(animOptions) : o.proxiedDuration;
373                         var animations = $.ui.accordion.animations,
374                                 duration = o.duration,
375                                 easing = o.animated;
377                         if (easing && !animations[easing] && !$.easing[easing]) {
378                                 easing = 'slide';
379                         }
380                         if (!animations[easing]) {
381                                 animations[easing] = function(options) {
382                                         this.slide(options, {
383                                                 easing: easing,
384                                                 duration: duration || 700
385                                         });
386                                 };
387                         }
389                         animations[easing](animOptions);
391                 } else {
393                         if (o.collapsible && clickedIsActive) {
394                                 toShow.toggle();
395                         } else {
396                                 toHide.hide();
397                                 toShow.show();
398                         }
400                         complete(true);
402                 }
404                 // TODO assert that the blur and focus triggers are really necessary, remove otherwise
405                 toHide.prev().attr('aria-expanded','false').attr("tabIndex", "-1").blur();
406                 toShow.prev().attr('aria-expanded','true').attr("tabIndex", "0").focus();
408         },
410         _completed: function(cancel) {
412                 var o = this.options;
414                 this.running = cancel ? 0 : --this.running;
415                 if (this.running) return;
417                 if (o.clearStyle) {
418                         this.toShow.add(this.toHide).css({
419                                 height: "",
420                                 overflow: ""
421                         });
422                 }
423                 
424                 // other classes are removed before the animation; this one needs to stay until completed
425                 this.toHide.removeClass("ui-accordion-content-active");
427                 this._trigger('change', null, this.data);
428         }
433 $.extend($.ui.accordion, {
434         version: "1.8.2",
435         animations: {
436                 slide: function(options, additions) {
437                         options = $.extend({
438                                 easing: "swing",
439                                 duration: 300
440                         }, options, additions);
441                         if ( !options.toHide.size() ) {
442                                 options.toShow.animate({height: "show"}, options);
443                                 return;
444                         }
445                         if ( !options.toShow.size() ) {
446                                 options.toHide.animate({height: "hide"}, options);
447                                 return;
448                         }
449                         var overflow = options.toShow.css('overflow'),
450                                 percentDone = 0,
451                                 showProps = {},
452                                 hideProps = {},
453                                 fxAttrs = [ "height", "paddingTop", "paddingBottom" ],
454                                 originalWidth;
455                         // fix width before calculating height of hidden element
456                         var s = options.toShow;
457                         originalWidth = s[0].style.width;
458                         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) );
459                         
460                         $.each(fxAttrs, function(i, prop) {
461                                 hideProps[prop] = 'hide';
462                                 
463                                 var parts = ('' + $.css(options.toShow[0], prop)).match(/^([\d+-.]+)(.*)$/);
464                                 showProps[prop] = {
465                                         value: parts[1],
466                                         unit: parts[2] || 'px'
467                                 };
468                         });
469                         options.toShow.css({ height: 0, overflow: 'hidden' }).show();
470                         options.toHide.filter(":hidden").each(options.complete).end().filter(":visible").animate(hideProps,{
471                                 step: function(now, settings) {
472                                         // only calculate the percent when animating height
473                                         // IE gets very inconsistent results when animating elements
474                                         // with small values, which is common for padding
475                                         if (settings.prop == 'height') {
476                                                 percentDone = ( settings.end - settings.start === 0 ) ? 0 :
477                                                         (settings.now - settings.start) / (settings.end - settings.start);
478                                         }
479                                         
480                                         options.toShow[0].style[settings.prop] =
481                                                 (percentDone * showProps[settings.prop].value) + showProps[settings.prop].unit;
482                                 },
483                                 duration: options.duration,
484                                 easing: options.easing,
485                                 complete: function() {
486                                         if ( !options.autoHeight ) {
487                                                 options.toShow.css("height", "");
488                                         }
489                                         options.toShow.css("width", originalWidth);
490                                         options.toShow.css({overflow: overflow});
491                                         options.complete();
492                                 }
493                         });
494                 },
495                 bounceslide: function(options) {
496                         this.slide(options, {
497                                 easing: options.down ? "easeOutBounce" : "swing",
498                                 duration: options.down ? 1000 : 200
499                         });
500                 }
501         }
504 })(jQuery);