collapse.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. import $ from 'jquery'
  2. import Util from './util'
  3. /**
  4. * --------------------------------------------------------------------------
  5. * Bootstrap (v4.1.3): collapse.js
  6. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
  7. * --------------------------------------------------------------------------
  8. */
  9. const Collapse = (($) => {
  10. /**
  11. * ------------------------------------------------------------------------
  12. * Constants
  13. * ------------------------------------------------------------------------
  14. */
  15. const NAME = 'collapse'
  16. const VERSION = '4.1.3'
  17. const DATA_KEY = 'bs.collapse'
  18. const EVENT_KEY = `.${DATA_KEY}`
  19. const DATA_API_KEY = '.data-api'
  20. const JQUERY_NO_CONFLICT = $.fn[NAME]
  21. const Default = {
  22. toggle : true,
  23. parent : ''
  24. }
  25. const DefaultType = {
  26. toggle : 'boolean',
  27. parent : '(string|element)'
  28. }
  29. const Event = {
  30. SHOW : `show${EVENT_KEY}`,
  31. SHOWN : `shown${EVENT_KEY}`,
  32. HIDE : `hide${EVENT_KEY}`,
  33. HIDDEN : `hidden${EVENT_KEY}`,
  34. CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`
  35. }
  36. const ClassName = {
  37. SHOW : 'show',
  38. COLLAPSE : 'collapse',
  39. COLLAPSING : 'collapsing',
  40. COLLAPSED : 'collapsed'
  41. }
  42. const Dimension = {
  43. WIDTH : 'width',
  44. HEIGHT : 'height'
  45. }
  46. const Selector = {
  47. ACTIVES : '.show, .collapsing',
  48. DATA_TOGGLE : '[data-toggle="collapse"]'
  49. }
  50. /**
  51. * ------------------------------------------------------------------------
  52. * Class Definition
  53. * ------------------------------------------------------------------------
  54. */
  55. class Collapse {
  56. constructor(element, config) {
  57. this._isTransitioning = false
  58. this._element = element
  59. this._config = this._getConfig(config)
  60. this._triggerArray = $.makeArray(document.querySelectorAll(
  61. `[data-toggle="collapse"][href="#${element.id}"],` +
  62. `[data-toggle="collapse"][data-target="#${element.id}"]`
  63. ))
  64. const toggleList = [].slice.call(document.querySelectorAll(Selector.DATA_TOGGLE))
  65. for (let i = 0, len = toggleList.length; i < len; i++) {
  66. const elem = toggleList[i]
  67. const selector = Util.getSelectorFromElement(elem)
  68. const filterElement = [].slice.call(document.querySelectorAll(selector))
  69. .filter((foundElem) => foundElem === element)
  70. if (selector !== null && filterElement.length > 0) {
  71. this._selector = selector
  72. this._triggerArray.push(elem)
  73. }
  74. }
  75. this._parent = this._config.parent ? this._getParent() : null
  76. if (!this._config.parent) {
  77. this._addAriaAndCollapsedClass(this._element, this._triggerArray)
  78. }
  79. if (this._config.toggle) {
  80. this.toggle()
  81. }
  82. }
  83. // Getters
  84. static get VERSION() {
  85. return VERSION
  86. }
  87. static get Default() {
  88. return Default
  89. }
  90. // Public
  91. toggle() {
  92. if ($(this._element).hasClass(ClassName.SHOW)) {
  93. this.hide()
  94. } else {
  95. this.show()
  96. }
  97. }
  98. show() {
  99. if (this._isTransitioning ||
  100. $(this._element).hasClass(ClassName.SHOW)) {
  101. return
  102. }
  103. let actives
  104. let activesData
  105. if (this._parent) {
  106. actives = [].slice.call(this._parent.querySelectorAll(Selector.ACTIVES))
  107. .filter((elem) => elem.getAttribute('data-parent') === this._config.parent)
  108. if (actives.length === 0) {
  109. actives = null
  110. }
  111. }
  112. if (actives) {
  113. activesData = $(actives).not(this._selector).data(DATA_KEY)
  114. if (activesData && activesData._isTransitioning) {
  115. return
  116. }
  117. }
  118. const startEvent = $.Event(Event.SHOW)
  119. $(this._element).trigger(startEvent)
  120. if (startEvent.isDefaultPrevented()) {
  121. return
  122. }
  123. if (actives) {
  124. Collapse._jQueryInterface.call($(actives).not(this._selector), 'hide')
  125. if (!activesData) {
  126. $(actives).data(DATA_KEY, null)
  127. }
  128. }
  129. const dimension = this._getDimension()
  130. $(this._element)
  131. .removeClass(ClassName.COLLAPSE)
  132. .addClass(ClassName.COLLAPSING)
  133. this._element.style[dimension] = 0
  134. if (this._triggerArray.length) {
  135. $(this._triggerArray)
  136. .removeClass(ClassName.COLLAPSED)
  137. .attr('aria-expanded', true)
  138. }
  139. this.setTransitioning(true)
  140. const complete = () => {
  141. $(this._element)
  142. .removeClass(ClassName.COLLAPSING)
  143. .addClass(ClassName.COLLAPSE)
  144. .addClass(ClassName.SHOW)
  145. this._element.style[dimension] = ''
  146. this.setTransitioning(false)
  147. $(this._element).trigger(Event.SHOWN)
  148. }
  149. const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1)
  150. const scrollSize = `scroll${capitalizedDimension}`
  151. const transitionDuration = Util.getTransitionDurationFromElement(this._element)
  152. $(this._element)
  153. .one(Util.TRANSITION_END, complete)
  154. .emulateTransitionEnd(transitionDuration)
  155. this._element.style[dimension] = `${this._element[scrollSize]}px`
  156. }
  157. hide() {
  158. if (this._isTransitioning ||
  159. !$(this._element).hasClass(ClassName.SHOW)) {
  160. return
  161. }
  162. const startEvent = $.Event(Event.HIDE)
  163. $(this._element).trigger(startEvent)
  164. if (startEvent.isDefaultPrevented()) {
  165. return
  166. }
  167. const dimension = this._getDimension()
  168. this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`
  169. Util.reflow(this._element)
  170. $(this._element)
  171. .addClass(ClassName.COLLAPSING)
  172. .removeClass(ClassName.COLLAPSE)
  173. .removeClass(ClassName.SHOW)
  174. const triggerArrayLength = this._triggerArray.length
  175. if (triggerArrayLength > 0) {
  176. for (let i = 0; i < triggerArrayLength; i++) {
  177. const trigger = this._triggerArray[i]
  178. const selector = Util.getSelectorFromElement(trigger)
  179. if (selector !== null) {
  180. const $elem = $([].slice.call(document.querySelectorAll(selector)))
  181. if (!$elem.hasClass(ClassName.SHOW)) {
  182. $(trigger).addClass(ClassName.COLLAPSED)
  183. .attr('aria-expanded', false)
  184. }
  185. }
  186. }
  187. }
  188. this.setTransitioning(true)
  189. const complete = () => {
  190. this.setTransitioning(false)
  191. $(this._element)
  192. .removeClass(ClassName.COLLAPSING)
  193. .addClass(ClassName.COLLAPSE)
  194. .trigger(Event.HIDDEN)
  195. }
  196. this._element.style[dimension] = ''
  197. const transitionDuration = Util.getTransitionDurationFromElement(this._element)
  198. $(this._element)
  199. .one(Util.TRANSITION_END, complete)
  200. .emulateTransitionEnd(transitionDuration)
  201. }
  202. setTransitioning(isTransitioning) {
  203. this._isTransitioning = isTransitioning
  204. }
  205. dispose() {
  206. $.removeData(this._element, DATA_KEY)
  207. this._config = null
  208. this._parent = null
  209. this._element = null
  210. this._triggerArray = null
  211. this._isTransitioning = null
  212. }
  213. // Private
  214. _getConfig(config) {
  215. config = {
  216. ...Default,
  217. ...config
  218. }
  219. config.toggle = Boolean(config.toggle) // Coerce string values
  220. Util.typeCheckConfig(NAME, config, DefaultType)
  221. return config
  222. }
  223. _getDimension() {
  224. const hasWidth = $(this._element).hasClass(Dimension.WIDTH)
  225. return hasWidth ? Dimension.WIDTH : Dimension.HEIGHT
  226. }
  227. _getParent() {
  228. let parent = null
  229. if (Util.isElement(this._config.parent)) {
  230. parent = this._config.parent
  231. // It's a jQuery object
  232. if (typeof this._config.parent.jquery !== 'undefined') {
  233. parent = this._config.parent[0]
  234. }
  235. } else {
  236. parent = document.querySelector(this._config.parent)
  237. }
  238. const selector =
  239. `[data-toggle="collapse"][data-parent="${this._config.parent}"]`
  240. const children = [].slice.call(parent.querySelectorAll(selector))
  241. $(children).each((i, element) => {
  242. this._addAriaAndCollapsedClass(
  243. Collapse._getTargetFromElement(element),
  244. [element]
  245. )
  246. })
  247. return parent
  248. }
  249. _addAriaAndCollapsedClass(element, triggerArray) {
  250. if (element) {
  251. const isOpen = $(element).hasClass(ClassName.SHOW)
  252. if (triggerArray.length) {
  253. $(triggerArray)
  254. .toggleClass(ClassName.COLLAPSED, !isOpen)
  255. .attr('aria-expanded', isOpen)
  256. }
  257. }
  258. }
  259. // Static
  260. static _getTargetFromElement(element) {
  261. const selector = Util.getSelectorFromElement(element)
  262. return selector ? document.querySelector(selector) : null
  263. }
  264. static _jQueryInterface(config) {
  265. return this.each(function () {
  266. const $this = $(this)
  267. let data = $this.data(DATA_KEY)
  268. const _config = {
  269. ...Default,
  270. ...$this.data(),
  271. ...typeof config === 'object' && config ? config : {}
  272. }
  273. if (!data && _config.toggle && /show|hide/.test(config)) {
  274. _config.toggle = false
  275. }
  276. if (!data) {
  277. data = new Collapse(this, _config)
  278. $this.data(DATA_KEY, data)
  279. }
  280. if (typeof config === 'string') {
  281. if (typeof data[config] === 'undefined') {
  282. throw new TypeError(`No method named "${config}"`)
  283. }
  284. data[config]()
  285. }
  286. })
  287. }
  288. }
  289. /**
  290. * ------------------------------------------------------------------------
  291. * Data Api implementation
  292. * ------------------------------------------------------------------------
  293. */
  294. $(document).on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {
  295. // preventDefault only for <a> elements (which change the URL) not inside the collapsible element
  296. if (event.currentTarget.tagName === 'A') {
  297. event.preventDefault()
  298. }
  299. const $trigger = $(this)
  300. const selector = Util.getSelectorFromElement(this)
  301. const selectors = [].slice.call(document.querySelectorAll(selector))
  302. $(selectors).each(function () {
  303. const $target = $(this)
  304. const data = $target.data(DATA_KEY)
  305. const config = data ? 'toggle' : $trigger.data()
  306. Collapse._jQueryInterface.call($target, config)
  307. })
  308. })
  309. /**
  310. * ------------------------------------------------------------------------
  311. * jQuery
  312. * ------------------------------------------------------------------------
  313. */
  314. $.fn[NAME] = Collapse._jQueryInterface
  315. $.fn[NAME].Constructor = Collapse
  316. $.fn[NAME].noConflict = function () {
  317. $.fn[NAME] = JQUERY_NO_CONFLICT
  318. return Collapse._jQueryInterface
  319. }
  320. return Collapse
  321. })($)
  322. export default Collapse