accordion.js 16 KB

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