accordion.js (15864B)
1 /*! 2 * jQuery UI Accordion 1.12.1 3 * http://jqueryui.com 4 * 5 * Copyright jQuery Foundation and other contributors 6 * Released under the MIT license. 7 * http://jquery.org/license 8 */ 9 10 //>>label: Accordion 11 //>>group: Widgets 12 // jscs:disable maximumLineLength 13 //>>description: Displays collapsible content panels for presenting information in a limited amount of space. 14 // jscs:enable maximumLineLength 15 //>>docs: http://api.jqueryui.com/accordion/ 16 //>>demos: http://jqueryui.com/accordion/ 17 //>>css.structure: ../../themes/base/core.css 18 //>>css.structure: ../../themes/base/accordion.css 19 //>>css.theme: ../../themes/base/theme.css 20 21 ( function( factory ) { 22 if ( typeof define === "function" && define.amd ) { 23 24 // AMD. Register as an anonymous module. 25 define( [ 26 "jquery", 27 "./core" 28 ], factory ); 29 } else { 30 31 // Browser globals 32 factory( jQuery ); 33 } 34 }( function( $ ) { 35 36 return $.widget( "ui.accordion", { 37 version: "1.12.1", 38 options: { 39 active: 0, 40 animate: {}, 41 classes: { 42 "ui-accordion-header": "ui-corner-top", 43 "ui-accordion-header-collapsed": "ui-corner-all", 44 "ui-accordion-content": "ui-corner-bottom" 45 }, 46 collapsible: false, 47 event: "click", 48 header: "> li > :first-child, > :not(li):even", 49 heightStyle: "auto", 50 icons: { 51 activeHeader: "ui-icon-triangle-1-s", 52 header: "ui-icon-triangle-1-e" 53 }, 54 55 // Callbacks 56 activate: null, 57 beforeActivate: null 58 }, 59 60 hideProps: { 61 borderTopWidth: "hide", 62 borderBottomWidth: "hide", 63 paddingTop: "hide", 64 paddingBottom: "hide", 65 height: "hide" 66 }, 67 68 showProps: { 69 borderTopWidth: "show", 70 borderBottomWidth: "show", 71 paddingTop: "show", 72 paddingBottom: "show", 73 height: "show" 74 }, 75 76 _create: function() { 77 var options = this.options; 78 79 this.prevShow = this.prevHide = $(); 80 this._addClass( "ui-accordion", "ui-widget ui-helper-reset" ); 81 this.element.attr( "role", "tablist" ); 82 83 // Don't allow collapsible: false and active: false / null 84 if ( !options.collapsible && ( options.active === false || options.active == null ) ) { 85 options.active = 0; 86 } 87 88 this._processPanels(); 89 90 // handle negative values 91 if ( options.active < 0 ) { 92 options.active += this.headers.length; 93 } 94 this._refresh(); 95 }, 96 97 _getCreateEventData: function() { 98 return { 99 header: this.active, 100 panel: !this.active.length ? $() : this.active.next() 101 }; 102 }, 103 104 _createIcons: function() { 105 var icon, children, 106 icons = this.options.icons; 107 108 if ( icons ) { 109 icon = $( "<span>" ); 110 this._addClass( icon, "ui-accordion-header-icon", "ui-icon " + icons.header ); 111 icon.prependTo( this.headers ); 112 children = this.active.children( ".ui-accordion-header-icon" ); 113 this._removeClass( children, icons.header ) 114 ._addClass( children, null, icons.activeHeader ) 115 ._addClass( this.headers, "ui-accordion-icons" ); 116 } 117 }, 118 119 _destroyIcons: function() { 120 this._removeClass( this.headers, "ui-accordion-icons" ); 121 this.headers.children( ".ui-accordion-header-icon" ).remove(); 122 }, 123 124 _destroy: function() { 125 var contents; 126 127 // Clean up main element 128 this.element.removeAttr( "role" ); 129 130 // Clean up headers 131 this.headers 132 .removeAttr( "role aria-expanded aria-selected aria-controls tabIndex" ) 133 .removeUniqueId(); 134 135 this._destroyIcons(); 136 137 // Clean up content panels 138 contents = this.headers.next() 139 .css( "display", "" ) 140 .removeAttr( "role aria-hidden aria-labelledby" ) 141 .removeUniqueId(); 142 143 if ( this.options.heightStyle !== "content" ) { 144 contents.css( "height", "" ); 145 } 146 }, 147 148 _setOption: function( key, value ) { 149 if ( key === "active" ) { 150 151 // _activate() will handle invalid values and update this.options 152 this._activate( value ); 153 return; 154 } 155 156 if ( key === "event" ) { 157 if ( this.options.event ) { 158 this._off( this.headers, this.options.event ); 159 } 160 this._setupEvents( value ); 161 } 162 163 this._super( key, value ); 164 165 // Setting collapsible: false while collapsed; open first panel 166 if ( key === "collapsible" && !value && this.options.active === false ) { 167 this._activate( 0 ); 168 } 169 170 if ( key === "icons" ) { 171 this._destroyIcons(); 172 if ( value ) { 173 this._createIcons(); 174 } 175 } 176 }, 177 178 _setOptionDisabled: function( value ) { 179 this._super( value ); 180 181 this.element.attr( "aria-disabled", value ); 182 183 // Support: IE8 Only 184 // #5332 / #6059 - opacity doesn't cascade to positioned elements in IE 185 // so we need to add the disabled class to the headers and panels 186 this._toggleClass( null, "ui-state-disabled", !!value ); 187 this._toggleClass( this.headers.add( this.headers.next() ), null, "ui-state-disabled", 188 !!value ); 189 }, 190 191 _keydown: function( event ) { 192 if ( event.altKey || event.ctrlKey ) { 193 return; 194 } 195 196 var keyCode = $.ui.keyCode, 197 length = this.headers.length, 198 currentIndex = this.headers.index( event.target ), 199 toFocus = false; 200 201 switch ( event.keyCode ) { 202 case keyCode.RIGHT: 203 case keyCode.DOWN: 204 toFocus = this.headers[ ( currentIndex + 1 ) % length ]; 205 break; 206 case keyCode.LEFT: 207 case keyCode.UP: 208 toFocus = this.headers[ ( currentIndex - 1 + length ) % length ]; 209 break; 210 case keyCode.SPACE: 211 case keyCode.ENTER: 212 this._eventHandler( event ); 213 break; 214 case keyCode.HOME: 215 toFocus = this.headers[ 0 ]; 216 break; 217 case keyCode.END: 218 toFocus = this.headers[ length - 1 ]; 219 break; 220 } 221 222 if ( toFocus ) { 223 $( event.target ).attr( "tabIndex", -1 ); 224 $( toFocus ).attr( "tabIndex", 0 ); 225 $( toFocus ).trigger( "focus" ); 226 event.preventDefault(); 227 } 228 }, 229 230 _panelKeyDown: function( event ) { 231 if ( event.keyCode === $.ui.keyCode.UP && event.ctrlKey ) { 232 $( event.currentTarget ).prev().trigger( "focus" ); 233 } 234 }, 235 236 refresh: function() { 237 var options = this.options; 238 this._processPanels(); 239 240 // Was collapsed or no panel 241 if ( ( options.active === false && options.collapsible === true ) || 242 !this.headers.length ) { 243 options.active = false; 244 this.active = $(); 245 246 // active false only when collapsible is true 247 } else if ( options.active === false ) { 248 this._activate( 0 ); 249 250 // was active, but active panel is gone 251 } else if ( this.active.length && !$.contains( this.element[ 0 ], this.active[ 0 ] ) ) { 252 253 // all remaining panel are disabled 254 if ( this.headers.length === this.headers.find( ".ui-state-disabled" ).length ) { 255 options.active = false; 256 this.active = $(); 257 258 // activate previous panel 259 } else { 260 this._activate( Math.max( 0, options.active - 1 ) ); 261 } 262 263 // was active, active panel still exists 264 } else { 265 266 // make sure active index is correct 267 options.active = this.headers.index( this.active ); 268 } 269 270 this._destroyIcons(); 271 272 this._refresh(); 273 }, 274 275 _processPanels: function() { 276 var prevHeaders = this.headers, 277 prevPanels = this.panels; 278 279 this.headers = this.element.find( this.options.header ); 280 this._addClass( this.headers, "ui-accordion-header ui-accordion-header-collapsed", 281 "ui-state-default" ); 282 283 this.panels = this.headers.next().filter( ":not(.ui-accordion-content-active)" ).hide(); 284 this._addClass( this.panels, "ui-accordion-content", "ui-helper-reset ui-widget-content" ); 285 286 // Avoid memory leaks (#10056) 287 if ( prevPanels ) { 288 this._off( prevHeaders.not( this.headers ) ); 289 this._off( prevPanels.not( this.panels ) ); 290 } 291 }, 292 293 _refresh: function() { 294 var maxHeight, 295 options = this.options, 296 heightStyle = options.heightStyle, 297 parent = this.element.parent(); 298 299 this.active = this._findActive( options.active ); 300 this._addClass( this.active, "ui-accordion-header-active", "ui-state-active" ) 301 ._removeClass( this.active, "ui-accordion-header-collapsed" ); 302 this._addClass( this.active.next(), "ui-accordion-content-active" ); 303 this.active.next().show(); 304 305 this.headers 306 .attr( "role", "tab" ) 307 .each( function() { 308 var header = $( this ), 309 headerId = header.uniqueId().attr( "id" ), 310 panel = header.next(), 311 panelId = panel.uniqueId().attr( "id" ); 312 header.attr( "aria-controls", panelId ); 313 panel.attr( "aria-labelledby", headerId ); 314 } ) 315 .next() 316 .attr( "role", "tabpanel" ); 317 318 this.headers 319 .not( this.active ) 320 .attr( { 321 "aria-selected": "false", 322 "aria-expanded": "false", 323 tabIndex: -1 324 } ) 325 .next() 326 .attr( { 327 "aria-hidden": "true" 328 } ) 329 .hide(); 330 331 // Make sure at least one header is in the tab order 332 if ( !this.active.length ) { 333 this.headers.eq( 0 ).attr( "tabIndex", 0 ); 334 } else { 335 this.active.attr( { 336 "aria-selected": "true", 337 "aria-expanded": "true", 338 tabIndex: 0 339 } ) 340 .next() 341 .attr( { 342 "aria-hidden": "false" 343 } ); 344 } 345 346 this._createIcons(); 347 348 this._setupEvents( options.event ); 349 350 if ( heightStyle === "fill" ) { 351 maxHeight = parent.height(); 352 this.element.siblings( ":visible" ).each( function() { 353 var elem = $( this ), 354 position = elem.css( "position" ); 355 356 if ( position === "absolute" || position === "fixed" ) { 357 return; 358 } 359 maxHeight -= elem.outerHeight( true ); 360 } ); 361 362 this.headers.each( function() { 363 maxHeight -= $( this ).outerHeight( true ); 364 } ); 365 366 this.headers.next() 367 .each( function() { 368 $( this ).height( Math.max( 0, maxHeight - 369 $( this ).innerHeight() + $( this ).height() ) ); 370 } ) 371 .css( "overflow", "auto" ); 372 } else if ( heightStyle === "auto" ) { 373 maxHeight = 0; 374 this.headers.next() 375 .each( function() { 376 var isVisible = $( this ).is( ":visible" ); 377 if ( !isVisible ) { 378 $( this ).show(); 379 } 380 maxHeight = Math.max( maxHeight, $( this ).css( "height", "" ).height() ); 381 if ( !isVisible ) { 382 $( this ).hide(); 383 } 384 } ) 385 .height( maxHeight ); 386 } 387 }, 388 389 _activate: function( index ) { 390 var active = this._findActive( index )[ 0 ]; 391 392 // Trying to activate the already active panel 393 if ( active === this.active[ 0 ] ) { 394 return; 395 } 396 397 // Trying to collapse, simulate a click on the currently active header 398 active = active || this.active[ 0 ]; 399 400 this._eventHandler( { 401 target: active, 402 currentTarget: active, 403 preventDefault: $.noop 404 } ); 405 }, 406 407 _findActive: function( selector ) { 408 return typeof selector === "number" ? this.headers.eq( selector ) : $(); 409 }, 410 411 _setupEvents: function( event ) { 412 var events = { 413 keydown: "_keydown" 414 }; 415 if ( event ) { 416 $.each( event.split( " " ), function( index, eventName ) { 417 events[ eventName ] = "_eventHandler"; 418 } ); 419 } 420 421 this._off( this.headers.add( this.headers.next() ) ); 422 this._on( this.headers, events ); 423 this._on( this.headers.next(), { keydown: "_panelKeyDown" } ); 424 this._hoverable( this.headers ); 425 this._focusable( this.headers ); 426 }, 427 428 _eventHandler: function( event ) { 429 var activeChildren, clickedChildren, 430 options = this.options, 431 active = this.active, 432 clicked = $( event.currentTarget ), 433 clickedIsActive = clicked[ 0 ] === active[ 0 ], 434 collapsing = clickedIsActive && options.collapsible, 435 toShow = collapsing ? $() : clicked.next(), 436 toHide = active.next(), 437 eventData = { 438 oldHeader: active, 439 oldPanel: toHide, 440 newHeader: collapsing ? $() : clicked, 441 newPanel: toShow 442 }; 443 444 event.preventDefault(); 445 446 if ( 447 448 // click on active header, but not collapsible 449 ( clickedIsActive && !options.collapsible ) || 450 451 // allow canceling activation 452 ( this._trigger( "beforeActivate", event, eventData ) === false ) ) { 453 return; 454 } 455 456 options.active = collapsing ? false : this.headers.index( clicked ); 457 458 // When the call to ._toggle() comes after the class changes 459 // it causes a very odd bug in IE 8 (see #6720) 460 this.active = clickedIsActive ? $() : clicked; 461 this._toggle( eventData ); 462 463 // Switch classes 464 // corner classes on the previously active header stay after the animation 465 this._removeClass( active, "ui-accordion-header-active", "ui-state-active" ); 466 if ( options.icons ) { 467 activeChildren = active.children( ".ui-accordion-header-icon" ); 468 this._removeClass( activeChildren, null, options.icons.activeHeader ) 469 ._addClass( activeChildren, null, options.icons.header ); 470 } 471 472 if ( !clickedIsActive ) { 473 this._removeClass( clicked, "ui-accordion-header-collapsed" ) 474 ._addClass( clicked, "ui-accordion-header-active", "ui-state-active" ); 475 if ( options.icons ) { 476 clickedChildren = clicked.children( ".ui-accordion-header-icon" ); 477 this._removeClass( clickedChildren, null, options.icons.header ) 478 ._addClass( clickedChildren, null, options.icons.activeHeader ); 479 } 480 481 this._addClass( clicked.next(), "ui-accordion-content-active" ); 482 } 483 }, 484 485 _toggle: function( data ) { 486 var toShow = data.newPanel, 487 toHide = this.prevShow.length ? this.prevShow : data.oldPanel; 488 489 // Handle activating a panel during the animation for another activation 490 this.prevShow.add( this.prevHide ).stop( true, true ); 491 this.prevShow = toShow; 492 this.prevHide = toHide; 493 494 if ( this.options.animate ) { 495 this._animate( toShow, toHide, data ); 496 } else { 497 toHide.hide(); 498 toShow.show(); 499 this._toggleComplete( data ); 500 } 501 502 toHide.attr( { 503 "aria-hidden": "true" 504 } ); 505 toHide.prev().attr( { 506 "aria-selected": "false", 507 "aria-expanded": "false" 508 } ); 509 510 // if we're switching panels, remove the old header from the tab order 511 // if we're opening from collapsed state, remove the previous header from the tab order 512 // if we're collapsing, then keep the collapsing header in the tab order 513 if ( toShow.length && toHide.length ) { 514 toHide.prev().attr( { 515 "tabIndex": -1, 516 "aria-expanded": "false" 517 } ); 518 } else if ( toShow.length ) { 519 this.headers.filter( function() { 520 return parseInt( $( this ).attr( "tabIndex" ), 10 ) === 0; 521 } ) 522 .attr( "tabIndex", -1 ); 523 } 524 525 toShow 526 .attr( "aria-hidden", "false" ) 527 .prev() 528 .attr( { 529 "aria-selected": "true", 530 "aria-expanded": "true", 531 tabIndex: 0 532 } ); 533 }, 534 535 _animate: function( toShow, toHide, data ) { 536 var total, easing, duration, 537 that = this, 538 adjust = 0, 539 boxSizing = toShow.css( "box-sizing" ), 540 down = toShow.length && 541 ( !toHide.length || ( toShow.index() < toHide.index() ) ), 542 animate = this.options.animate || {}, 543 options = down && animate.down || animate, 544 complete = function() { 545 that._toggleComplete( data ); 546 }; 547 548 if ( typeof options === "number" ) { 549 duration = options; 550 } 551 if ( typeof options === "string" ) { 552 easing = options; 553 } 554 555 // fall back from options to animation in case of partial down settings 556 easing = easing || options.easing || animate.easing; 557 duration = duration || options.duration || animate.duration; 558 559 if ( !toHide.length ) { 560 return toShow.animate( this.showProps, duration, easing, complete ); 561 } 562 if ( !toShow.length ) { 563 return toHide.animate( this.hideProps, duration, easing, complete ); 564 } 565 566 total = toShow.show().outerHeight(); 567 toHide.animate( this.hideProps, { 568 duration: duration, 569 easing: easing, 570 step: function( now, fx ) { 571 fx.now = Math.round( now ); 572 } 573 } ); 574 toShow 575 .hide() 576 .animate( this.showProps, { 577 duration: duration, 578 easing: easing, 579 complete: complete, 580 step: function( now, fx ) { 581 fx.now = Math.round( now ); 582 if ( fx.prop !== "height" ) { 583 if ( boxSizing === "content-box" ) { 584 adjust += fx.now; 585 } 586 } else if ( that.options.heightStyle !== "content" ) { 587 fx.now = Math.round( total - toHide.outerHeight() - adjust ); 588 adjust = 0; 589 } 590 } 591 } ); 592 }, 593 594 _toggleComplete: function( data ) { 595 var toHide = data.oldPanel, 596 prev = toHide.prev(); 597 598 this._removeClass( toHide, "ui-accordion-content-active" ); 599 this._removeClass( prev, "ui-accordion-header-active" ) 600 ._addClass( prev, "ui-accordion-header-collapsed" ); 601 602 // Work around for rendering bug in IE (#5421) 603 if ( toHide.length ) { 604 toHide.parent()[ 0 ].className = toHide.parent()[ 0 ].className; 605 } 606 this._trigger( "activate", null, data ); 607 } 608 } ); 609 610 } ) );