accordion.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584
  1. /*!
  2. * jQuery UI Accordion 1.11.2
  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: "1.11.2",
  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. var prevHeaders = this.headers,
  229. prevPanels = this.panels;
  230. this.headers = this.element.find( this.options.header )
  231. .addClass( "ui-accordion-header ui-state-default ui-corner-all" );
  232. this.panels = this.headers.next()
  233. .addClass( "ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom" )
  234. .filter( ":not(.ui-accordion-content-active)" )
  235. .hide();
  236. // Avoid memory leaks (#10056)
  237. if ( prevPanels ) {
  238. this._off( prevHeaders.not( this.headers ) );
  239. this._off( prevPanels.not( this.panels ) );
  240. }
  241. },
  242. _refresh: function() {
  243. var maxHeight,
  244. options = this.options,
  245. heightStyle = options.heightStyle,
  246. parent = this.element.parent();
  247. this.active = this._findActive( options.active )
  248. .addClass( "ui-accordion-header-active ui-state-active ui-corner-top" )
  249. .removeClass( "ui-corner-all" );
  250. this.active.next()
  251. .addClass( "ui-accordion-content-active" )
  252. .show();
  253. this.headers
  254. .attr( "role", "tab" )
  255. .each(function() {
  256. var header = $( this ),
  257. headerId = header.uniqueId().attr( "id" ),
  258. panel = header.next(),
  259. panelId = panel.uniqueId().attr( "id" );
  260. header.attr( "aria-controls", panelId );
  261. panel.attr( "aria-labelledby", headerId );
  262. })
  263. .next()
  264. .attr( "role", "tabpanel" );
  265. this.headers
  266. .not( this.active )
  267. .attr({
  268. "aria-selected": "false",
  269. "aria-expanded": "false",
  270. tabIndex: -1
  271. })
  272. .next()
  273. .attr({
  274. "aria-hidden": "true"
  275. })
  276. .hide();
  277. // make sure at least one header is in the tab order
  278. if ( !this.active.length ) {
  279. this.headers.eq( 0 ).attr( "tabIndex", 0 );
  280. } else {
  281. this.active.attr({
  282. "aria-selected": "true",
  283. "aria-expanded": "true",
  284. tabIndex: 0
  285. })
  286. .next()
  287. .attr({
  288. "aria-hidden": "false"
  289. });
  290. }
  291. this._createIcons();
  292. this._setupEvents( options.event );
  293. if ( heightStyle === "fill" ) {
  294. maxHeight = parent.height();
  295. this.element.siblings( ":visible" ).each(function() {
  296. var elem = $( this ),
  297. position = elem.css( "position" );
  298. if ( position === "absolute" || position === "fixed" ) {
  299. return;
  300. }
  301. maxHeight -= elem.outerHeight( true );
  302. });
  303. this.headers.each(function() {
  304. maxHeight -= $( this ).outerHeight( true );
  305. });
  306. this.headers.next()
  307. .each(function() {
  308. $( this ).height( Math.max( 0, maxHeight -
  309. $( this ).innerHeight() + $( this ).height() ) );
  310. })
  311. .css( "overflow", "auto" );
  312. } else if ( heightStyle === "auto" ) {
  313. maxHeight = 0;
  314. this.headers.next()
  315. .each(function() {
  316. maxHeight = Math.max( maxHeight, $( this ).css( "height", "" ).height() );
  317. })
  318. .height( maxHeight );
  319. }
  320. },
  321. _activate: function( index ) {
  322. var active = this._findActive( index )[ 0 ];
  323. // trying to activate the already active panel
  324. if ( active === this.active[ 0 ] ) {
  325. return;
  326. }
  327. // trying to collapse, simulate a click on the currently active header
  328. active = active || this.active[ 0 ];
  329. this._eventHandler({
  330. target: active,
  331. currentTarget: active,
  332. preventDefault: $.noop
  333. });
  334. },
  335. _findActive: function( selector ) {
  336. return typeof selector === "number" ? this.headers.eq( selector ) : $();
  337. },
  338. _setupEvents: function( event ) {
  339. var events = {
  340. keydown: "_keydown"
  341. };
  342. if ( event ) {
  343. $.each( event.split( " " ), function( index, eventName ) {
  344. events[ eventName ] = "_eventHandler";
  345. });
  346. }
  347. this._off( this.headers.add( this.headers.next() ) );
  348. this._on( this.headers, events );
  349. this._on( this.headers.next(), { keydown: "_panelKeyDown" });
  350. this._hoverable( this.headers );
  351. this._focusable( this.headers );
  352. },
  353. _eventHandler: function( event ) {
  354. var options = this.options,
  355. active = this.active,
  356. clicked = $( event.currentTarget ),
  357. clickedIsActive = clicked[ 0 ] === active[ 0 ],
  358. collapsing = clickedIsActive && options.collapsible,
  359. toShow = collapsing ? $() : clicked.next(),
  360. toHide = active.next(),
  361. eventData = {
  362. oldHeader: active,
  363. oldPanel: toHide,
  364. newHeader: collapsing ? $() : clicked,
  365. newPanel: toShow
  366. };
  367. event.preventDefault();
  368. if (
  369. // click on active header, but not collapsible
  370. ( clickedIsActive && !options.collapsible ) ||
  371. // allow canceling activation
  372. ( this._trigger( "beforeActivate", event, eventData ) === false ) ) {
  373. return;
  374. }
  375. options.active = collapsing ? false : this.headers.index( clicked );
  376. // when the call to ._toggle() comes after the class changes
  377. // it causes a very odd bug in IE 8 (see #6720)
  378. this.active = clickedIsActive ? $() : clicked;
  379. this._toggle( eventData );
  380. // switch classes
  381. // corner classes on the previously active header stay after the animation
  382. active.removeClass( "ui-accordion-header-active ui-state-active" );
  383. if ( options.icons ) {
  384. active.children( ".ui-accordion-header-icon" )
  385. .removeClass( options.icons.activeHeader )
  386. .addClass( options.icons.header );
  387. }
  388. if ( !clickedIsActive ) {
  389. clicked
  390. .removeClass( "ui-corner-all" )
  391. .addClass( "ui-accordion-header-active ui-state-active ui-corner-top" );
  392. if ( options.icons ) {
  393. clicked.children( ".ui-accordion-header-icon" )
  394. .removeClass( options.icons.header )
  395. .addClass( options.icons.activeHeader );
  396. }
  397. clicked
  398. .next()
  399. .addClass( "ui-accordion-content-active" );
  400. }
  401. },
  402. _toggle: function( data ) {
  403. var toShow = data.newPanel,
  404. toHide = this.prevShow.length ? this.prevShow : data.oldPanel;
  405. // handle activating a panel during the animation for another activation
  406. this.prevShow.add( this.prevHide ).stop( true, true );
  407. this.prevShow = toShow;
  408. this.prevHide = toHide;
  409. if ( this.options.animate ) {
  410. this._animate( toShow, toHide, data );
  411. } else {
  412. toHide.hide();
  413. toShow.show();
  414. this._toggleComplete( data );
  415. }
  416. toHide.attr({
  417. "aria-hidden": "true"
  418. });
  419. toHide.prev().attr( "aria-selected", "false" );
  420. // if we're switching panels, remove the old header from the tab order
  421. // if we're opening from collapsed state, remove the previous header from the tab order
  422. // if we're collapsing, then keep the collapsing header in the tab order
  423. if ( toShow.length && toHide.length ) {
  424. toHide.prev().attr({
  425. "tabIndex": -1,
  426. "aria-expanded": "false"
  427. });
  428. } else if ( toShow.length ) {
  429. this.headers.filter(function() {
  430. return $( this ).attr( "tabIndex" ) === 0;
  431. })
  432. .attr( "tabIndex", -1 );
  433. }
  434. toShow
  435. .attr( "aria-hidden", "false" )
  436. .prev()
  437. .attr({
  438. "aria-selected": "true",
  439. tabIndex: 0,
  440. "aria-expanded": "true"
  441. });
  442. },
  443. _animate: function( toShow, toHide, data ) {
  444. var total, easing, duration,
  445. that = this,
  446. adjust = 0,
  447. down = toShow.length &&
  448. ( !toHide.length || ( toShow.index() < toHide.index() ) ),
  449. animate = this.options.animate || {},
  450. options = down && animate.down || animate,
  451. complete = function() {
  452. that._toggleComplete( data );
  453. };
  454. if ( typeof options === "number" ) {
  455. duration = options;
  456. }
  457. if ( typeof options === "string" ) {
  458. easing = options;
  459. }
  460. // fall back from options to animation in case of partial down settings
  461. easing = easing || options.easing || animate.easing;
  462. duration = duration || options.duration || animate.duration;
  463. if ( !toHide.length ) {
  464. return toShow.animate( this.showProps, duration, easing, complete );
  465. }
  466. if ( !toShow.length ) {
  467. return toHide.animate( this.hideProps, duration, easing, complete );
  468. }
  469. total = toShow.show().outerHeight();
  470. toHide.animate( this.hideProps, {
  471. duration: duration,
  472. easing: easing,
  473. step: function( now, fx ) {
  474. fx.now = Math.round( now );
  475. }
  476. });
  477. toShow
  478. .hide()
  479. .animate( this.showProps, {
  480. duration: duration,
  481. easing: easing,
  482. complete: complete,
  483. step: function( now, fx ) {
  484. fx.now = Math.round( now );
  485. if ( fx.prop !== "height" ) {
  486. adjust += fx.now;
  487. } else if ( that.options.heightStyle !== "content" ) {
  488. fx.now = Math.round( total - toHide.outerHeight() - adjust );
  489. adjust = 0;
  490. }
  491. }
  492. });
  493. },
  494. _toggleComplete: function( data ) {
  495. var toHide = data.oldPanel;
  496. toHide
  497. .removeClass( "ui-accordion-content-active" )
  498. .prev()
  499. .removeClass( "ui-corner-top" )
  500. .addClass( "ui-corner-all" );
  501. // Work around for rendering bug in IE (#5421)
  502. if ( toHide.length ) {
  503. toHide.parent()[ 0 ].className = toHide.parent()[ 0 ].className;
  504. }
  505. this._trigger( "activate", null, data );
  506. }
  507. });
  508. }));