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

Johann-S's avatar
Johann-S committed
8
9
10
import Data from './dom/data'
import EventHandler from './dom/eventHandler'
import SelectorEngine from './dom/selectorEngine'
11
12
import Util from './util'

Johann-S's avatar
Johann-S committed
13
14
15
16
17
/**
 * ------------------------------------------------------------------------
 * Constants
 * ------------------------------------------------------------------------
 */
fat's avatar
fat committed
18

Johann-S's avatar
Johann-S committed
19
const NAME                = 'collapse'
XhmikosR's avatar
XhmikosR committed
20
const VERSION             = '4.3.1'
Johann-S's avatar
Johann-S committed
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
58
const DATA_KEY            = 'bs.collapse'
const EVENT_KEY           = `.${DATA_KEY}`
const DATA_API_KEY        = '.data-api'

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
59

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Johann-S's avatar
Johann-S committed
144
    if (actives) {
Johann-S's avatar
Johann-S committed
145
      activesData = Data.getData(actives[0], DATA_KEY)
Johann-S's avatar
Johann-S committed
146
      if (activesData && activesData._isTransitioning) {
fat's avatar
fat committed
147
148
        return
      }
Johann-S's avatar
Johann-S committed
149
    }
fat's avatar
fat committed
150

Johann-S's avatar
Johann-S committed
151
152
    const startEvent = EventHandler.trigger(this._element, Event.SHOW)
    if (startEvent.defaultPrevented) {
Johann-S's avatar
Johann-S committed
153
154
155
156
      return
    }

    if (actives) {
Johann-S's avatar
Johann-S committed
157
      actives.forEach((elemActive) => Collapse._collapseInterface(elemActive, 'hide'))
Johann-S's avatar
Johann-S committed
158
      if (!activesData) {
Johann-S's avatar
Johann-S committed
159
        Data.setData(actives[0], 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
    this._element.classList.remove(ClassName.COLLAPSE)
    this._element.classList.add(ClassName.COLLAPSING)
fat's avatar
fat committed
167

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

Johann-S's avatar
Johann-S committed
170
    if (this._triggerArray.length) {
Johann-S's avatar
Johann-S committed
171
172
173
174
      this._triggerArray.forEach((element) => {
        element.classList.remove(ClassName.COLLAPSED)
        element.setAttribute('aria-expanded', true)
      })
Johann-S's avatar
Johann-S committed
175
176
177
    }

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

Johann-S's avatar
Johann-S committed
179
    const complete = () => {
Johann-S's avatar
Johann-S committed
180
181
182
      this._element.classList.remove(ClassName.COLLAPSING)
      this._element.classList.add(ClassName.COLLAPSE)
      this._element.classList.add(ClassName.SHOW)
fat's avatar
fat committed
183

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

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

Johann-S's avatar
Johann-S committed
188
      EventHandler.trigger(this._element, Event.SHOWN)
Johann-S's avatar
Johann-S committed
189
    }
fat's avatar
fat committed
190

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

Johann-S's avatar
Johann-S committed
195
    EventHandler.one(this._element, Util.TRANSITION_END, complete)
fat's avatar
fat committed
196

Johann-S's avatar
Johann-S committed
197
    Util.emulateTransitionEnd(this._element, transitionDuration)
Johann-S's avatar
Johann-S committed
198
199
    this._element.style[dimension] = `${this._element[scrollSize]}px`
  }
fat's avatar
fat committed
200

Johann-S's avatar
Johann-S committed
201
202
  hide() {
    if (this._isTransitioning ||
Johann-S's avatar
Johann-S committed
203
      !this._element.classList.contains(ClassName.SHOW)) {
Johann-S's avatar
Johann-S committed
204
      return
fat's avatar
fat committed
205
206
    }

Johann-S's avatar
Johann-S committed
207
208
    const startEvent = EventHandler.trigger(this._element, Event.HIDE)
    if (startEvent.defaultPrevented) {
Johann-S's avatar
Johann-S committed
209
210
      return
    }
fat's avatar
fat committed
211

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

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

Johann-S's avatar
Johann-S committed
216
    Util.reflow(this._element)
fat's avatar
fat committed
217

Johann-S's avatar
Johann-S committed
218
219
220
    this._element.classList.add(ClassName.COLLAPSING)
    this._element.classList.remove(ClassName.COLLAPSE)
    this._element.classList.remove(ClassName.SHOW)
fat's avatar
fat committed
221

Johann-S's avatar
Johann-S committed
222
223
224
225
226
    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)
227

Johann-S's avatar
Johann-S committed
228
        if (selector !== null) {
Johann-S's avatar
Johann-S committed
229
230
231
232
233
          const elem = SelectorEngine.findOne(selector)

          if (!elem.classList.contains(ClassName.SHOW)) {
            trigger.classList.add(ClassName.COLLAPSED)
            trigger.setAttribute('aria-expanded', false)
234
235
          }
        }
fat's avatar
fat committed
236
      }
Johann-S's avatar
Johann-S committed
237
    }
fat's avatar
fat committed
238

Johann-S's avatar
Johann-S committed
239
    this.setTransitioning(true)
240

Johann-S's avatar
Johann-S committed
241
242
    const complete = () => {
      this.setTransitioning(false)
Johann-S's avatar
Johann-S committed
243
244
245
      this._element.classList.remove(ClassName.COLLAPSING)
      this._element.classList.add(ClassName.COLLAPSE)
      EventHandler.trigger(this._element, Event.HIDDEN)
fat's avatar
fat committed
246
247
    }

Johann-S's avatar
Johann-S committed
248
249
    this._element.style[dimension] = ''
    const transitionDuration = Util.getTransitionDurationFromElement(this._element)
fat's avatar
fat committed
250

Johann-S's avatar
Johann-S committed
251
252
    EventHandler.one(this._element, Util.TRANSITION_END, complete)
    Util.emulateTransitionEnd(this._element, transitionDuration)
Johann-S's avatar
Johann-S committed
253
  }
fat's avatar
fat committed
254

Johann-S's avatar
Johann-S committed
255
256
257
  setTransitioning(isTransitioning) {
    this._isTransitioning = isTransitioning
  }
fat's avatar
fat committed
258

Johann-S's avatar
Johann-S committed
259
  dispose() {
Johann-S's avatar
Johann-S committed
260
    Data.removeData(this._element, DATA_KEY)
fat's avatar
fat committed
261

Johann-S's avatar
Johann-S committed
262
263
264
265
266
267
268
269
    this._config          = null
    this._parent          = null
    this._element         = null
    this._triggerArray    = null
    this._isTransitioning = null
  }

  // Private
fat's avatar
fat committed
270

Johann-S's avatar
Johann-S committed
271
272
273
274
  _getConfig(config) {
    config = {
      ...Default,
      ...config
fat's avatar
fat committed
275
    }
Johann-S's avatar
Johann-S committed
276
277
278
279
    config.toggle = Boolean(config.toggle) // Coerce string values
    Util.typeCheckConfig(NAME, config, DefaultType)
    return config
  }
fat's avatar
fat committed
280

Johann-S's avatar
Johann-S committed
281
  _getDimension() {
Johann-S's avatar
Johann-S committed
282
    const hasWidth = this._element.classList.contains(Dimension.WIDTH)
Johann-S's avatar
Johann-S committed
283
284
    return hasWidth ? Dimension.WIDTH : Dimension.HEIGHT
  }
285

Johann-S's avatar
Johann-S committed
286
287
  _getParent() {
    let parent
288

Johann-S's avatar
Johann-S committed
289
290
291
292
293
294
    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]
295
      }
Johann-S's avatar
Johann-S committed
296
    } else {
Johann-S's avatar
Johann-S committed
297
      parent = SelectorEngine.findOne(this._config.parent)
Johann-S's avatar
Johann-S committed
298
    }
299

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

Johann-S's avatar
Johann-S committed
303
304
    const elements = Util.makeArray(SelectorEngine.find(selector, parent))
    elements.forEach((element) => {
Johann-S's avatar
Johann-S committed
305
306
307
308
309
      this._addAriaAndCollapsedClass(
        Collapse._getTargetFromElement(element),
        [element]
      )
    })
fat's avatar
fat committed
310

Johann-S's avatar
Johann-S committed
311
312
    return parent
  }
fat's avatar
fat committed
313

Johann-S's avatar
Johann-S committed
314
  _addAriaAndCollapsedClass(element, triggerArray) {
Johann-S's avatar
Johann-S committed
315
316
317
318
319
320
321
322
323
324
325
326
327
    if (element) {
      const isOpen = element.classList.contains(ClassName.SHOW)

      if (triggerArray.length) {
        triggerArray.forEach((elem) => {
          if (!isOpen) {
            elem.classList.add(ClassName.COLLAPSED)
          } else {
            elem.classList.remove(ClassName.COLLAPSED)
          }
          elem.setAttribute('aria-expanded', isOpen)
        })
      }
fat's avatar
fat committed
328
    }
Johann-S's avatar
Johann-S committed
329
  }
fat's avatar
fat committed
330

Johann-S's avatar
Johann-S committed
331
  // Static
fat's avatar
fat committed
332

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

Johann-S's avatar
Johann-S committed
338
339
340
341
342
343
344
  static _collapseInterface(element, config) {
    let data      = Data.getData(element, DATA_KEY)
    const _config = {
      ...Default,
      ...Util.getDataAttributes(element),
      ...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(element, _config)
      Data.setData(element, 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 Error(`No method named "${config}"`)
Johann-S's avatar
Johann-S committed
358
      }
Johann-S's avatar
Johann-S committed
359
360
361
362
363
364
365
      data[config]()
    }
  }

  static _jQueryInterface(config) {
    return this.each(function () {
      Collapse._collapseInterface(this, config)
Johann-S's avatar
Johann-S committed
366
    })
fat's avatar
fat committed
367
  }
Johann-S's avatar
Johann-S committed
368
}
fat's avatar
fat committed
369

Johann-S's avatar
Johann-S committed
370
371
372
373
374
/**
 * ------------------------------------------------------------------------
 * Data Api implementation
 * ------------------------------------------------------------------------
 */
fat's avatar
fat committed
375

Johann-S's avatar
Johann-S committed
376
EventHandler.on(document, Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {
Johann-S's avatar
Johann-S committed
377
  // preventDefault only for <a> elements (which change the URL) not inside the collapsible element
Johann-S's avatar
Johann-S committed
378
  if (event.target.tagName === 'A') {
Johann-S's avatar
Johann-S committed
379
380
    event.preventDefault()
  }
fat's avatar
fat committed
381

Johann-S's avatar
Johann-S committed
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
  const triggerData      = Util.getDataAttributes(this)
  const selector         = Util.getSelectorFromElement(this)
  const selectorElements = Util.makeArray(SelectorEngine.find(selector))

  selectorElements.forEach((element) => {
    const data = Data.getData(element, DATA_KEY)
    let config
    if (data) {
      // update parent attribute
      if (data._parent === null && typeof triggerData.parent === 'string') {
        data._config.parent = triggerData.parent
        data._parent = data._getParent()
      }
      config = 'toggle'
    } else {
      config = triggerData
    }
399

Johann-S's avatar
Johann-S committed
400
    Collapse._collapseInterface(element, config)
fat's avatar
fat committed
401
  })
Johann-S's avatar
Johann-S committed
402
})
fat's avatar
fat committed
403

Johann-S's avatar
Johann-S committed
404
405
406
407
/**
 * ------------------------------------------------------------------------
 * jQuery
 * ------------------------------------------------------------------------
Johann-S's avatar
Johann-S committed
408
 * add .collapse to jQuery only if jQuery is present
Johann-S's avatar
Johann-S committed
409
 */
fat's avatar
fat committed
410

Johann-S's avatar
Johann-S committed
411
412
413
414
415
416
417
418
419
const $ = Util.jQuery
if (typeof $ !== 'undefined') {
  const JQUERY_NO_CONFLICT  = $.fn[NAME]
  $.fn[NAME]                = Collapse._jQueryInterface
  $.fn[NAME].Constructor    = Collapse
  $.fn[NAME].noConflict     = () => {
    $.fn[NAME] = JQUERY_NO_CONFLICT
    return Collapse._jQueryInterface
  }
Johann-S's avatar
Johann-S committed
420
}
fat's avatar
fat committed
421
422

export default Collapse