collapse.js 10.2 KB
Newer Older
fat's avatar
fat committed
1
2
/**
 * --------------------------------------------------------------------------
Mark Otto's avatar
Mark Otto committed
3
 * Bootstrap (v4.3.0): collapse.js
fat's avatar
fat committed
4
5
6
7
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
 * --------------------------------------------------------------------------
 */

8
9
10
import $ from 'jquery'
import Util from './util'

Johann-S's avatar
Johann-S committed
11
12
13
14
15
/**
 * ------------------------------------------------------------------------
 * Constants
 * ------------------------------------------------------------------------
 */
fat's avatar
fat committed
16

Johann-S's avatar
Johann-S committed
17
const NAME                = 'collapse'
Mark Otto's avatar
Mark Otto committed
18
const VERSION             = '4.3.0'
Johann-S's avatar
Johann-S committed
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
const DATA_KEY            = 'bs.collapse'
const EVENT_KEY           = `.${DATA_KEY}`
const DATA_API_KEY        = '.data-api'
const JQUERY_NO_CONFLICT  = $.fn[NAME]

const Default = {
  toggle : true,
  parent : ''
}

const DefaultType = {
  toggle : 'boolean',
  parent : '(string|element)'
}

const Event = {
  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}`
}

const ClassName = {
  SHOW       : 'show',
  COLLAPSE   : 'collapse',
  COLLAPSING : 'collapsing',
  COLLAPSED  : 'collapsed'
}

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

const Selector = {
  ACTIVES     : '.show, .collapsing',
  DATA_TOGGLE : '[data-toggle="collapse"]'
}
fat's avatar
fat committed
58

Johann-S's avatar
Johann-S committed
59
60
61
62
63
/**
 * ------------------------------------------------------------------------
 * Class Definition
 * ------------------------------------------------------------------------
 */
fat's avatar
fat committed
64

Johann-S's avatar
Johann-S committed
65
66
67
68
69
class Collapse {
  constructor(element, config) {
    this._isTransitioning = false
    this._element         = element
    this._config          = this._getConfig(config)
70
    this._triggerArray    = [].slice.call(document.querySelectorAll(
Johann-S's avatar
Johann-S committed
71
72
73
      `[data-toggle="collapse"][href="#${element.id}"],` +
      `[data-toggle="collapse"][data-target="#${element.id}"]`
    ))
74

Johann-S's avatar
Johann-S committed
75
76
77
78
79
80
81
82
83
84
    const toggleList = [].slice.call(document.querySelectorAll(Selector.DATA_TOGGLE))
    for (let i = 0, len = toggleList.length; i < len; i++) {
      const elem = toggleList[i]
      const selector = Util.getSelectorFromElement(elem)
      const filterElement = [].slice.call(document.querySelectorAll(selector))
        .filter((foundElem) => foundElem === element)

      if (selector !== null && filterElement.length > 0) {
        this._selector = selector
        this._triggerArray.push(elem)
85
      }
Johann-S's avatar
Johann-S committed
86
    }
87

Johann-S's avatar
Johann-S committed
88
    this._parent = this._config.parent ? this._getParent() : null
fat's avatar
fat committed
89

Johann-S's avatar
Johann-S committed
90
91
92
    if (!this._config.parent) {
      this._addAriaAndCollapsedClass(this._element, this._triggerArray)
    }
fat's avatar
fat committed
93

Johann-S's avatar
Johann-S committed
94
95
    if (this._config.toggle) {
      this.toggle()
96
    }
Johann-S's avatar
Johann-S committed
97
  }
98

Johann-S's avatar
Johann-S committed
99
  // Getters
fat's avatar
fat committed
100

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

Johann-S's avatar
Johann-S committed
105
106
107
  static get Default() {
    return Default
  }
108

Johann-S's avatar
Johann-S committed
109
  // Public
fat's avatar
fat committed
110

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

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

Johann-S's avatar
Johann-S committed
125
126
    let actives
    let activesData
127

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

Johann-S's avatar
Johann-S committed
135
136
          return elem.classList.contains(ClassName.COLLAPSE)
        })
fat's avatar
fat committed
137

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

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

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

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

Johann-S's avatar
Johann-S committed
163
    const dimension = this._getDimension()
fat's avatar
fat committed
164

Johann-S's avatar
Johann-S committed
165
166
167
    $(this._element)
      .removeClass(ClassName.COLLAPSE)
      .addClass(ClassName.COLLAPSING)
fat's avatar
fat committed
168

Johann-S's avatar
Johann-S committed
169
    this._element.style[dimension] = 0
fat's avatar
fat committed
170

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

    this.setTransitioning(true)
fat's avatar
fat committed
178

Johann-S's avatar
Johann-S committed
179
180
181
182
183
    const complete = () => {
      $(this._element)
        .removeClass(ClassName.COLLAPSING)
        .addClass(ClassName.COLLAPSE)
        .addClass(ClassName.SHOW)
fat's avatar
fat committed
184

Johann-S's avatar
Johann-S committed
185
      this._element.style[dimension] = ''
fat's avatar
fat committed
186

Johann-S's avatar
Johann-S committed
187
      this.setTransitioning(false)
fat's avatar
fat committed
188

Johann-S's avatar
Johann-S committed
189
190
      $(this._element).trigger(Event.SHOWN)
    }
fat's avatar
fat committed
191

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

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

Johann-S's avatar
Johann-S committed
200
201
    this._element.style[dimension] = `${this._element[scrollSize]}px`
  }
fat's avatar
fat committed
202

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

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

Johann-S's avatar
Johann-S committed
215
    const dimension = this._getDimension()
fat's avatar
fat committed
216

Johann-S's avatar
Johann-S committed
217
    this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`
fat's avatar
fat committed
218

Johann-S's avatar
Johann-S committed
219
    Util.reflow(this._element)
fat's avatar
fat committed
220

Johann-S's avatar
Johann-S committed
221
222
223
224
    $(this._element)
      .addClass(ClassName.COLLAPSING)
      .removeClass(ClassName.COLLAPSE)
      .removeClass(ClassName.SHOW)
fat's avatar
fat committed
225

Johann-S's avatar
Johann-S committed
226
227
228
229
230
    const triggerArrayLength = this._triggerArray.length
    if (triggerArrayLength > 0) {
      for (let i = 0; i < triggerArrayLength; i++) {
        const trigger = this._triggerArray[i]
        const selector = Util.getSelectorFromElement(trigger)
231

Johann-S's avatar
Johann-S committed
232
233
234
235
236
        if (selector !== null) {
          const $elem = $([].slice.call(document.querySelectorAll(selector)))
          if (!$elem.hasClass(ClassName.SHOW)) {
            $(trigger).addClass(ClassName.COLLAPSED)
              .attr('aria-expanded', false)
237
238
          }
        }
fat's avatar
fat committed
239
      }
Johann-S's avatar
Johann-S committed
240
    }
fat's avatar
fat committed
241

Johann-S's avatar
Johann-S committed
242
    this.setTransitioning(true)
243

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

Johann-S's avatar
Johann-S committed
252
253
    this._element.style[dimension] = ''
    const transitionDuration = Util.getTransitionDurationFromElement(this._element)
fat's avatar
fat committed
254

Johann-S's avatar
Johann-S committed
255
256
257
258
    $(this._element)
      .one(Util.TRANSITION_END, complete)
      .emulateTransitionEnd(transitionDuration)
  }
fat's avatar
fat committed
259

Johann-S's avatar
Johann-S committed
260
261
262
  setTransitioning(isTransitioning) {
    this._isTransitioning = isTransitioning
  }
fat's avatar
fat committed
263

Johann-S's avatar
Johann-S committed
264
265
  dispose() {
    $.removeData(this._element, DATA_KEY)
fat's avatar
fat committed
266

Johann-S's avatar
Johann-S committed
267
268
269
270
271
272
273
274
    this._config          = null
    this._parent          = null
    this._element         = null
    this._triggerArray    = null
    this._isTransitioning = null
  }

  // Private
fat's avatar
fat committed
275

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

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

Johann-S's avatar
Johann-S committed
291
292
  _getParent() {
    let parent
293

Johann-S's avatar
Johann-S committed
294
295
296
297
298
299
    if (Util.isElement(this._config.parent)) {
      parent = this._config.parent

      // It's a jQuery object
      if (typeof this._config.parent.jquery !== 'undefined') {
        parent = this._config.parent[0]
300
      }
Johann-S's avatar
Johann-S committed
301
302
303
    } else {
      parent = document.querySelector(this._config.parent)
    }
304

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

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

Johann-S's avatar
Johann-S committed
316
317
    return parent
  }
fat's avatar
fat committed
318

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

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

Johann-S's avatar
Johann-S committed
329
  // Static
fat's avatar
fat committed
330

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

Johann-S's avatar
Johann-S committed
336
337
338
339
340
341
342
343
344
  static _jQueryInterface(config) {
    return this.each(function () {
      const $this   = $(this)
      let data      = $this.data(DATA_KEY)
      const _config = {
        ...Default,
        ...$this.data(),
        ...typeof config === 'object' && config ? config : {}
      }
fat's avatar
fat committed
345

Johann-S's avatar
Johann-S committed
346
347
348
      if (!data && _config.toggle && /show|hide/.test(config)) {
        _config.toggle = false
      }
fat's avatar
fat committed
349

Johann-S's avatar
Johann-S committed
350
351
352
353
      if (!data) {
        data = new Collapse(this, _config)
        $this.data(DATA_KEY, data)
      }
fat's avatar
fat committed
354

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

Johann-S's avatar
Johann-S committed
365
366
367
368
369
/**
 * ------------------------------------------------------------------------
 * Data Api implementation
 * ------------------------------------------------------------------------
 */
fat's avatar
fat committed
370

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

Johann-S's avatar
Johann-S committed
377
378
379
  const $trigger = $(this)
  const selector = Util.getSelectorFromElement(this)
  const selectors = [].slice.call(document.querySelectorAll(selector))
380

Johann-S's avatar
Johann-S committed
381
382
383
384
385
  $(selectors).each(function () {
    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
386
  })
Johann-S's avatar
Johann-S committed
387
})
fat's avatar
fat committed
388

Johann-S's avatar
Johann-S committed
389
390
391
392
393
/**
 * ------------------------------------------------------------------------
 * jQuery
 * ------------------------------------------------------------------------
 */
fat's avatar
fat committed
394

Johann-S's avatar
Johann-S committed
395
396
397
398
399
400
$.fn[NAME] = Collapse._jQueryInterface
$.fn[NAME].Constructor = Collapse
$.fn[NAME].noConflict = () => {
  $.fn[NAME] = JQUERY_NO_CONFLICT
  return Collapse._jQueryInterface
}
fat's avatar
fat committed
401
402

export default Collapse