collapse.js 11.6 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
import Data from './dom/data'
import EventHandler from './dom/eventHandler'
10
import Manipulator from './dom/manipulator'
Johann-S's avatar
Johann-S committed
11
import SelectorEngine from './dom/selectorEngine'
12
13
import Util from './util'

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

Johann-S's avatar
Johann-S committed
20
const NAME                = 'collapse'
XhmikosR's avatar
XhmikosR committed
21
const VERSION             = '4.3.1'
Johann-S's avatar
Johann-S committed
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
59
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
60

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

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

Johann-S's avatar
Johann-S committed
77
    const toggleList = Util.makeArray(document.querySelectorAll(Selector.DATA_TOGGLE))
Johann-S's avatar
Johann-S committed
78
79
80
    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
81
      const filterElement = Util.makeArray(document.querySelectorAll(selector))
Johann-S's avatar
Johann-S committed
82
83
        .filter((foundElem) => foundElem === element)

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

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

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

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

    Data.setData(element, DATA_KEY, this)
Johann-S's avatar
Johann-S committed
101
  }
102

Johann-S's avatar
Johann-S committed
103
  // Getters
fat's avatar
fat committed
104

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

Johann-S's avatar
Johann-S committed
109
110
111
  static get Default() {
    return Default
  }
112

Johann-S's avatar
Johann-S committed
113
  // Public
fat's avatar
fat committed
114

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

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

Johann-S's avatar
Johann-S committed
129
130
    let actives
    let activesData
131

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

Johann-S's avatar
Johann-S committed
139
140
          return elem.classList.contains(ClassName.COLLAPSE)
        })
fat's avatar
fat committed
141

Johann-S's avatar
Johann-S committed
142
143
      if (actives.length === 0) {
        actives = null
fat's avatar
fat committed
144
      }
Johann-S's avatar
Johann-S committed
145
    }
fat's avatar
fat committed
146

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

Johann-S's avatar
Johann-S committed
152
      if (activesData && activesData._isTransitioning) {
fat's avatar
fat committed
153
154
        return
      }
Johann-S's avatar
Johann-S committed
155
    }
fat's avatar
fat committed
156

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

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

        if (!activesData) {
          Data.setData(elemActive, DATA_KEY, null)
        }
Johann-S's avatar
Johann-S committed
171
      })
Johann-S's avatar
Johann-S committed
172
    }
fat's avatar
fat committed
173

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

Johann-S's avatar
Johann-S committed
176
177
    this._element.classList.remove(ClassName.COLLAPSE)
    this._element.classList.add(ClassName.COLLAPSING)
fat's avatar
fat committed
178

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

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

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

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

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

Johann-S's avatar
Johann-S committed
197
      this.setTransitioning(false)
fat's avatar
fat committed
198

Johann-S's avatar
Johann-S committed
199
      EventHandler.trigger(this._element, Event.SHOWN)
Johann-S's avatar
Johann-S committed
200
    }
fat's avatar
fat committed
201

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

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

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

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

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

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

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

Johann-S's avatar
Johann-S committed
227
    Util.reflow(this._element)
fat's avatar
fat committed
228

Johann-S's avatar
Johann-S committed
229
230
231
    this._element.classList.add(ClassName.COLLAPSING)
    this._element.classList.remove(ClassName.COLLAPSE)
    this._element.classList.remove(ClassName.SHOW)
fat's avatar
fat committed
232

Johann-S's avatar
Johann-S committed
233
234
235
236
237
    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)
238

Johann-S's avatar
Johann-S committed
239
        if (selector !== null) {
Johann-S's avatar
Johann-S committed
240
241
242
243
244
          const elem = SelectorEngine.findOne(selector)

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

Johann-S's avatar
Johann-S committed
250
    this.setTransitioning(true)
251

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

Johann-S's avatar
Johann-S committed
259
260
    this._element.style[dimension] = ''
    const transitionDuration = Util.getTransitionDurationFromElement(this._element)
fat's avatar
fat committed
261

Johann-S's avatar
Johann-S committed
262
263
    EventHandler.one(this._element, Util.TRANSITION_END, complete)
    Util.emulateTransitionEnd(this._element, transitionDuration)
Johann-S's avatar
Johann-S committed
264
  }
fat's avatar
fat committed
265

Johann-S's avatar
Johann-S committed
266
267
268
  setTransitioning(isTransitioning) {
    this._isTransitioning = isTransitioning
  }
fat's avatar
fat committed
269

Johann-S's avatar
Johann-S committed
270
  dispose() {
Johann-S's avatar
Johann-S committed
271
    Data.removeData(this._element, DATA_KEY)
fat's avatar
fat committed
272

Johann-S's avatar
Johann-S committed
273
274
275
276
277
278
279
280
    this._config          = null
    this._parent          = null
    this._element         = null
    this._triggerArray    = null
    this._isTransitioning = null
  }

  // Private
fat's avatar
fat committed
281

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

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

Johann-S's avatar
Johann-S committed
297
298
  _getParent() {
    let parent
299

Johann-S's avatar
Johann-S committed
300
301
302
    if (Util.isElement(this._config.parent)) {
      parent = this._config.parent

Johann-S's avatar
Johann-S committed
303
304
      // 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
305
        parent = this._config.parent[0]
306
      }
Johann-S's avatar
Johann-S committed
307
    } else {
Johann-S's avatar
Johann-S committed
308
      parent = SelectorEngine.findOne(this._config.parent)
Johann-S's avatar
Johann-S committed
309
    }
310

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

314
315
316
317
318
319
320
    Util.makeArray(SelectorEngine.find(selector, parent))
      .forEach((element) => {
        this._addAriaAndCollapsedClass(
          Collapse._getTargetFromElement(element),
          [element]
        )
      })
fat's avatar
fat committed
321

Johann-S's avatar
Johann-S committed
322
323
    return parent
  }
fat's avatar
fat committed
324

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

Johann-S's avatar
Johann-S committed
342
  // Static
fat's avatar
fat committed
343

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

Johann-S's avatar
Johann-S committed
349
350
351
352
  static _collapseInterface(element, config) {
    let data      = Data.getData(element, DATA_KEY)
    const _config = {
      ...Default,
353
      ...Manipulator.getDataAttributes(element),
Johann-S's avatar
Johann-S committed
354
355
      ...typeof config === 'object' && config ? config : {}
    }
fat's avatar
fat committed
356

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

Johann-S's avatar
Johann-S committed
361
362
363
    if (!data) {
      data = new Collapse(element, _config)
    }
fat's avatar
fat committed
364

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

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

  static _getInstance(element) {
    return Data.getData(element, DATA_KEY)
  }
Johann-S's avatar
Johann-S committed
382
}
fat's avatar
fat committed
383

Johann-S's avatar
Johann-S committed
384
385
386
387
388
/**
 * ------------------------------------------------------------------------
 * Data Api implementation
 * ------------------------------------------------------------------------
 */
fat's avatar
fat committed
389

Johann-S's avatar
Johann-S committed
390
EventHandler.on(document, Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {
Johann-S's avatar
Johann-S committed
391
  // preventDefault only for <a> elements (which change the URL) not inside the collapsible element
Johann-S's avatar
Johann-S committed
392
  if (event.target.tagName === 'A') {
Johann-S's avatar
Johann-S committed
393
394
    event.preventDefault()
  }
fat's avatar
fat committed
395

396
  const triggerData      = Manipulator.getDataAttributes(this)
Johann-S's avatar
Johann-S committed
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
  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
    }
413

Johann-S's avatar
Johann-S committed
414
    Collapse._collapseInterface(element, config)
fat's avatar
fat committed
415
  })
Johann-S's avatar
Johann-S committed
416
})
fat's avatar
fat committed
417

Johann-S's avatar
Johann-S committed
418
419
420
421
/**
 * ------------------------------------------------------------------------
 * jQuery
 * ------------------------------------------------------------------------
Johann-S's avatar
Johann-S committed
422
 * add .collapse to jQuery only if jQuery is present
Johann-S's avatar
Johann-S committed
423
 */
fat's avatar
fat committed
424

Johann-S's avatar
Johann-S committed
425
426
427
428
429
430
431
432
433
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
434
}
fat's avatar
fat committed
435
436

export default Collapse