Set privacy note font to small.
[modest] / www / jquery.accordion.js
1 /*
2  * Accordion 1.5 - jQuery menu widget
3  *
4  * Copyright (c) 2007 Jörn Zaefferer, Frank Marcia
5  *
6  * http://bassistance.de/jquery-plugins/jquery-plugin-accordion/
7  *
8  * Dual licensed under the MIT and GPL licenses:
9  *   http://www.opensource.org/licenses/mit-license.php
10  *   http://www.gnu.org/licenses/gpl.html
11  *
12  * Revision: $Id: jquery.accordion.js 2880 2007-08-24 21:44:37Z joern.zaefferer $
13  *
14  */
15
16 /**
17  * Make the selected elements Accordion widgets.
18  *
19  * Semantic requirements:
20  *
21  * If the structure of your container is flat with unique
22  * tags for header and content elements, eg. a definition list
23  * (dl > dt + dd), you don't have to specify any options at
24  * all.
25  *
26  * If your structure uses the same elements for header and
27  * content or uses some kind of nested structure, you have to
28  * specify the header elements, eg. via class, see the second example.
29  *
30  * Use activate(Number) to change the active content programmatically.
31  *
32  * A change event is triggered everytime the accordion changes. Apart from
33  * the event object, all arguments are jQuery objects.
34  * Arguments: event, newHeader, oldHeader, newContent, oldContent
35  *
36  * @example jQuery('#nav').Accordion();
37  * @before <dl id="nav">
38  *   <dt>Header 1</dt>
39  *   <dd>Content 1</dd>
40  *   <dt>Header 2</dt>
41  *   <dd>Content 2</dd>
42  * </dl>
43  * @desc Creates an Accordion from the given definition list
44  *
45  * @example jQuery('#nav').Accordion({
46  *   header: '.title'
47  * });
48  * @before <div id="nav">
49  *  <div>
50  *    <div class="title">Header 1</div>
51  *    <div>Content 1</div>
52  *  </div>
53  *  <div>
54  *    <div class="title">Header 2</div>
55  *    <div>Content 2</div>
56  *  </div>
57  * </div>
58  * @desc Creates an Accordion from the given div structure
59  *
60  * @example jQuery('#nav').Accordion({
61  *   header: '.head',
62  *       navigation: true
63  * });
64  * @before <ul id="nav">
65  *   <li>
66  *     <a class="head" href="books/">Books</a>
67  *     <ul>
68  *       <li><a href="books/fantasy/">Fantasy</a></li>
69  *       <li><a href="books/programming/">Programming</a></li>
70  *     </ul>
71  *   </li>
72  *   <li>
73  *     <a class="head" href="movies/">Movies</a>
74  *     <ul>
75  *       <li><a href="movies/fantasy/">Fantasy</a></li>
76  *       <li><a href="movies/programming/">Programming</a></li>
77  *     </ul>
78  *   </li>
79  * </ul>
80  * @after <ul id="nav">
81  *   <li>
82  *     <a class="head" href="">Books</a>
83  *     <ul style="display: none">
84  *       <li><a href="books/fantasy/">Fantasy</a></li>
85  *       <li><a href="books/programming/">Programming</a></li>
86  *     </ul>
87  *   </li>
88  *   <li>
89  *     <a class="head" href="">Movies</a>
90  *     <ul>
91  *       <li><a class="current" href="movies/fantasy/">Fantasy</a></li>
92  *       <li><a href="movies/programming/">Programming</a></li>
93  *     </ul>
94  *   </li>
95  * </ul>
96  * @desc Creates an Accordion from the given navigation list, activating those accordion parts
97  * that match the current location.href. Assuming the user clicked on "Fantasy" in the "Movies" section,
98  * the accordion displayed after loading the page with the "Movies" section open and the "Fantasy" link highlighted
99  * with a class "current".
100  *
101  * @example jQuery('#accordion').Accordion().change(function(event, newHeader, oldHeader, newContent, oldContent) {
102  *   jQuery('#status').html(newHeader.text());
103  * });
104  * @desc Updates the element with id status with the text of the selected header every time the accordion changes
105  *
106  * @param Map options key/value pairs of optional settings.
107  * @option String|Element|jQuery|Boolean|Number active Selector for the active element. Set to false to display none at start. Default: first child
108  * @option String|Element|jQuery header Selector for the header element, eg. 'div.title', 'a.head'. Default: first child's tagname
109  * @option String|Number speed 
110  * @option String selectedClass Class for active header elements. Default: 'selected'
111  * @option Boolean alwaysOpen Whether there must be one content element open. Default: true
112  * @option Boolean|String animated Choose your favorite animation, or disable them (set to false). In addition to the default, "bounceslide" and "easeslide" are supported (both require the easing plugin). Default: 'slide'
113  * @option String event The event on which to trigger the accordion, eg. "mouseover". Default: "click"
114  * @option Boolean navigation If set, looks for the anchor that matches location.href and activates it. Great for href-based pseudo-state-saving. Default: false
115  * @option Boolean autoheight If set, the highest content part is used as height reference for all other parts. Provides more consistent animations. Default: false
116  *
117  * @type jQuery
118  * @see activate(Number)
119  * @name Accordion
120  * @cat Plugins/Accordion
121  */
122
123 /**
124  * Activate a content part of the Accordion programmatically.
125  *
126  * The index can be a zero-indexed number to match the position of the header to close
127  * or a string expression matching an element. Pass -1 to close all (only possible with alwaysOpen:false).
128  *
129  * @example jQuery('#accordion').activate(1);
130  * @desc Activate the second content of the Accordion contained in <div id="accordion">.
131  *
132  * @example jQuery('#accordion').activate("a:first");
133  * @desc Activate the first element matching the given expression.
134  *
135  * @example jQuery('#nav').activate(false);
136  * @desc Close all content parts of the accordion.
137  *
138  * @param String|Element|jQuery|Boolean|Number index An Integer specifying the zero-based index of the content to be
139  *                               activated or an expression specifying the element, or an element/jQuery object, or a boolean false to close all.
140  *
141  * @type jQuery
142  * @name activate
143  * @cat Plugins/Accordion
144  */
145
146 (function($) {
147
148 $.Accordion = {};
149 $.extend($.Accordion, {
150         defaults: {
151                 selectedClass: "selected",
152                 alwaysOpen: true,
153                 animated: 'slide',
154                 event: "click"
155         },
156         Animations: {
157                 slide: function(settings, additions) {
158                         settings = $.extend({
159                                 easing: "swing",
160                                 duration: 300
161                         }, settings, additions);
162                         if ( !settings.toHide.size() ) {
163                                 settings.toShow.animate({height: "show"}, {
164                                         duration: settings.duration,
165                                         easing: settings.easing,
166                                         complete: settings.finished
167                                 });
168                                 return;
169                         }
170                         var height = settings.toHide.height();
171                         settings.toShow.css({ height: 0, overflow: 'hidden' }).show();
172                         settings.toHide.filter(":hidden").each(settings.finished).end().filter(":visible").animate({height:"hide"},{
173                                 step: function(n){
174                                         settings.toShow.height(Math.ceil(height - ($.fn.stop ? n * height : n)));
175                                 },
176                                 duration: settings.duration,
177                                 easing: settings.easing,
178                                 complete: settings.finished
179                         });
180                 },
181                 bounceslide: function(settings) {
182                         this.slide(settings, {
183                                 easing: settings.down ? "bounceout" : "swing",
184                                 duration: settings.down ? 1000 : 200
185                         });
186                 },
187                 easeslide: function(settings) {
188                         this.slide(settings, {
189                                 easing: "easeinout",
190                                 duration: 700
191                         })
192                 }
193         }
194 });
195
196 $.fn.extend({
197         nextUntil: function(expr) {
198             var match = [];
199         
200             // We need to figure out which elements to push onto the array
201             this.each(function(){
202                 // Traverse through the sibling nodes
203                 for( var i = this.nextSibling; i; i = i.nextSibling ) {
204                     // Make sure that we're only dealing with elements
205                     if ( i.nodeType != 1 ) continue;
206         
207                     // If we find a match then we need to stop
208                     if ( $.filter( expr, [i] ).r.length ) break;
209         
210                     // Otherwise, add it on to the stack
211                     match.push( i );
212                 }
213             });
214         
215             return this.pushStack( match );
216         },
217         // the plugin method itself
218         Accordion: function(settings) {
219                 if ( !this.length )
220                         return this;
221         
222                 // setup configuration
223                 settings = $.extend({}, $.Accordion.defaults, {
224                         // define context defaults
225                         header: $(':first-child', this)[0].tagName // take first childs tagName as header
226                 }, settings);
227                 
228                 if ( settings.navigation ) {
229                         var current = this.find("a").filter(function() { return this.href == location.href; });
230                         if ( current.length ) {
231                                 if ( current.filter(settings.header).length ) {
232                                         settings.active = current;
233                                 } else {
234                                         settings.active = current.parent().parent().prev();
235                                         current.addClass("current");
236                                 }
237                         }
238                 }
239                 
240                 // calculate active if not specified, using the first header
241                 var container = this,
242                         headers = container.find(settings.header),
243                         active = findActive(settings.active),
244                         running = 0;
245
246                 if ( settings.autoheight ) {
247                         var maxHeight = 0;
248                         headers.nextUntil(settings.header).each(function() {
249                                 maxHeight = Math.max(maxHeight, $(this).height());
250                         }).height(maxHeight);
251                 }
252
253                 headers
254                         .not(active || "")
255                         .nextUntil(settings.header)
256                         .hide();
257                 active.addClass(settings.selectedClass);
258                 
259                 
260                 function findActive(selector) {
261                         return selector != undefined
262                                 ? typeof selector == "number"
263                                         ? headers.eq(selector)
264                                         : headers.not(headers.not(selector))
265                                 : selector === false
266                                         ? $("<div>")
267                                         : headers.eq(0)
268                 }
269                 
270                 function toggle(toShow, toHide, data, clickedActive, down) {
271                         var finished = function(cancel) {
272                                 running = cancel ? 0 : --running;
273                                 if ( running )
274                                         return;
275                                 // trigger custom change event
276                                 container.trigger("change", data);
277                         };
278                         
279                         // count elements to animate
280                         running = toHide.size() == 0 ? toShow.size() : toHide.size();
281                         
282                         if ( settings.animated ) {
283                                 if ( !settings.alwaysOpen && clickedActive ) {
284                                         toShow.slideToggle(settings.animated);
285                                         finished(true);
286                                 } else {
287                                         $.Accordion.Animations[settings.animated]({
288                                                 toShow: toShow,
289                                                 toHide: toHide,
290                                                 finished: finished,
291                                                 down: down
292                                         });
293                                 }
294                         } else {
295                                 if ( !settings.alwaysOpen && clickedActive ) {
296                                         toShow.toggle();
297                                 } else {
298                                         toHide.hide();
299                                         toShow.show();
300                                 }
301                                 finished(true);
302                         }
303                 }
304                 
305                 function clickHandler(event) {
306                         // called only when using activate(false) to close all parts programmatically
307                         if ( !event.target && !settings.alwaysOpen ) {
308                                 active.toggleClass(settings.selectedClass);
309                                 var toHide = active.nextUntil(settings.header);
310                                 var toShow = active = $([]);
311                                 toggle( toShow, toHide );
312                                 return;
313                         }
314                         // get the click target
315                         var clicked = $(event.target);
316                         
317                         // due to the event delegation model, we have to check if one
318                         // of the parent elements is our actual header, and find that
319                         if ( clicked.parents(settings.header).length )
320                                 while ( !clicked.is(settings.header) )
321                                         clicked = clicked.parent();
322                         
323                         var clickedActive = clicked[0] == active[0];
324                         
325                         // if animations are still active, or the active header is the target, ignore click
326                         if(running || (settings.alwaysOpen && clickedActive) || !clicked.is(settings.header))
327                                 return;
328
329                         // switch classes
330                         active.toggleClass(settings.selectedClass);
331                         if ( !clickedActive ) {
332                                 clicked.addClass(settings.selectedClass);
333                         }
334
335                         // find elements to show and hide
336                         var toShow = clicked.nextUntil(settings.header),
337                                 toHide = active.nextUntil(settings.header),
338                                 data = [clicked, active, toShow, toHide],
339                                 down = headers.index( active[0] ) > headers.index( clicked[0] );
340                         
341                         active = clickedActive ? $([]) : clicked;
342                         toggle( toShow, toHide, data, clickedActive, down );
343
344                         return !toShow.length;
345                 };
346                 function activateHandler(event, index) {
347                         // IE manages to call activateHandler on normal clicks
348                         if ( arguments.length == 1 )
349                                 return;
350                         // call clickHandler with custom event
351                         clickHandler({
352                                 target: findActive(index)[0]
353                         });
354                 };
355
356                 return container
357                         .bind(settings.event, clickHandler)
358                         .bind("activate", activateHandler);
359         },
360         activate: function(index) {
361                 return this.trigger('activate', [index]);
362         }
363 });
364
365 })(jQuery);