carousel.js 13.7 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): carousel.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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
const NAME                   = 'carousel'
const VERSION                = '4.1.3'
const DATA_KEY               = 'bs.carousel'
const EVENT_KEY              = `.${DATA_KEY}`
const DATA_API_KEY           = '.data-api'
const JQUERY_NO_CONFLICT     = $.fn[NAME]
const ARROW_LEFT_KEYCODE     = 37 // KeyboardEvent.which value for left arrow key
const ARROW_RIGHT_KEYCODE    = 39 // KeyboardEvent.which value for right arrow key
const TOUCHEVENT_COMPAT_WAIT = 500 // Time for mouse compat events to fire after touch

const Default = {
  interval : 5000,
  keyboard : true,
  slide    : false,
  pause    : 'hover',
  wrap     : true
}

const DefaultType = {
  interval : '(number|boolean)',
  keyboard : 'boolean',
  slide    : '(boolean|string)',
  pause    : '(string|boolean)',
  wrap     : 'boolean'
}

const Direction = {
  NEXT     : 'next',
  PREV     : 'prev',
  LEFT     : 'left',
  RIGHT    : 'right'
}

const Event = {
  SLIDE          : `slide${EVENT_KEY}`,
  SLID           : `slid${EVENT_KEY}`,
  KEYDOWN        : `keydown${EVENT_KEY}`,
  MOUSEENTER     : `mouseenter${EVENT_KEY}`,
  MOUSELEAVE     : `mouseleave${EVENT_KEY}`,
  TOUCHEND       : `touchend${EVENT_KEY}`,
  LOAD_DATA_API  : `load${EVENT_KEY}${DATA_API_KEY}`,
  CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`
}

const ClassName = {
  CAROUSEL : 'carousel',
  ACTIVE   : 'active',
  SLIDE    : 'slide',
  RIGHT    : 'carousel-item-right',
  LEFT     : 'carousel-item-left',
  NEXT     : 'carousel-item-next',
  PREV     : 'carousel-item-prev',
  ITEM     : 'carousel-item'
}

const Selector = {
  ACTIVE      : '.active',
  ACTIVE_ITEM : '.active.carousel-item',
  ITEM        : '.carousel-item',
  NEXT_PREV   : '.carousel-item-next, .carousel-item-prev',
  INDICATORS  : '.carousel-indicators',
  DATA_SLIDE  : '[data-slide], [data-slide-to]',
  DATA_RIDE   : '[data-ride="carousel"]'
}
fat's avatar
fat committed
81

Johann-S's avatar
Johann-S committed
82
83
84
85
86
/**
 * ------------------------------------------------------------------------
 * Class Definition
 * ------------------------------------------------------------------------
 */
fat's avatar
fat committed
87

Johann-S's avatar
Johann-S committed
88
89
90
91
92
class Carousel {
  constructor(element, config) {
    this._items              = null
    this._interval           = null
    this._activeElement      = null
fat's avatar
fat committed
93

Johann-S's avatar
Johann-S committed
94
95
    this._isPaused           = false
    this._isSliding          = false
fat's avatar
fat committed
96

Johann-S's avatar
Johann-S committed
97
    this.touchTimeout        = null
fat's avatar
fat committed
98

Johann-S's avatar
Johann-S committed
99
100
101
    this._config             = this._getConfig(config)
    this._element            = $(element)[0]
    this._indicatorsElement  = this._element.querySelector(Selector.INDICATORS)
fat's avatar
fat committed
102

Johann-S's avatar
Johann-S committed
103
104
    this._addEventListeners()
  }
fat's avatar
fat committed
105

Johann-S's avatar
Johann-S committed
106
  // Getters
107

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

Johann-S's avatar
Johann-S committed
112
113
114
  static get Default() {
    return Default
  }
115

Johann-S's avatar
Johann-S committed
116
  // Public
117

Johann-S's avatar
Johann-S committed
118
119
120
  next() {
    if (!this._isSliding) {
      this._slide(Direction.NEXT)
121
    }
Johann-S's avatar
Johann-S committed
122
  }
fat's avatar
fat committed
123

Johann-S's avatar
Johann-S committed
124
125
126
127
128
129
  nextWhenVisible() {
    // Don't call next when the page isn't visible
    // or the carousel or its parent isn't visible
    if (!document.hidden &&
      ($(this._element).is(':visible') && $(this._element).css('visibility') !== 'hidden')) {
      this.next()
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
135
  prev() {
    if (!this._isSliding) {
      this._slide(Direction.PREV)
fat's avatar
fat committed
136
    }
Johann-S's avatar
Johann-S committed
137
  }
fat's avatar
fat committed
138

Johann-S's avatar
Johann-S committed
139
140
141
  pause(event) {
    if (!event) {
      this._isPaused = true
142
143
    }

Johann-S's avatar
Johann-S committed
144
145
146
    if (this._element.querySelector(Selector.NEXT_PREV)) {
      Util.triggerTransitionEnd(this._element)
      this.cycle(true)
fat's avatar
fat committed
147
148
    }

Johann-S's avatar
Johann-S committed
149
150
151
    clearInterval(this._interval)
    this._interval = null
  }
fat's avatar
fat committed
152

Johann-S's avatar
Johann-S committed
153
154
155
156
  cycle(event) {
    if (!event) {
      this._isPaused = false
    }
fat's avatar
fat committed
157

Johann-S's avatar
Johann-S committed
158
    if (this._interval) {
fat's avatar
fat committed
159
160
161
162
      clearInterval(this._interval)
      this._interval = null
    }

Johann-S's avatar
Johann-S committed
163
164
165
166
167
168
169
    if (this._config.interval && !this._isPaused) {
      this._interval = setInterval(
        (document.visibilityState ? this.nextWhenVisible : this.next).bind(this),
        this._config.interval
      )
    }
  }
fat's avatar
fat committed
170

Johann-S's avatar
Johann-S committed
171
172
  to(index) {
    this._activeElement = this._element.querySelector(Selector.ACTIVE_ITEM)
fat's avatar
fat committed
173

Johann-S's avatar
Johann-S committed
174
175
176
177
    const activeIndex = this._getItemIndex(this._activeElement)

    if (index > this._items.length - 1 || index < 0) {
      return
fat's avatar
fat committed
178
179
    }

Johann-S's avatar
Johann-S committed
180
181
182
183
    if (this._isSliding) {
      $(this._element).one(Event.SLID, () => this.to(index))
      return
    }
fat's avatar
fat committed
184

Johann-S's avatar
Johann-S committed
185
186
187
188
189
    if (activeIndex === index) {
      this.pause()
      this.cycle()
      return
    }
fat's avatar
fat committed
190

Johann-S's avatar
Johann-S committed
191
192
193
    const direction = index > activeIndex
      ? Direction.NEXT
      : Direction.PREV
fat's avatar
fat committed
194

Johann-S's avatar
Johann-S committed
195
196
    this._slide(direction, this._items[index])
  }
fat's avatar
fat committed
197

Johann-S's avatar
Johann-S committed
198
199
200
201
202
203
204
205
206
207
208
209
210
  dispose() {
    $(this._element).off(EVENT_KEY)
    $.removeData(this._element, DATA_KEY)

    this._items             = null
    this._config            = null
    this._element           = null
    this._interval          = null
    this._isPaused          = null
    this._isSliding         = null
    this._activeElement     = null
    this._indicatorsElement = null
  }
fat's avatar
fat committed
211

Johann-S's avatar
Johann-S committed
212
  // Private
fat's avatar
fat committed
213

Johann-S's avatar
Johann-S committed
214
215
216
217
  _getConfig(config) {
    config = {
      ...Default,
      ...config
fat's avatar
fat committed
218
    }
Johann-S's avatar
Johann-S committed
219
220
221
    Util.typeCheckConfig(NAME, config, DefaultType)
    return config
  }
fat's avatar
fat committed
222

Johann-S's avatar
Johann-S committed
223
224
225
226
  _addEventListeners() {
    if (this._config.keyboard) {
      $(this._element)
        .on(Event.KEYDOWN, (event) => this._keydown(event))
fat's avatar
fat committed
227
228
    }

Johann-S's avatar
Johann-S committed
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
    if (this._config.pause === 'hover') {
      $(this._element)
        .on(Event.MOUSEENTER, (event) => this.pause(event))
        .on(Event.MOUSELEAVE, (event) => this.cycle(event))
      if ('ontouchstart' in document.documentElement) {
        // If it's a touch-enabled device, mouseenter/leave are fired as
        // part of the mouse compatibility events on first tap - the carousel
        // would stop cycling until user tapped out of it;
        // here, we listen for touchend, explicitly pause the carousel
        // (as if it's the second time we tap on it, mouseenter compat event
        // is NOT fired) and after a timeout (to allow for mouse compatibility
        // events to fire) we explicitly restart cycling
        $(this._element).on(Event.TOUCHEND, () => {
          this.pause()
          if (this.touchTimeout) {
            clearTimeout(this.touchTimeout)
          }
          this.touchTimeout = setTimeout((event) => this.cycle(event), TOUCHEVENT_COMPAT_WAIT + this._config.interval)
        })
248
      }
fat's avatar
fat committed
249
    }
Johann-S's avatar
Johann-S committed
250
  }
fat's avatar
fat committed
251

Johann-S's avatar
Johann-S committed
252
253
254
  _keydown(event) {
    if (/input|textarea/i.test(event.target.tagName)) {
      return
fat's avatar
fat committed
255
256
    }

Johann-S's avatar
Johann-S committed
257
258
259
260
261
262
263
264
265
266
    switch (event.which) {
      case ARROW_LEFT_KEYCODE:
        event.preventDefault()
        this.prev()
        break
      case ARROW_RIGHT_KEYCODE:
        event.preventDefault()
        this.next()
        break
      default:
fat's avatar
fat committed
267
    }
Johann-S's avatar
Johann-S committed
268
  }
fat's avatar
fat committed
269

Johann-S's avatar
Johann-S committed
270
271
272
273
274
275
  _getItemIndex(element) {
    this._items = element && element.parentNode
      ? [].slice.call(element.parentNode.querySelectorAll(Selector.ITEM))
      : []
    return this._items.indexOf(element)
  }
fat's avatar
fat committed
276

Johann-S's avatar
Johann-S committed
277
278
279
280
281
282
283
  _getItemByDirection(direction, activeElement) {
    const isNextDirection = direction === Direction.NEXT
    const isPrevDirection = direction === Direction.PREV
    const activeIndex     = this._getItemIndex(activeElement)
    const lastItemIndex   = this._items.length - 1
    const isGoingToWrap   = isPrevDirection && activeIndex === 0 ||
                            isNextDirection && activeIndex === lastItemIndex
fat's avatar
fat committed
284

Johann-S's avatar
Johann-S committed
285
286
287
    if (isGoingToWrap && !this._config.wrap) {
      return activeElement
    }
fat's avatar
fat committed
288

Johann-S's avatar
Johann-S committed
289
290
    const delta     = direction === Direction.PREV ? -1 : 1
    const itemIndex = (activeIndex + delta) % this._items.length
fat's avatar
fat committed
291

Johann-S's avatar
Johann-S committed
292
293
294
    return itemIndex === -1
      ? this._items[this._items.length - 1] : this._items[itemIndex]
  }
fat's avatar
fat committed
295

Johann-S's avatar
Johann-S committed
296
297
298
299
300
301
302
303
304
  _triggerSlideEvent(relatedTarget, eventDirectionName) {
    const targetIndex = this._getItemIndex(relatedTarget)
    const fromIndex = this._getItemIndex(this._element.querySelector(Selector.ACTIVE_ITEM))
    const slideEvent = $.Event(Event.SLIDE, {
      relatedTarget,
      direction: eventDirectionName,
      from: fromIndex,
      to: targetIndex
    })
fat's avatar
fat committed
305

Johann-S's avatar
Johann-S committed
306
    $(this._element).trigger(slideEvent)
fat's avatar
fat committed
307

Johann-S's avatar
Johann-S committed
308
309
    return slideEvent
  }
fat's avatar
fat committed
310

Johann-S's avatar
Johann-S committed
311
312
313
314
315
  _setActiveIndicatorElement(element) {
    if (this._indicatorsElement) {
      const indicators = [].slice.call(this._indicatorsElement.querySelectorAll(Selector.ACTIVE))
      $(indicators)
        .removeClass(ClassName.ACTIVE)
fat's avatar
fat committed
316

Johann-S's avatar
Johann-S committed
317
318
319
      const nextIndicator = this._indicatorsElement.children[
        this._getItemIndex(element)
      ]
fat's avatar
fat committed
320

Johann-S's avatar
Johann-S committed
321
322
      if (nextIndicator) {
        $(nextIndicator).addClass(ClassName.ACTIVE)
fat's avatar
fat committed
323
324
      }
    }
Johann-S's avatar
Johann-S committed
325
  }
fat's avatar
fat committed
326

Johann-S's avatar
Johann-S committed
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
  _slide(direction, element) {
    const activeElement = this._element.querySelector(Selector.ACTIVE_ITEM)
    const activeElementIndex = this._getItemIndex(activeElement)
    const nextElement   = element || activeElement &&
      this._getItemByDirection(direction, activeElement)
    const nextElementIndex = this._getItemIndex(nextElement)
    const isCycling = Boolean(this._interval)

    let directionalClassName
    let orderClassName
    let eventDirectionName

    if (direction === Direction.NEXT) {
      directionalClassName = ClassName.LEFT
      orderClassName = ClassName.NEXT
      eventDirectionName = Direction.LEFT
    } else {
      directionalClassName = ClassName.RIGHT
      orderClassName = ClassName.PREV
      eventDirectionName = Direction.RIGHT
    }
Mark Otto's avatar
Mark Otto committed
348

Johann-S's avatar
Johann-S committed
349
350
351
352
    if (nextElement && $(nextElement).hasClass(ClassName.ACTIVE)) {
      this._isSliding = false
      return
    }
fat's avatar
fat committed
353

Johann-S's avatar
Johann-S committed
354
355
356
357
    const slideEvent = this._triggerSlideEvent(nextElement, eventDirectionName)
    if (slideEvent.isDefaultPrevented()) {
      return
    }
fat's avatar
fat committed
358

Johann-S's avatar
Johann-S committed
359
360
361
362
    if (!activeElement || !nextElement) {
      // Some weirdness is happening, so we bail
      return
    }
fat's avatar
fat committed
363

Johann-S's avatar
Johann-S committed
364
    this._isSliding = true
fat's avatar
fat committed
365

Johann-S's avatar
Johann-S committed
366
367
368
    if (isCycling) {
      this.pause()
    }
fat's avatar
fat committed
369

Johann-S's avatar
Johann-S committed
370
    this._setActiveIndicatorElement(nextElement)
fat's avatar
fat committed
371

Johann-S's avatar
Johann-S committed
372
373
374
375
376
377
    const slidEvent = $.Event(Event.SLID, {
      relatedTarget: nextElement,
      direction: eventDirectionName,
      from: activeElementIndex,
      to: nextElementIndex
    })
fat's avatar
fat committed
378

Johann-S's avatar
Johann-S committed
379
380
    if ($(this._element).hasClass(ClassName.SLIDE)) {
      $(nextElement).addClass(orderClassName)
fat's avatar
fat committed
381

Johann-S's avatar
Johann-S committed
382
      Util.reflow(nextElement)
fat's avatar
fat committed
383

Johann-S's avatar
Johann-S committed
384
385
      $(activeElement).addClass(directionalClassName)
      $(nextElement).addClass(directionalClassName)
fat's avatar
fat committed
386

Johann-S's avatar
Johann-S committed
387
388
389
390
391
392
393
      const nextElementInterval = parseInt(nextElement.getAttribute('data-interval'), 10)
      if (nextElementInterval) {
        this._config.defaultInterval = this._config.defaultInterval || this._config.interval
        this._config.interval = nextElementInterval
      } else {
        this._config.interval = this._config.defaultInterval || this._config.interval
      }
394

Johann-S's avatar
Johann-S committed
395
      const transitionDuration = Util.getTransitionDurationFromElement(activeElement)
396

Johann-S's avatar
Johann-S committed
397
398
399
400
401
      $(activeElement)
        .one(Util.TRANSITION_END, () => {
          $(nextElement)
            .removeClass(`${directionalClassName} ${orderClassName}`)
            .addClass(ClassName.ACTIVE)
fat's avatar
fat committed
402

Johann-S's avatar
Johann-S committed
403
          $(activeElement).removeClass(`${ClassName.ACTIVE} ${orderClassName} ${directionalClassName}`)
fat's avatar
fat committed
404

Johann-S's avatar
Johann-S committed
405
          this._isSliding = false
fat's avatar
fat committed
406

Johann-S's avatar
Johann-S committed
407
408
409
410
411
412
          setTimeout(() => $(this._element).trigger(slidEvent), 0)
        })
        .emulateTransitionEnd(transitionDuration)
    } else {
      $(activeElement).removeClass(ClassName.ACTIVE)
      $(nextElement).addClass(ClassName.ACTIVE)
fat's avatar
fat committed
413

Johann-S's avatar
Johann-S committed
414
415
416
      this._isSliding = false
      $(this._element).trigger(slidEvent)
    }
fat's avatar
fat committed
417

Johann-S's avatar
Johann-S committed
418
419
    if (isCycling) {
      this.cycle()
fat's avatar
fat committed
420
    }
Johann-S's avatar
Johann-S committed
421
  }
fat's avatar
fat committed
422

Johann-S's avatar
Johann-S committed
423
  // Static
fat's avatar
fat committed
424

Johann-S's avatar
Johann-S committed
425
426
427
428
429
430
431
  static _jQueryInterface(config) {
    return this.each(function () {
      let data = $(this).data(DATA_KEY)
      let _config = {
        ...Default,
        ...$(this).data()
      }
fat's avatar
fat committed
432

Johann-S's avatar
Johann-S committed
433
434
435
436
      if (typeof config === 'object') {
        _config = {
          ..._config,
          ...config
fat's avatar
fat committed
437
        }
Johann-S's avatar
Johann-S committed
438
      }
fat's avatar
fat committed
439

Johann-S's avatar
Johann-S committed
440
      const action = typeof config === 'string' ? config : _config.slide
fat's avatar
fat committed
441

Johann-S's avatar
Johann-S committed
442
443
444
445
      if (!data) {
        data = new Carousel(this, _config)
        $(this).data(DATA_KEY, data)
      }
fat's avatar
fat committed
446

Johann-S's avatar
Johann-S committed
447
448
449
450
451
      if (typeof config === 'number') {
        data.to(config)
      } else if (typeof action === 'string') {
        if (typeof data[action] === 'undefined') {
          throw new TypeError(`No method named "${action}"`)
fat's avatar
fat committed
452
        }
Johann-S's avatar
Johann-S committed
453
454
455
456
        data[action]()
      } else if (_config.interval) {
        data.pause()
        data.cycle()
fat's avatar
fat committed
457
      }
Johann-S's avatar
Johann-S committed
458
459
    })
  }
fat's avatar
fat committed
460

Johann-S's avatar
Johann-S committed
461
462
  static _dataApiClickHandler(event) {
    const selector = Util.getSelectorFromElement(this)
fat's avatar
fat committed
463

Johann-S's avatar
Johann-S committed
464
465
466
    if (!selector) {
      return
    }
fat's avatar
fat committed
467

Johann-S's avatar
Johann-S committed
468
    const target = $(selector)[0]
Jacob Thornton's avatar
Jacob Thornton committed
469

Johann-S's avatar
Johann-S committed
470
471
472
    if (!target || !$(target).hasClass(ClassName.CAROUSEL)) {
      return
    }
fat's avatar
fat committed
473

Johann-S's avatar
Johann-S committed
474
475
476
477
478
    const config = {
      ...$(target).data(),
      ...$(this).data()
    }
    const slideIndex = this.getAttribute('data-slide-to')
fat's avatar
fat committed
479

Johann-S's avatar
Johann-S committed
480
481
482
483
484
    if (slideIndex) {
      config.interval = false
    }

    Carousel._jQueryInterface.call($(target), config)
fat's avatar
fat committed
485

Johann-S's avatar
Johann-S committed
486
487
    if (slideIndex) {
      $(target).data(DATA_KEY).to(slideIndex)
fat's avatar
fat committed
488
    }
Johann-S's avatar
Johann-S committed
489
490

    event.preventDefault()
fat's avatar
fat committed
491
  }
Johann-S's avatar
Johann-S committed
492
}
fat's avatar
fat committed
493

Johann-S's avatar
Johann-S committed
494
495
496
497
498
/**
 * ------------------------------------------------------------------------
 * Data Api implementation
 * ------------------------------------------------------------------------
 */
fat's avatar
fat committed
499

Johann-S's avatar
Johann-S committed
500
501
$(document)
  .on(Event.CLICK_DATA_API, Selector.DATA_SLIDE, Carousel._dataApiClickHandler)
fat's avatar
fat committed
502

Johann-S's avatar
Johann-S committed
503
504
505
506
507
$(window).on(Event.LOAD_DATA_API, () => {
  const carousels = [].slice.call(document.querySelectorAll(Selector.DATA_RIDE))
  for (let i = 0, len = carousels.length; i < len; i++) {
    const $carousel = $(carousels[i])
    Carousel._jQueryInterface.call($carousel, $carousel.data())
fat's avatar
fat committed
508
  }
Johann-S's avatar
Johann-S committed
509
510
511
512
513
514
515
})

/**
 * ------------------------------------------------------------------------
 * jQuery
 * ------------------------------------------------------------------------
 */
fat's avatar
fat committed
516

Johann-S's avatar
Johann-S committed
517
518
519
520
521
522
$.fn[NAME] = Carousel._jQueryInterface
$.fn[NAME].Constructor = Carousel
$.fn[NAME].noConflict = () => {
  $.fn[NAME] = JQUERY_NO_CONFLICT
  return Carousel._jQueryInterface
}
fat's avatar
fat committed
523
524

export default Carousel