accordion.js 20 KB

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