collapse.js 11.4 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

144
    const container = SelectorEngine.findOne(this._selector)
Johann-S's avatar
Johann-S committed
145
    if (actives) {
146
      const tempActiveData = actives.filter((elem) => container !== elem)
Johann-S's avatar
Johann-S committed
147
      activesData = tempActiveData[0] ? Data.getData(tempActiveData[0], DATA_KEY) : null
148

Johann-S's avatar
Johann-S committed
149
      if (activesData && activesData._isTransitioning) {
fat's avatar
fat committed
150
151
        return
      }
Johann-S's avatar
Johann-S committed
152
    }
fat's avatar
fat committed
153

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

    if (actives) {
Johann-S's avatar
Johann-S committed
160
      actives.forEach((elemActive) => {
161
        if (container !== elemActive) {
Johann-S's avatar
Johann-S committed
162
163
          Collapse._collapseInterface(elemActive, 'hide')
        }
164
165
166
167

        if (!activesData) {
          Data.setData(elemActive, DATA_KEY, null)
        }
Johann-S's avatar
Johann-S committed
168
      })
Johann-S's avatar
Johann-S committed
169
    }
fat's avatar
fat committed
170

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

Johann-S's avatar
Johann-S committed
173
174
    this._element.classList.remove(ClassName.COLLAPSE)
    this._element.classList.add(ClassName.COLLAPSING)
fat's avatar
fat committed
175

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

Johann-S's avatar
Johann-S committed
178
    if (this._triggerArray.length) {
Johann-S's avatar
Johann-S committed
179
180
181
182
      this._triggerArray.forEach((element) => {
        element.classList.remove(ClassName.COLLAPSED)
        element.setAttribute('aria-expanded', true)
      })
Johann-S's avatar
Johann-S committed
183
184
185
    }

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

Johann-S's avatar
Johann-S committed
187
    const complete = () => {
Johann-S's avatar
Johann-S committed
188
189
190
      this._element.classList.remove(ClassName.COLLAPSING)
      this._element.classList.add(ClassName.COLLAPSE)
      this._element.classList.add(ClassName.SHOW)
fat's avatar
fat committed
191

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

Johann-S's avatar
Johann-S committed
194
      this.setTransitioning(false)
fat's avatar
fat committed
195

Johann-S's avatar
Johann-S committed
196
      EventHandler.trigger(this._element, Event.SHOWN)
Johann-S's avatar
Johann-S committed
197
    }
fat's avatar
fat committed
198

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

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

Johann-S's avatar
Johann-S committed
205
    Util.emulateTransitionEnd(this._element, transitionDuration)
Johann-S's avatar
Johann-S committed
206
207
    this._element.style[dimension] = `${this._element[scrollSize]}px`
  }
fat's avatar
fat committed
208

Johann-S's avatar
Johann-S committed
209
210
  hide() {
    if (this._isTransitioning ||
Johann-S's avatar
Johann-S committed
211
      !this._element.classList.contains(ClassName.SHOW)) {
Johann-S's avatar
Johann-S committed
212
      return
fat's avatar
fat committed
213
214
    }

Johann-S's avatar
Johann-S committed
215
216
    const startEvent = EventHandler.trigger(this._element, Event.HIDE)
    if (startEvent.defaultPrevented) {
Johann-S's avatar
Johann-S committed
217
218
      return
    }
fat's avatar
fat committed
219

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

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

Johann-S's avatar
Johann-S committed
224
    Util.reflow(this._element)
fat's avatar
fat committed
225

Johann-S's avatar
Johann-S committed
226
227
228
    this._element.classList.add(ClassName.COLLAPSING)
    this._element.classList.remove(ClassName.COLLAPSE)
    this._element.classList.remove(ClassName.SHOW)
fat's avatar
fat committed
229

Johann-S's avatar
Johann-S committed
230
231
232
233
234
    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)
235

Johann-S's avatar
Johann-S committed
236
        if (selector !== null) {
Johann-S's avatar
Johann-S committed
237
238
239
240
241
          const elem = SelectorEngine.findOne(selector)

          if (!elem.classList.contains(ClassName.SHOW)) {
            trigger.classList.add(ClassName.COLLAPSED)
            trigger.setAttribute('aria-expanded', false)
242
243
          }
        }
fat's avatar
fat committed
244
      }
Johann-S's avatar
Johann-S committed
245
    }
fat's avatar
fat committed
246

Johann-S's avatar
Johann-S committed
247
    this.setTransitioning(true)
248

Johann-S's avatar
Johann-S committed
249
250
    const complete = () => {
      this.setTransitioning(false)
Johann-S's avatar
Johann-S committed
251
252
253
      this._element.classList.remove(ClassName.COLLAPSING)
      this._element.classList.add(ClassName.COLLAPSE)
      EventHandler.trigger(this._element, Event.HIDDEN)
fat's avatar
fat committed
254
255
    }

Johann-S's avatar
Johann-S committed
256
257
    this._element.style[dimension] = ''
    const transitionDuration = Util.getTransitionDurationFromElement(this._element)
fat's avatar
fat committed
258

Johann-S's avatar
Johann-S committed
259
260
    EventHandler.one(this._element, Util.TRANSITION_END, complete)
    Util.emulateTransitionEnd(this._element, transitionDuration)
Johann-S's avatar
Johann-S committed
261
  }
fat's avatar
fat committed
262

Johann-S's avatar
Johann-S committed
263
264
265
  setTransitioning(isTransitioning) {
    this._isTransitioning = isTransitioning
  }
fat's avatar
fat committed
266

Johann-S's avatar
Johann-S committed
267
  dispose() {
Johann-S's avatar
Johann-S committed
268
    Data.removeData(this._element, DATA_KEY)
fat's avatar
fat committed
269

Johann-S's avatar
Johann-S committed
270
271
272
273
274
275
276
277
    this._config          = null
    this._parent          = null
    this._element         = null
    this._triggerArray    = null
    this._isTransitioning = null
  }

  // Private
fat's avatar
fat committed
278

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

Johann-S's avatar
Johann-S committed
289
  _getDimension() {
Johann-S's avatar
Johann-S committed
290
    const hasWidth = this._element.classList.contains(Dimension.WIDTH)
Johann-S's avatar
Johann-S committed
291
292
    return hasWidth ? Dimension.WIDTH : Dimension.HEIGHT
  }
293

Johann-S's avatar
Johann-S committed
294
295
  _getParent() {
    let parent
296

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

Johann-S's avatar
Johann-S committed
300
301
      // it's a jQuery object
      if (typeof this._config.parent.jquery !== 'undefined' || typeof this._config.parent[0] !== 'undefined') {
Johann-S's avatar
Johann-S committed
302
        parent = this._config.parent[0]
303
      }
Johann-S's avatar
Johann-S committed
304
    } else {
Johann-S's avatar
Johann-S committed
305
      parent = SelectorEngine.findOne(this._config.parent)
Johann-S's avatar
Johann-S committed
306
    }
307

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

Johann-S's avatar
Johann-S committed
311
312
    const elements = Util.makeArray(SelectorEngine.find(selector, parent))
    elements.forEach((element) => {
Johann-S's avatar
Johann-S committed
313
314
315
316
317
      this._addAriaAndCollapsedClass(
        Collapse._getTargetFromElement(element),
        [element]
      )
    })
fat's avatar
fat committed
318

Johann-S's avatar
Johann-S committed
319
320
    return parent
  }
fat's avatar
fat committed
321

Johann-S's avatar
Johann-S committed
322
  _addAriaAndCollapsedClass(element, triggerArray) {
Johann-S's avatar
Johann-S committed
323
324
325
326
327
328
329
330
331
332
333
334
335
    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
336
    }
Johann-S's avatar
Johann-S committed
337
  }
fat's avatar
fat committed
338

Johann-S's avatar
Johann-S committed
339
  // Static
fat's avatar
fat committed
340

Johann-S's avatar
Johann-S committed
341
342
343
344
  static _getTargetFromElement(element) {
    const selector = Util.getSelectorFromElement(element)
    return selector ? document.querySelector(selector) : null
  }
fat's avatar
fat committed
345

Johann-S's avatar
Johann-S committed
346
347
348
349
350
351
352
  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
353

Johann-S's avatar
Johann-S committed
354
355
356
    if (!data && _config.toggle && /show|hide/.test(config)) {
      _config.toggle = false
    }
fat's avatar
fat committed
357

Johann-S's avatar
Johann-S committed
358
359
360
361
    if (!data) {
      data = new Collapse(element, _config)
      Data.setData(element, DATA_KEY, data)
    }
fat's avatar
fat committed
362

Johann-S's avatar
Johann-S committed
363
364
365
    if (typeof config === 'string') {
      if (typeof data[config] === 'undefined') {
        throw new Error(`No method named "${config}"`)
Johann-S's avatar
Johann-S committed
366
      }
Johann-S's avatar
Johann-S committed
367
368
369
370
371
372
373
      data[config]()
    }
  }

  static _jQueryInterface(config) {
    return this.each(function () {
      Collapse._collapseInterface(this, config)
Johann-S's avatar
Johann-S committed
374
    })
fat's avatar
fat committed
375
  }
Johann-S's avatar
Johann-S committed
376
}
fat's avatar
fat committed
377

Johann-S's avatar
Johann-S committed
378
379
380
381
382
/**
 * ------------------------------------------------------------------------
 * Data Api implementation
 * ------------------------------------------------------------------------
 */
fat's avatar
fat committed
383

Johann-S's avatar
Johann-S committed
384
EventHandler.on(document, Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {
Johann-S's avatar
Johann-S committed
385
  // preventDefault only for <a> elements (which change the URL) not inside the collapsible element
Johann-S's avatar
Johann-S committed
386
  if (event.target.tagName === 'A') {
Johann-S's avatar
Johann-S committed
387
388
    event.preventDefault()
  }
fat's avatar
fat committed
389

Johann-S's avatar
Johann-S committed
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
  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
    }
407

Johann-S's avatar
Johann-S committed
408
    Collapse._collapseInterface(element, config)
fat's avatar
fat committed
409
  })
Johann-S's avatar
Johann-S committed
410
})
fat's avatar
fat committed
411

Johann-S's avatar
Johann-S committed
412
413
414
415
/**
 * ------------------------------------------------------------------------
 * jQuery
 * ------------------------------------------------------------------------
Johann-S's avatar
Johann-S committed
416
 * add .collapse to jQuery only if jQuery is present
Johann-S's avatar
Johann-S committed
417
 */
fat's avatar
fat committed
418

Johann-S's avatar
Johann-S committed
419
420
421
422
423
424
425
426
427
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
428
}
fat's avatar
fat committed
429
430

export default Collapse