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)
 * --------------------------------------------------------------------------
 */

8
9
10
11
12
13
14
15
16
17
18
import {
  jQuery as $,
  TRANSITION_END,
  emulateTransitionEnd,
  getSelectorFromElement,
  getTransitionDurationFromElement,
  isElement,
  makeArray,
  reflow,
  typeCheckConfig
} from './util/index'
Johann-S's avatar
Johann-S committed
19
20
import Data from './dom/data'
import EventHandler from './dom/eventHandler'
21
import Manipulator from './dom/manipulator'
Johann-S's avatar
Johann-S committed
22
import SelectorEngine from './dom/selectorEngine'
23

Johann-S's avatar
Johann-S committed
24
25
26
27
28
/**
 * ------------------------------------------------------------------------
 * Constants
 * ------------------------------------------------------------------------
 */
fat's avatar
fat committed
29

Johann-S's avatar
Johann-S committed
30
const NAME                = 'collapse'
XhmikosR's avatar
XhmikosR committed
31
const VERSION             = '4.3.1'
Johann-S's avatar
Johann-S committed
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
60
61
62
63
64
65
66
67
68
69
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
70

Johann-S's avatar
Johann-S committed
71
72
73
74
75
/**
 * ------------------------------------------------------------------------
 * Class Definition
 * ------------------------------------------------------------------------
 */
fat's avatar
fat committed
76

Johann-S's avatar
Johann-S committed
77
78
79
80
81
class Collapse {
  constructor(element, config) {
    this._isTransitioning = false
    this._element         = element
    this._config          = this._getConfig(config)
82
    this._triggerArray    = makeArray(SelectorEngine.find(
Johann-S's avatar
Johann-S committed
83
84
85
      `[data-toggle="collapse"][href="#${element.id}"],` +
      `[data-toggle="collapse"][data-target="#${element.id}"]`
    ))
86

87
    const toggleList = makeArray(SelectorEngine.find(Selector.DATA_TOGGLE))
Johann-S's avatar
Johann-S committed
88
89
    for (let i = 0, len = toggleList.length; i < len; i++) {
      const elem = toggleList[i]
90
91
      const selector = getSelectorFromElement(elem)
      const filterElement = makeArray(SelectorEngine.find(selector))
Johann-S's avatar
Johann-S committed
92
93
        .filter((foundElem) => foundElem === element)

Johann-S's avatar
Johann-S committed
94
      if (selector !== null && filterElement.length) {
Johann-S's avatar
Johann-S committed
95
96
        this._selector = selector
        this._triggerArray.push(elem)
97
      }
Johann-S's avatar
Johann-S committed
98
    }
99

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

Johann-S's avatar
Johann-S committed
102
103
104
    if (!this._config.parent) {
      this._addAriaAndCollapsedClass(this._element, this._triggerArray)
    }
fat's avatar
fat committed
105

Johann-S's avatar
Johann-S committed
106
107
    if (this._config.toggle) {
      this.toggle()
108
    }
109
110

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

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

Johann-S's avatar
Johann-S committed
115
116
117
  static get VERSION() {
    return VERSION
  }
fat's avatar
fat committed
118

Johann-S's avatar
Johann-S committed
119
120
121
  static get Default() {
    return Default
  }
122

Johann-S's avatar
Johann-S committed
123
  // Public
fat's avatar
fat committed
124

Johann-S's avatar
Johann-S committed
125
  toggle() {
Johann-S's avatar
Johann-S committed
126
    if (this._element.classList.contains(ClassName.SHOW)) {
Johann-S's avatar
Johann-S committed
127
128
129
      this.hide()
    } else {
      this.show()
fat's avatar
fat committed
130
    }
Johann-S's avatar
Johann-S committed
131
  }
fat's avatar
fat committed
132

Johann-S's avatar
Johann-S committed
133
134
  show() {
    if (this._isTransitioning ||
Johann-S's avatar
Johann-S committed
135
      this._element.classList.contains(ClassName.SHOW)) {
Johann-S's avatar
Johann-S committed
136
137
      return
    }
fat's avatar
fat committed
138

Johann-S's avatar
Johann-S committed
139
140
    let actives
    let activesData
141

Johann-S's avatar
Johann-S committed
142
    if (this._parent) {
143
      actives = makeArray(SelectorEngine.find(Selector.ACTIVES, this._parent))
Johann-S's avatar
Johann-S committed
144
145
146
147
        .filter((elem) => {
          if (typeof this._config.parent === 'string') {
            return elem.getAttribute('data-parent') === this._config.parent
          }
148

Johann-S's avatar
Johann-S committed
149
150
          return elem.classList.contains(ClassName.COLLAPSE)
        })
fat's avatar
fat committed
151

Johann-S's avatar
Johann-S committed
152
153
      if (actives.length === 0) {
        actives = null
fat's avatar
fat committed
154
      }
Johann-S's avatar
Johann-S committed
155
    }
fat's avatar
fat committed
156

157
    const container = SelectorEngine.findOne(this._selector)
Johann-S's avatar
Johann-S committed
158
    if (actives) {
159
      const tempActiveData = actives.filter((elem) => container !== elem)
Johann-S's avatar
Johann-S committed
160
      activesData = tempActiveData[0] ? Data.getData(tempActiveData[0], DATA_KEY) : null
161

Johann-S's avatar
Johann-S committed
162
      if (activesData && activesData._isTransitioning) {
fat's avatar
fat committed
163
164
        return
      }
Johann-S's avatar
Johann-S committed
165
    }
fat's avatar
fat committed
166

Johann-S's avatar
Johann-S committed
167
168
    const startEvent = EventHandler.trigger(this._element, Event.SHOW)
    if (startEvent.defaultPrevented) {
Johann-S's avatar
Johann-S committed
169
170
171
172
      return
    }

    if (actives) {
Johann-S's avatar
Johann-S committed
173
      actives.forEach((elemActive) => {
174
        if (container !== elemActive) {
Johann-S's avatar
Johann-S committed
175
176
          Collapse._collapseInterface(elemActive, 'hide')
        }
177
178
179
180

        if (!activesData) {
          Data.setData(elemActive, DATA_KEY, null)
        }
Johann-S's avatar
Johann-S committed
181
      })
Johann-S's avatar
Johann-S committed
182
    }
fat's avatar
fat committed
183

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

Johann-S's avatar
Johann-S committed
186
187
    this._element.classList.remove(ClassName.COLLAPSE)
    this._element.classList.add(ClassName.COLLAPSING)
fat's avatar
fat committed
188

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

Johann-S's avatar
Johann-S committed
191
    if (this._triggerArray.length) {
Johann-S's avatar
Johann-S committed
192
193
194
195
      this._triggerArray.forEach((element) => {
        element.classList.remove(ClassName.COLLAPSED)
        element.setAttribute('aria-expanded', true)
      })
Johann-S's avatar
Johann-S committed
196
197
198
    }

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

Johann-S's avatar
Johann-S committed
200
    const complete = () => {
Johann-S's avatar
Johann-S committed
201
202
203
      this._element.classList.remove(ClassName.COLLAPSING)
      this._element.classList.add(ClassName.COLLAPSE)
      this._element.classList.add(ClassName.SHOW)
fat's avatar
fat committed
204

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

Johann-S's avatar
Johann-S committed
207
      this.setTransitioning(false)
fat's avatar
fat committed
208

Johann-S's avatar
Johann-S committed
209
      EventHandler.trigger(this._element, Event.SHOWN)
Johann-S's avatar
Johann-S committed
210
    }
fat's avatar
fat committed
211

Johann-S's avatar
Johann-S committed
212
213
    const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1)
    const scrollSize = `scroll${capitalizedDimension}`
214
    const transitionDuration = getTransitionDurationFromElement(this._element)
fat's avatar
fat committed
215

216
    EventHandler.one(this._element, TRANSITION_END, complete)
fat's avatar
fat committed
217

218
    emulateTransitionEnd(this._element, transitionDuration)
Johann-S's avatar
Johann-S committed
219
220
    this._element.style[dimension] = `${this._element[scrollSize]}px`
  }
fat's avatar
fat committed
221

Johann-S's avatar
Johann-S committed
222
223
  hide() {
    if (this._isTransitioning ||
Johann-S's avatar
Johann-S committed
224
      !this._element.classList.contains(ClassName.SHOW)) {
Johann-S's avatar
Johann-S committed
225
      return
fat's avatar
fat committed
226
227
    }

Johann-S's avatar
Johann-S committed
228
229
    const startEvent = EventHandler.trigger(this._element, Event.HIDE)
    if (startEvent.defaultPrevented) {
Johann-S's avatar
Johann-S committed
230
231
      return
    }
fat's avatar
fat committed
232

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

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

237
    reflow(this._element)
fat's avatar
fat committed
238

Johann-S's avatar
Johann-S committed
239
240
241
    this._element.classList.add(ClassName.COLLAPSING)
    this._element.classList.remove(ClassName.COLLAPSE)
    this._element.classList.remove(ClassName.SHOW)
fat's avatar
fat committed
242

Johann-S's avatar
Johann-S committed
243
244
245
246
    const triggerArrayLength = this._triggerArray.length
    if (triggerArrayLength > 0) {
      for (let i = 0; i < triggerArrayLength; i++) {
        const trigger = this._triggerArray[i]
247
        const selector = getSelectorFromElement(trigger)
248

Johann-S's avatar
Johann-S committed
249
        if (selector !== null) {
Johann-S's avatar
Johann-S committed
250
251
252
253
254
          const elem = SelectorEngine.findOne(selector)

          if (!elem.classList.contains(ClassName.SHOW)) {
            trigger.classList.add(ClassName.COLLAPSED)
            trigger.setAttribute('aria-expanded', false)
255
256
          }
        }
fat's avatar
fat committed
257
      }
Johann-S's avatar
Johann-S committed
258
    }
fat's avatar
fat committed
259

Johann-S's avatar
Johann-S committed
260
    this.setTransitioning(true)
261

Johann-S's avatar
Johann-S committed
262
263
    const complete = () => {
      this.setTransitioning(false)
Johann-S's avatar
Johann-S committed
264
265
266
      this._element.classList.remove(ClassName.COLLAPSING)
      this._element.classList.add(ClassName.COLLAPSE)
      EventHandler.trigger(this._element, Event.HIDDEN)
fat's avatar
fat committed
267
268
    }

Johann-S's avatar
Johann-S committed
269
    this._element.style[dimension] = ''
270
    const transitionDuration = getTransitionDurationFromElement(this._element)
fat's avatar
fat committed
271

272
273
    EventHandler.one(this._element, TRANSITION_END, complete)
    emulateTransitionEnd(this._element, transitionDuration)
Johann-S's avatar
Johann-S committed
274
  }
fat's avatar
fat committed
275

Johann-S's avatar
Johann-S committed
276
277
278
  setTransitioning(isTransitioning) {
    this._isTransitioning = isTransitioning
  }
fat's avatar
fat committed
279

Johann-S's avatar
Johann-S committed
280
  dispose() {
Johann-S's avatar
Johann-S committed
281
    Data.removeData(this._element, DATA_KEY)
fat's avatar
fat committed
282

Johann-S's avatar
Johann-S committed
283
284
285
286
287
288
289
290
    this._config          = null
    this._parent          = null
    this._element         = null
    this._triggerArray    = null
    this._isTransitioning = null
  }

  // Private
fat's avatar
fat committed
291

Johann-S's avatar
Johann-S committed
292
293
294
295
  _getConfig(config) {
    config = {
      ...Default,
      ...config
fat's avatar
fat committed
296
    }
Johann-S's avatar
Johann-S committed
297
    config.toggle = Boolean(config.toggle) // Coerce string values
298
    typeCheckConfig(NAME, config, DefaultType)
Johann-S's avatar
Johann-S committed
299
300
    return config
  }
fat's avatar
fat committed
301

Johann-S's avatar
Johann-S committed
302
  _getDimension() {
Johann-S's avatar
Johann-S committed
303
    const hasWidth = this._element.classList.contains(Dimension.WIDTH)
Johann-S's avatar
Johann-S committed
304
305
    return hasWidth ? Dimension.WIDTH : Dimension.HEIGHT
  }
306

Johann-S's avatar
Johann-S committed
307
308
  _getParent() {
    let parent
309

310
    if (isElement(this._config.parent)) {
Johann-S's avatar
Johann-S committed
311
312
      parent = this._config.parent

Johann-S's avatar
Johann-S committed
313
314
      // 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
315
        parent = this._config.parent[0]
316
      }
Johann-S's avatar
Johann-S committed
317
    } else {
Johann-S's avatar
Johann-S committed
318
      parent = SelectorEngine.findOne(this._config.parent)
Johann-S's avatar
Johann-S committed
319
    }
320

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

324
    makeArray(SelectorEngine.find(selector, parent))
325
326
327
328
329
330
      .forEach((element) => {
        this._addAriaAndCollapsedClass(
          Collapse._getTargetFromElement(element),
          [element]
        )
      })
fat's avatar
fat committed
331

Johann-S's avatar
Johann-S committed
332
333
    return parent
  }
fat's avatar
fat committed
334

Johann-S's avatar
Johann-S committed
335
  _addAriaAndCollapsedClass(element, triggerArray) {
Johann-S's avatar
Johann-S committed
336
337
338
339
340
341
342
343
344
345
346
347
348
    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
349
    }
Johann-S's avatar
Johann-S committed
350
  }
fat's avatar
fat committed
351

Johann-S's avatar
Johann-S committed
352
  // Static
fat's avatar
fat committed
353

Johann-S's avatar
Johann-S committed
354
  static _getTargetFromElement(element) {
355
    const selector = getSelectorFromElement(element)
356
    return selector ? SelectorEngine.findOne(selector) : null
Johann-S's avatar
Johann-S committed
357
  }
fat's avatar
fat committed
358

Johann-S's avatar
Johann-S committed
359
360
361
362
  static _collapseInterface(element, config) {
    let data      = Data.getData(element, DATA_KEY)
    const _config = {
      ...Default,
363
      ...Manipulator.getDataAttributes(element),
Johann-S's avatar
Johann-S committed
364
365
      ...typeof config === 'object' && config ? config : {}
    }
fat's avatar
fat committed
366

Johann-S's avatar
Johann-S committed
367
368
369
    if (!data && _config.toggle && /show|hide/.test(config)) {
      _config.toggle = false
    }
fat's avatar
fat committed
370

Johann-S's avatar
Johann-S committed
371
372
373
    if (!data) {
      data = new Collapse(element, _config)
    }
fat's avatar
fat committed
374

Johann-S's avatar
Johann-S committed
375
376
377
    if (typeof config === 'string') {
      if (typeof data[config] === 'undefined') {
        throw new Error(`No method named "${config}"`)
Johann-S's avatar
Johann-S committed
378
      }
Johann-S's avatar
Johann-S committed
379
380
381
382
383
384
385
      data[config]()
    }
  }

  static _jQueryInterface(config) {
    return this.each(function () {
      Collapse._collapseInterface(this, config)
Johann-S's avatar
Johann-S committed
386
    })
fat's avatar
fat committed
387
  }
388
389
390
391

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

Johann-S's avatar
Johann-S committed
394
395
396
397
398
/**
 * ------------------------------------------------------------------------
 * Data Api implementation
 * ------------------------------------------------------------------------
 */
fat's avatar
fat committed
399

Johann-S's avatar
Johann-S committed
400
EventHandler.on(document, Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {
Johann-S's avatar
Johann-S committed
401
  // preventDefault only for <a> elements (which change the URL) not inside the collapsible element
Johann-S's avatar
Johann-S committed
402
  if (event.target.tagName === 'A') {
Johann-S's avatar
Johann-S committed
403
404
    event.preventDefault()
  }
fat's avatar
fat committed
405

406
  const triggerData      = Manipulator.getDataAttributes(this)
407
408
  const selector         = getSelectorFromElement(this)
  const selectorElements = makeArray(SelectorEngine.find(selector))
Johann-S's avatar
Johann-S committed
409
410
411
412
413
414
415
416
417
418
419
420
421
422

  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
    }
423

Johann-S's avatar
Johann-S committed
424
    Collapse._collapseInterface(element, config)
fat's avatar
fat committed
425
  })
Johann-S's avatar
Johann-S committed
426
})
fat's avatar
fat committed
427

Johann-S's avatar
Johann-S committed
428
429
430
431
/**
 * ------------------------------------------------------------------------
 * jQuery
 * ------------------------------------------------------------------------
Johann-S's avatar
Johann-S committed
432
 * add .collapse to jQuery only if jQuery is present
Johann-S's avatar
Johann-S committed
433
 */
fat's avatar
fat committed
434

Johann-S's avatar
Johann-S committed
435
436
437
438
439
440
441
442
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
443
}
fat's avatar
fat committed
444
445

export default Collapse