collapse.js 10.8 KB
Newer Older
1
import $ from 'jquery'
fat's avatar
fat committed
2
3
4
5
import Util from './util'

/**
 * --------------------------------------------------------------------------
Mark Otto's avatar
Mark Otto committed
6
 * Bootstrap (v4.1.3): collapse.js
fat's avatar
fat committed
7
8
9
10
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
 * --------------------------------------------------------------------------
 */

11
const Collapse = (($) => {
fat's avatar
fat committed
12
13
14
15
16
17
18
  /**
   * ------------------------------------------------------------------------
   * Constants
   * ------------------------------------------------------------------------
   */

  const NAME                = 'collapse'
Mark Otto's avatar
Mark Otto committed
19
  const VERSION             = '4.1.3'
fat's avatar
fat committed
20
  const DATA_KEY            = 'bs.collapse'
fat's avatar
fat committed
21
22
  const EVENT_KEY           = `.${DATA_KEY}`
  const DATA_API_KEY        = '.data-api'
fat's avatar
fat committed
23
24
  const JQUERY_NO_CONFLICT  = $.fn[NAME]

25
  const Default = {
fat's avatar
fat committed
26
    toggle : true,
27
    parent : ''
fat's avatar
fat committed
28
29
  }

fat's avatar
fat committed
30
31
  const DefaultType = {
    toggle : 'boolean',
32
    parent : '(string|element)'
fat's avatar
fat committed
33
34
  }

fat's avatar
fat committed
35
  const Event = {
fat's avatar
fat committed
36
37
38
39
40
    SHOW           : `show${EVENT_KEY}`,
    SHOWN          : `shown${EVENT_KEY}`,
    HIDE           : `hide${EVENT_KEY}`,
    HIDDEN         : `hidden${EVENT_KEY}`,
    CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`
fat's avatar
fat committed
41
42
43
  }

  const ClassName = {
Starsam80's avatar
Starsam80 committed
44
    SHOW       : 'show',
fat's avatar
fat committed
45
46
47
48
49
50
51
52
53
54
55
    COLLAPSE   : 'collapse',
    COLLAPSING : 'collapsing',
    COLLAPSED  : 'collapsed'
  }

  const Dimension = {
    WIDTH  : 'width',
    HEIGHT : 'height'
  }

  const Selector = {
56
57
    ACTIVES     : '.show, .collapsing',
    DATA_TOGGLE : '[data-toggle="collapse"]'
fat's avatar
fat committed
58
59
60
61
62
63
64
65
66
67
68
69
  }

  /**
   * ------------------------------------------------------------------------
   * Class Definition
   * ------------------------------------------------------------------------
   */

  class Collapse {
    constructor(element, config) {
      this._isTransitioning = false
      this._element         = element
fat's avatar
fat committed
70
      this._config          = this._getConfig(config)
71
      this._triggerArray    = $.makeArray(document.querySelectorAll(
fat's avatar
fat committed
72
73
74
        `[data-toggle="collapse"][href="#${element.id}"],` +
        `[data-toggle="collapse"][data-target="#${element.id}"]`
      ))
75
76
77
      const toggleList = [].slice.call(document.querySelectorAll(Selector.DATA_TOGGLE))
      for (let i = 0, len = toggleList.length; i < len; i++) {
        const elem = toggleList[i]
78
        const selector = Util.getSelectorFromElement(elem)
79
80
81
82
        const filterElement = [].slice.call(document.querySelectorAll(selector))
          .filter((foundElem) => foundElem === element)

        if (selector !== null && filterElement.length > 0) {
83
          this._selector = selector
84
85
86
          this._triggerArray.push(elem)
        }
      }
87

fat's avatar
fat committed
88
89
90
91
92
93
94
95
96
      this._parent = this._config.parent ? this._getParent() : null

      if (!this._config.parent) {
        this._addAriaAndCollapsedClass(this._element, this._triggerArray)
      }

      if (this._config.toggle) {
        this.toggle()
      }
97
98
    }

XhmikosR's avatar
XhmikosR committed
99
    // Getters
fat's avatar
fat committed
100

101
102
    static get VERSION() {
      return VERSION
fat's avatar
fat committed
103
104
    }

105
106
107
108
    static get Default() {
      return Default
    }

XhmikosR's avatar
XhmikosR committed
109
    // Public
fat's avatar
fat committed
110
111

    toggle() {
Starsam80's avatar
Starsam80 committed
112
      if ($(this._element).hasClass(ClassName.SHOW)) {
fat's avatar
fat committed
113
114
115
116
117
118
119
        this.hide()
      } else {
        this.show()
      }
    }

    show() {
120
121
      if (this._isTransitioning ||
        $(this._element).hasClass(ClassName.SHOW)) {
fat's avatar
fat committed
122
123
124
125
        return
      }

      let actives
fat's avatar
fat committed
126
      let activesData
fat's avatar
fat committed
127
128

      if (this._parent) {
129
        actives = [].slice.call(this._parent.querySelectorAll(Selector.ACTIVES))
130
131
132
133
134
135
136
          .filter((elem) => {
            if (typeof this._config.parent === 'string') {
              return elem.getAttribute('data-parent') === this._config.parent
            }

            return elem.classList.contains(ClassName.COLLAPSE)
          })
137

XhmikosR's avatar
XhmikosR committed
138
        if (actives.length === 0) {
fat's avatar
fat committed
139
140
141
142
143
          actives = null
        }
      }

      if (actives) {
144
        activesData = $(actives).not(this._selector).data(DATA_KEY)
fat's avatar
fat committed
145
146
147
148
149
        if (activesData && activesData._isTransitioning) {
          return
        }
      }

150
      const startEvent = $.Event(Event.SHOW)
fat's avatar
fat committed
151
152
153
154
155
156
      $(this._element).trigger(startEvent)
      if (startEvent.isDefaultPrevented()) {
        return
      }

      if (actives) {
157
        Collapse._jQueryInterface.call($(actives).not(this._selector), 'hide')
fat's avatar
fat committed
158
159
160
161
162
        if (!activesData) {
          $(actives).data(DATA_KEY, null)
        }
      }

163
      const dimension = this._getDimension()
fat's avatar
fat committed
164
165
166
167
168
169
170

      $(this._element)
        .removeClass(ClassName.COLLAPSE)
        .addClass(ClassName.COLLAPSING)

      this._element.style[dimension] = 0

171
      if (this._triggerArray.length) {
fat's avatar
fat committed
172
173
174
175
176
177
178
        $(this._triggerArray)
          .removeClass(ClassName.COLLAPSED)
          .attr('aria-expanded', true)
      }

      this.setTransitioning(true)

179
      const complete = () => {
fat's avatar
fat committed
180
181
182
        $(this._element)
          .removeClass(ClassName.COLLAPSING)
          .addClass(ClassName.COLLAPSE)
Starsam80's avatar
Starsam80 committed
183
          .addClass(ClassName.SHOW)
fat's avatar
fat committed
184
185
186
187
188
189
190
191

        this._element.style[dimension] = ''

        this.setTransitioning(false)

        $(this._element).trigger(Event.SHOWN)
      }

192
      const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1)
XhmikosR's avatar
XhmikosR committed
193
      const scrollSize = `scroll${capitalizedDimension}`
194
      const transitionDuration = Util.getTransitionDurationFromElement(this._element)
fat's avatar
fat committed
195
196
197

      $(this._element)
        .one(Util.TRANSITION_END, complete)
198
        .emulateTransitionEnd(transitionDuration)
fat's avatar
fat committed
199

Jacob Thornton's avatar
Jacob Thornton committed
200
      this._element.style[dimension] = `${this._element[scrollSize]}px`
fat's avatar
fat committed
201
202
203
    }

    hide() {
204
205
      if (this._isTransitioning ||
        !$(this._element).hasClass(ClassName.SHOW)) {
fat's avatar
fat committed
206
207
208
        return
      }

209
      const startEvent = $.Event(Event.HIDE)
fat's avatar
fat committed
210
211
212
213
214
      $(this._element).trigger(startEvent)
      if (startEvent.isDefaultPrevented()) {
        return
      }

XhmikosR's avatar
XhmikosR committed
215
      const dimension = this._getDimension()
fat's avatar
fat committed
216

217
      this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`
fat's avatar
fat committed
218
219
220
221
222
223

      Util.reflow(this._element)

      $(this._element)
        .addClass(ClassName.COLLAPSING)
        .removeClass(ClassName.COLLAPSE)
Starsam80's avatar
Starsam80 committed
224
        .removeClass(ClassName.SHOW)
fat's avatar
fat committed
225

226
227
228
      const triggerArrayLength = this._triggerArray.length
      if (triggerArrayLength > 0) {
        for (let i = 0; i < triggerArrayLength; i++) {
229
230
231
          const trigger = this._triggerArray[i]
          const selector = Util.getSelectorFromElement(trigger)
          if (selector !== null) {
232
            const $elem = $([].slice.call(document.querySelectorAll(selector)))
233
234
            if (!$elem.hasClass(ClassName.SHOW)) {
              $(trigger).addClass(ClassName.COLLAPSED)
XhmikosR's avatar
XhmikosR committed
235
                .attr('aria-expanded', false)
236
237
238
            }
          }
        }
fat's avatar
fat committed
239
240
241
242
      }

      this.setTransitioning(true)

243
      const complete = () => {
fat's avatar
fat committed
244
245
246
247
248
249
250
        this.setTransitioning(false)
        $(this._element)
          .removeClass(ClassName.COLLAPSING)
          .addClass(ClassName.COLLAPSE)
          .trigger(Event.HIDDEN)
      }

251
      this._element.style[dimension] = ''
252
253
      const transitionDuration = Util.getTransitionDurationFromElement(this._element)

fat's avatar
fat committed
254
255
      $(this._element)
        .one(Util.TRANSITION_END, complete)
256
        .emulateTransitionEnd(transitionDuration)
fat's avatar
fat committed
257
258
259
260
261
262
    }

    setTransitioning(isTransitioning) {
      this._isTransitioning = isTransitioning
    }

fat's avatar
fat committed
263
264
265
266
267
268
269
270
271
272
    dispose() {
      $.removeData(this._element, DATA_KEY)

      this._config          = null
      this._parent          = null
      this._element         = null
      this._triggerArray    = null
      this._isTransitioning = null
    }

XhmikosR's avatar
XhmikosR committed
273
    // Private
fat's avatar
fat committed
274

fat's avatar
fat committed
275
    _getConfig(config) {
276
277
278
279
      config = {
        ...Default,
        ...config
      }
XhmikosR's avatar
XhmikosR committed
280
      config.toggle = Boolean(config.toggle) // Coerce string values
fat's avatar
fat committed
281
282
283
284
      Util.typeCheckConfig(NAME, config, DefaultType)
      return config
    }

fat's avatar
fat committed
285
    _getDimension() {
286
      const hasWidth = $(this._element).hasClass(Dimension.WIDTH)
fat's avatar
fat committed
287
288
289
290
      return hasWidth ? Dimension.WIDTH : Dimension.HEIGHT
    }

    _getParent() {
291
292
      let parent

293
294
295
      if (Util.isElement(this._config.parent)) {
        parent = this._config.parent

XhmikosR's avatar
XhmikosR committed
296
        // It's a jQuery object
297
298
299
300
        if (typeof this._config.parent.jquery !== 'undefined') {
          parent = this._config.parent[0]
        }
      } else {
301
        parent = document.querySelector(this._config.parent)
302
303
      }

304
      const selector =
fat's avatar
fat committed
305
306
        `[data-toggle="collapse"][data-parent="${this._config.parent}"]`

307
308
      const children = [].slice.call(parent.querySelectorAll(selector))
      $(children).each((i, element) => {
fat's avatar
fat committed
309
310
311
312
313
314
315
316
317
318
        this._addAriaAndCollapsedClass(
          Collapse._getTargetFromElement(element),
          [element]
        )
      })

      return parent
    }

    _addAriaAndCollapsedClass(element, triggerArray) {
319
      const isOpen = $(element).hasClass(ClassName.SHOW)
fat's avatar
fat committed
320

321
322
323
324
      if (triggerArray.length) {
        $(triggerArray)
          .toggleClass(ClassName.COLLAPSED, !isOpen)
          .attr('aria-expanded', isOpen)
fat's avatar
fat committed
325
326
327
      }
    }

XhmikosR's avatar
XhmikosR committed
328
    // Static
fat's avatar
fat committed
329
330

    static _getTargetFromElement(element) {
331
      const selector = Util.getSelectorFromElement(element)
332
      return selector ? document.querySelector(selector) : null
fat's avatar
fat committed
333
334
335
336
    }

    static _jQueryInterface(config) {
      return this.each(function () {
337
338
        const $this   = $(this)
        let data      = $this.data(DATA_KEY)
339
340
341
        const _config = {
          ...Default,
          ...$this.data(),
342
          ...typeof config === 'object' && config ? config : {}
343
        }
fat's avatar
fat committed
344
345
346
347
348
349
350
351
352
353
354

        if (!data && _config.toggle && /show|hide/.test(config)) {
          _config.toggle = false
        }

        if (!data) {
          data = new Collapse(this, _config)
          $this.data(DATA_KEY, data)
        }

        if (typeof config === 'string') {
XhmikosR's avatar
XhmikosR committed
355
          if (typeof data[config] === 'undefined') {
XhmikosR's avatar
XhmikosR committed
356
            throw new TypeError(`No method named "${config}"`)
357
          }
fat's avatar
fat committed
358
359
360
361
362
363
364
365
366
367
368
369
          data[config]()
        }
      })
    }
  }

  /**
   * ------------------------------------------------------------------------
   * Data Api implementation
   * ------------------------------------------------------------------------
   */

fat's avatar
fat committed
370
  $(document).on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {
371
    // preventDefault only for <a> elements (which change the URL) not inside the collapsible element
372
    if (event.currentTarget.tagName === 'A') {
373
374
      event.preventDefault()
    }
fat's avatar
fat committed
375

376
377
    const $trigger = $(this)
    const selector = Util.getSelectorFromElement(this)
378
379
    const selectors = [].slice.call(document.querySelectorAll(selector))
    $(selectors).each(function () {
380
381
382
383
384
      const $target = $(this)
      const data    = $target.data(DATA_KEY)
      const config  = data ? 'toggle' : $trigger.data()
      Collapse._jQueryInterface.call($target, config)
    })
fat's avatar
fat committed
385
386
387
388
389
390
391
392
  })

  /**
   * ------------------------------------------------------------------------
   * jQuery
   * ------------------------------------------------------------------------
   */

XhmikosR's avatar
XhmikosR committed
393
  $.fn[NAME] = Collapse._jQueryInterface
fat's avatar
fat committed
394
  $.fn[NAME].Constructor = Collapse
XhmikosR's avatar
XhmikosR committed
395
  $.fn[NAME].noConflict = () => {
fat's avatar
fat committed
396
397
398
399
400
    $.fn[NAME] = JQUERY_NO_CONFLICT
    return Collapse._jQueryInterface
  }

  return Collapse
401
})($)
fat's avatar
fat committed
402
403

export default Collapse