carousel.js 14.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): carousel.js
fat's avatar
fat committed
7
8
9
10
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
 * --------------------------------------------------------------------------
 */

11
const Carousel = (($) => {
fat's avatar
fat committed
12
13
14
15
16
17
  /**
   * ------------------------------------------------------------------------
   * Constants
   * ------------------------------------------------------------------------
   */

18
  const NAME                   = 'carousel'
Mark Otto's avatar
Mark Otto committed
19
  const VERSION                = '4.1.3'
20
21
22
23
24
25
26
  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
fat's avatar
fat committed
27

28
  const Default = {
fat's avatar
fat committed
29
30
31
32
33
34
35
    interval : 5000,
    keyboard : true,
    slide    : false,
    pause    : 'hover',
    wrap     : true
  }

fat's avatar
fat committed
36
37
38
39
40
41
42
43
  const DefaultType = {
    interval : '(number|boolean)',
    keyboard : 'boolean',
    slide    : '(boolean|string)',
    pause    : '(string|boolean)',
    wrap     : 'boolean'
  }

fat's avatar
fat committed
44
45
  const Direction = {
    NEXT     : 'next',
Mark Otto's avatar
Mark Otto committed
46
    PREV     : 'prev',
Mark Otto's avatar
Mark Otto committed
47
48
    LEFT     : 'left',
    RIGHT    : 'right'
fat's avatar
fat committed
49
50
51
  }

  const Event = {
fat's avatar
fat committed
52
53
54
55
56
    SLIDE          : `slide${EVENT_KEY}`,
    SLID           : `slid${EVENT_KEY}`,
    KEYDOWN        : `keydown${EVENT_KEY}`,
    MOUSEENTER     : `mouseenter${EVENT_KEY}`,
    MOUSELEAVE     : `mouseleave${EVENT_KEY}`,
57
    TOUCHEND       : `touchend${EVENT_KEY}`,
fat's avatar
fat committed
58
59
    LOAD_DATA_API  : `load${EVENT_KEY}${DATA_API_KEY}`,
    CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`
fat's avatar
fat committed
60
61
62
63
64
65
  }

  const ClassName = {
    CAROUSEL : 'carousel',
    ACTIVE   : 'active',
    SLIDE    : 'slide',
Mark Otto's avatar
Mark Otto committed
66
67
68
    RIGHT    : 'carousel-item-right',
    LEFT     : 'carousel-item-left',
    NEXT     : 'carousel-item-next',
Mark Otto's avatar
Mark Otto committed
69
    PREV     : 'carousel-item-prev',
fat's avatar
fat committed
70
71
72
73
74
75
76
    ITEM     : 'carousel-item'
  }

  const Selector = {
    ACTIVE      : '.active',
    ACTIVE_ITEM : '.active.carousel-item',
    ITEM        : '.carousel-item',
Mark Otto's avatar
Mark Otto committed
77
    NEXT_PREV   : '.carousel-item-next, .carousel-item-prev',
fat's avatar
fat committed
78
79
80
81
82
83
84
85
86
87
88
89
90
    INDICATORS  : '.carousel-indicators',
    DATA_SLIDE  : '[data-slide], [data-slide-to]',
    DATA_RIDE   : '[data-ride="carousel"]'
  }

  /**
   * ------------------------------------------------------------------------
   * Class Definition
   * ------------------------------------------------------------------------
   */

  class Carousel {
    constructor(element, config) {
91
92
93
      this._items              = null
      this._interval           = null
      this._activeElement      = null
fat's avatar
fat committed
94

95
96
      this._isPaused           = false
      this._isSliding          = false
fat's avatar
fat committed
97

98
      this.touchTimeout        = null
99

100
101
      this._config             = this._getConfig(config)
      this._element            = $(element)[0]
102
      this._indicatorsElement  = this._element.querySelector(Selector.INDICATORS)
103

fat's avatar
fat committed
104
      this._addEventListeners()
105
106
    }

XhmikosR's avatar
XhmikosR committed
107
    // Getters
108
109
110
111

    static get VERSION() {
      return VERSION
    }
fat's avatar
fat committed
112

113
114
    static get Default() {
      return Default
fat's avatar
fat committed
115
116
    }

XhmikosR's avatar
XhmikosR committed
117
    // Public
fat's avatar
fat committed
118
119

    next() {
120
121
      if (!this._isSliding) {
        this._slide(Direction.NEXT)
fat's avatar
fat committed
122
123
124
      }
    }

125
126
    nextWhenVisible() {
      // Don't call next when the page isn't visible
127
      // or the carousel or its parent isn't visible
128
129
      if (!document.hidden &&
        ($(this._element).is(':visible') && $(this._element).css('visibility') !== 'hidden')) {
130
131
132
133
        this.next()
      }
    }

fat's avatar
fat committed
134
    prev() {
135
136
      if (!this._isSliding) {
        this._slide(Direction.PREV)
fat's avatar
fat committed
137
138
139
140
141
142
143
144
      }
    }

    pause(event) {
      if (!event) {
        this._isPaused = true
      }

145
      if (this._element.querySelector(Selector.NEXT_PREV)) {
fat's avatar
fat committed
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
        Util.triggerTransitionEnd(this._element)
        this.cycle(true)
      }

      clearInterval(this._interval)
      this._interval = null
    }

    cycle(event) {
      if (!event) {
        this._isPaused = false
      }

      if (this._interval) {
        clearInterval(this._interval)
        this._interval = null
      }

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

    to(index) {
173
      this._activeElement = this._element.querySelector(Selector.ACTIVE_ITEM)
fat's avatar
fat committed
174

175
      const activeIndex = this._getItemIndex(this._activeElement)
fat's avatar
fat committed
176

177
      if (index > this._items.length - 1 || index < 0) {
fat's avatar
fat committed
178
179
180
181
182
183
184
185
        return
      }

      if (this._isSliding) {
        $(this._element).one(Event.SLID, () => this.to(index))
        return
      }

Jacob Thornton's avatar
Jacob Thornton committed
186
      if (activeIndex === index) {
fat's avatar
fat committed
187
188
189
190
191
        this.pause()
        this.cycle()
        return
      }

XhmikosR's avatar
XhmikosR committed
192
193
194
      const direction = index > activeIndex
        ? Direction.NEXT
        : Direction.PREV
fat's avatar
fat committed
195
196
197
198

      this._slide(direction, this._items[index])
    }

fat's avatar
fat committed
199
200
201
202
203
204
205
206
207
208
209
210
211
212
    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
    }

XhmikosR's avatar
XhmikosR committed
213
    // Private
fat's avatar
fat committed
214

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

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

230
      if (this._config.pause === 'hover') {
fat's avatar
fat committed
231
        $(this._element)
232
233
          .on(Event.MOUSEENTER, (event) => this.pause(event))
          .on(Event.MOUSELEAVE, (event) => this.cycle(event))
234
        if ('ontouchstart' in document.documentElement) {
XhmikosR's avatar
XhmikosR committed
235
          // If it's a touch-enabled device, mouseenter/leave are fired as
236
237
238
239
240
241
242
243
244
245
246
247
248
249
          // 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)
          })
        }
fat's avatar
fat committed
250
251
252
253
      }
    }

    _keydown(event) {
Jacob Thornton's avatar
Jacob Thornton committed
254
255
256
      if (/input|textarea/i.test(event.target.tagName)) {
        return
      }
fat's avatar
fat committed
257
258

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

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

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

      if (isGoingToWrap && !this._config.wrap) {
        return activeElement
      }

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

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

Mark Otto's avatar
Mark Otto committed
297
    _triggerSlideEvent(relatedTarget, eventDirectionName) {
298
      const targetIndex = this._getItemIndex(relatedTarget)
299
      const fromIndex = this._getItemIndex(this._element.querySelector(Selector.ACTIVE_ITEM))
300
      const slideEvent = $.Event(Event.SLIDE, {
Jacob Thornton's avatar
Jacob Thornton committed
301
        relatedTarget,
302
303
304
        direction: eventDirectionName,
        from: fromIndex,
        to: targetIndex
fat's avatar
fat committed
305
306
307
308
309
310
311
312
313
      })

      $(this._element).trigger(slideEvent)

      return slideEvent
    }

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

318
        const nextIndicator = this._indicatorsElement.children[
fat's avatar
fat committed
319
320
321
322
323
324
325
326
327
328
          this._getItemIndex(element)
        ]

        if (nextIndicator) {
          $(nextIndicator).addClass(ClassName.ACTIVE)
        }
      }
    }

    _slide(direction, element) {
329
      const activeElement = this._element.querySelector(Selector.ACTIVE_ITEM)
330
      const activeElementIndex = this._getItemIndex(activeElement)
331
      const nextElement   = element || activeElement &&
fat's avatar
fat committed
332
        this._getItemByDirection(direction, activeElement)
333
      const nextElementIndex = this._getItemIndex(nextElement)
334
      const isCycling = Boolean(this._interval)
fat's avatar
fat committed
335

Mark Otto's avatar
Mark Otto committed
336
337
338
339
340
341
342
343
344
345
346
347
348
349
      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
      }

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

Mark Otto's avatar
Mark Otto committed
355
      const slideEvent = this._triggerSlideEvent(nextElement, eventDirectionName)
fat's avatar
fat committed
356
357
358
359
360
      if (slideEvent.isDefaultPrevented()) {
        return
      }

      if (!activeElement || !nextElement) {
XhmikosR's avatar
XhmikosR committed
361
        // Some weirdness is happening, so we bail
fat's avatar
fat committed
362
363
364
365
366
367
368
369
370
371
372
        return
      }

      this._isSliding = true

      if (isCycling) {
        this.pause()
      }

      this._setActiveIndicatorElement(nextElement)

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

380
      if ($(this._element).hasClass(ClassName.SLIDE)) {
Mark Otto's avatar
Mark Otto committed
381
        $(nextElement).addClass(orderClassName)
fat's avatar
fat committed
382
383
384
385
386
387

        Util.reflow(nextElement)

        $(activeElement).addClass(directionalClassName)
        $(nextElement).addClass(directionalClassName)

388
389
        const transitionDuration = Util.getTransitionDurationFromElement(activeElement)

fat's avatar
fat committed
390
391
392
        $(activeElement)
          .one(Util.TRANSITION_END, () => {
            $(nextElement)
393
              .removeClass(`${directionalClassName} ${orderClassName}`)
J2TeaM's avatar
J2TeaM committed
394
              .addClass(ClassName.ACTIVE)
fat's avatar
fat committed
395

396
            $(activeElement).removeClass(`${ClassName.ACTIVE} ${orderClassName} ${directionalClassName}`)
fat's avatar
fat committed
397
398
399
400
401

            this._isSliding = false

            setTimeout(() => $(this._element).trigger(slidEvent), 0)
          })
402
          .emulateTransitionEnd(transitionDuration)
fat's avatar
fat committed
403
404
405
406
407
408
409
410
411
412
413
414
415
      } else {
        $(activeElement).removeClass(ClassName.ACTIVE)
        $(nextElement).addClass(ClassName.ACTIVE)

        this._isSliding = false
        $(this._element).trigger(slidEvent)
      }

      if (isCycling) {
        this.cycle()
      }
    }

XhmikosR's avatar
XhmikosR committed
416
    // Static
fat's avatar
fat committed
417
418
419

    static _jQueryInterface(config) {
      return this.each(function () {
XhmikosR's avatar
XhmikosR committed
420
        let data = $(this).data(DATA_KEY)
421
422
423
424
        let _config = {
          ...Default,
          ...$(this).data()
        }
fat's avatar
fat committed
425
426

        if (typeof config === 'object') {
427
428
429
430
          _config = {
            ..._config,
            ...config
          }
fat's avatar
fat committed
431
432
        }

433
        const action = typeof config === 'string' ? config : _config.slide
fat's avatar
fat committed
434
435
436
437
438
439

        if (!data) {
          data = new Carousel(this, _config)
          $(this).data(DATA_KEY, data)
        }

Jacob Thornton's avatar
Jacob Thornton committed
440
        if (typeof config === 'number') {
fat's avatar
fat committed
441
          data.to(config)
442
        } else if (typeof action === 'string') {
XhmikosR's avatar
XhmikosR committed
443
          if (typeof data[action] === 'undefined') {
XhmikosR's avatar
XhmikosR committed
444
            throw new TypeError(`No method named "${action}"`)
445
          }
fat's avatar
fat committed
446
447
448
449
450
451
452
453
454
          data[action]()
        } else if (_config.interval) {
          data.pause()
          data.cycle()
        }
      })
    }

    static _dataApiClickHandler(event) {
455
      const selector = Util.getSelectorFromElement(this)
fat's avatar
fat committed
456
457
458
459
460

      if (!selector) {
        return
      }

461
      const target = $(selector)[0]
fat's avatar
fat committed
462
463
464
465
466

      if (!target || !$(target).hasClass(ClassName.CAROUSEL)) {
        return
      }

467
468
469
470
      const config = {
        ...$(target).data(),
        ...$(this).data()
      }
471
      const slideIndex = this.getAttribute('data-slide-to')
Jacob Thornton's avatar
Jacob Thornton committed
472

fat's avatar
fat committed
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
      if (slideIndex) {
        config.interval = false
      }

      Carousel._jQueryInterface.call($(target), config)

      if (slideIndex) {
        $(target).data(DATA_KEY).to(slideIndex)
      }

      event.preventDefault()
    }
  }

  /**
   * ------------------------------------------------------------------------
   * Data Api implementation
   * ------------------------------------------------------------------------
   */

  $(document)
fat's avatar
fat committed
494
    .on(Event.CLICK_DATA_API, Selector.DATA_SLIDE, Carousel._dataApiClickHandler)
fat's avatar
fat committed
495

Jacob Thornton's avatar
Jacob Thornton committed
496
  $(window).on(Event.LOAD_DATA_API, () => {
497
498
499
    const carousels = [].slice.call(document.querySelectorAll(Selector.DATA_RIDE))
    for (let i = 0, len = carousels.length; i < len; i++) {
      const $carousel = $(carousels[i])
fat's avatar
fat committed
500
      Carousel._jQueryInterface.call($carousel, $carousel.data())
501
    }
fat's avatar
fat committed
502
503
504
505
506
507
508
509
  })

  /**
   * ------------------------------------------------------------------------
   * jQuery
   * ------------------------------------------------------------------------
   */

XhmikosR's avatar
XhmikosR committed
510
  $.fn[NAME] = Carousel._jQueryInterface
fat's avatar
fat committed
511
  $.fn[NAME].Constructor = Carousel
XhmikosR's avatar
XhmikosR committed
512
  $.fn[NAME].noConflict = () => {
fat's avatar
fat committed
513
514
515
516
517
    $.fn[NAME] = JQUERY_NO_CONFLICT
    return Carousel._jQueryInterface
  }

  return Carousel
518
})($)
fat's avatar
fat committed
519
520

export default Carousel