collapse.js 10.2 KB
Newer Older
1
import $ from 'jquery'
fat's avatar
fat committed
2
3
4
5
import Util from './util'

/**
 * --------------------------------------------------------------------------
Mark Otto's avatar
Mark Otto committed
6
 * Bootstrap (v4.1.3): collapse.js
fat's avatar
fat committed
7
8
9
10
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
 * --------------------------------------------------------------------------
 */

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
18
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 NAME                = 'collapse'
const VERSION             = '4.1.3'
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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
class Collapse {
  constructor(element, config) {
    this._isTransitioning = false
    this._element         = element
    this._config          = this._getConfig(config)
    this._triggerArray    = $.makeArray(document.querySelectorAll(
      `[data-toggle="collapse"][href="#${element.id}"],` +
      `[data-toggle="collapse"][data-target="#${element.id}"]`
    ))
    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)
84
      }
Johann-S's avatar
Johann-S committed
85
    }
86

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

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

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

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

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

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

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

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

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

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

Johann-S's avatar
Johann-S committed
127
128
129
130
131
132
    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
          }
133

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

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

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

Johann-S's avatar
Johann-S committed
149
150
151
152
153
154
155
156
157
158
    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
159
      }
Johann-S's avatar
Johann-S committed
160
    }
fat's avatar
fat committed
161

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

Johann-S's avatar
Johann-S committed
164
165
166
    $(this._element)
      .removeClass(ClassName.COLLAPSE)
      .addClass(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
171
172
173
174
175
176
    if (this._triggerArray.length) {
      $(this._triggerArray)
        .removeClass(ClassName.COLLAPSED)
        .attr('aria-expanded', true)
    }

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

Johann-S's avatar
Johann-S committed
178
179
180
181
182
    const complete = () => {
      $(this._element)
        .removeClass(ClassName.COLLAPSING)
        .addClass(ClassName.COLLAPSE)
        .addClass(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
189
      $(this._element).trigger(Event.SHOWN)
    }
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
196
197
    $(this._element)
      .one(Util.TRANSITION_END, complete)
      .emulateTransitionEnd(transitionDuration)
fat's avatar
fat committed
198

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

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

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

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

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

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

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

Johann-S's avatar
Johann-S committed
225
226
227
228
229
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)
        if (selector !== null) {
          const $elem = $([].slice.call(document.querySelectorAll(selector)))
          if (!$elem.hasClass(ClassName.SHOW)) {
            $(trigger).addClass(ClassName.COLLAPSED)
              .attr('aria-expanded', false)
235
236
          }
        }
fat's avatar
fat committed
237
      }
Johann-S's avatar
Johann-S committed
238
    }
fat's avatar
fat committed
239

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

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

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

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

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

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

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

  // Private
fat's avatar
fat committed
273

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

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

Johann-S's avatar
Johann-S committed
289
290
  _getParent() {
    let parent
291

Johann-S's avatar
Johann-S committed
292
293
294
295
296
297
    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]
298
      }
Johann-S's avatar
Johann-S committed
299
300
301
    } else {
      parent = document.querySelector(this._config.parent)
    }
302

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

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

Johann-S's avatar
Johann-S committed
314
315
    return parent
  }
fat's avatar
fat committed
316

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

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

Johann-S's avatar
Johann-S committed
327
  // Static
fat's avatar
fat committed
328

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

Johann-S's avatar
Johann-S committed
334
335
336
337
338
339
340
341
342
  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
343

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

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

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

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

Johann-S's avatar
Johann-S committed
369
370
371
372
373
$(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
374

Johann-S's avatar
Johann-S committed
375
376
377
378
379
380
381
382
  const $trigger = $(this)
  const selector = Util.getSelectorFromElement(this)
  const selectors = [].slice.call(document.querySelectorAll(selector))
  $(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
383
  })
Johann-S's avatar
Johann-S committed
384
})
fat's avatar
fat committed
385

Johann-S's avatar
Johann-S committed
386
387
388
389
390
/**
 * ------------------------------------------------------------------------
 * jQuery
 * ------------------------------------------------------------------------
 */
fat's avatar
fat committed
391

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

export default Collapse