carousel.js 14.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.0.0): 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
19
20
21
22
23
24
25
26
27
  const NAME                    = 'carousel'
  const VERSION                 = '4.0.0'
  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 MILLISECONDS_MULTIPLIER = 1000
fat's avatar
fat committed
28

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

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

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

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

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

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

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

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

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

99
      this.touchTimeout        = null
100

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

      this._transitionDuration = this._getTransitionDuration()
fat's avatar
fat committed
106
107

      this._addEventListeners()
108
109
    }

XhmikosR's avatar
XhmikosR committed
110
    // Getters
111
112
113
114

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

116
117
    static get Default() {
      return Default
fat's avatar
fat committed
118
119
    }

XhmikosR's avatar
XhmikosR committed
120
    // Public
fat's avatar
fat committed
121
122

    next() {
123
124
      if (!this._isSliding) {
        this._slide(Direction.NEXT)
fat's avatar
fat committed
125
126
127
      }
    }

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

fat's avatar
fat committed
137
    prev() {
138
139
      if (!this._isSliding) {
        this._slide(Direction.PREV)
fat's avatar
fat committed
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
      }
    }

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

      if ($(this._element).find(Selector.NEXT_PREV)[0] &&
        Util.supportsTransitionEnd()) {
        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(
170
171
          (document.visibilityState ? this.nextWhenVisible : this.next).bind(this),
          this._config.interval
fat's avatar
fat committed
172
173
174
175
176
177
178
        )
      }
    }

    to(index) {
      this._activeElement = $(this._element).find(Selector.ACTIVE_ITEM)[0]

179
      const activeIndex = this._getItemIndex(this._activeElement)
fat's avatar
fat committed
180

181
      if (index > this._items.length - 1 || index < 0) {
fat's avatar
fat committed
182
183
184
185
186
187
188
189
        return
      }

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

Jacob Thornton's avatar
Jacob Thornton committed
190
      if (activeIndex === index) {
fat's avatar
fat committed
191
192
193
194
195
        this.pause()
        this.cycle()
        return
      }

XhmikosR's avatar
XhmikosR committed
196
197
198
      const direction = index > activeIndex
        ? Direction.NEXT
        : Direction.PREV
fat's avatar
fat committed
199
200
201
202

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

fat's avatar
fat committed
203
204
205
206
207
208
209
210
211
212
213
214
215
216
    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
217
    // Private
fat's avatar
fat committed
218

fat's avatar
fat committed
219
    _getConfig(config) {
220
221
222
223
      config = {
        ...Default,
        ...config
      }
fat's avatar
fat committed
224
225
226
227
      Util.typeCheckConfig(NAME, config, DefaultType)
      return config
    }

228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
    _getTransitionDuration() {
      // Get transition-duration of first element in the carousel
      let transitionDuration = $(this._element).find(Selector.ITEM).css('transition-duration')

      // Return 0 carousel item is not found
      if (!transitionDuration) {
        return 0
      }

      // If multiple durations are defined, take the first
      transitionDuration = transitionDuration.split(',')[0]

      // Multiply by 1000 if transition-duration is defined in seconds
      return transitionDuration.indexOf('ms') > -1
        ? parseFloat(transitionDuration)
        : parseFloat(transitionDuration) * MILLISECONDS_MULTIPLIER
    }

fat's avatar
fat committed
246
247
248
    _addEventListeners() {
      if (this._config.keyboard) {
        $(this._element)
249
          .on(Event.KEYDOWN, (event) => this._keydown(event))
fat's avatar
fat committed
250
251
      }

252
      if (this._config.pause === 'hover') {
fat's avatar
fat committed
253
        $(this._element)
254
255
          .on(Event.MOUSEENTER, (event) => this.pause(event))
          .on(Event.MOUSELEAVE, (event) => this.cycle(event))
256
        if ('ontouchstart' in document.documentElement) {
XhmikosR's avatar
XhmikosR committed
257
          // If it's a touch-enabled device, mouseenter/leave are fired as
258
259
260
261
262
263
264
265
266
267
268
269
270
271
          // 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
272
273
274
275
      }
    }

    _keydown(event) {
Jacob Thornton's avatar
Jacob Thornton committed
276
277
278
      if (/input|textarea/i.test(event.target.tagName)) {
        return
      }
fat's avatar
fat committed
279
280

      switch (event.which) {
281
        case ARROW_LEFT_KEYCODE:
282
          event.preventDefault()
283
284
285
          this.prev()
          break
        case ARROW_RIGHT_KEYCODE:
286
          event.preventDefault()
287
288
289
          this.next()
          break
        default:
fat's avatar
fat committed
290
291
292
293
294
295
296
297
298
      }
    }

    _getItemIndex(element) {
      this._items = $.makeArray($(element).parent().find(Selector.ITEM))
      return this._items.indexOf(element)
    }

    _getItemByDirection(direction, activeElement) {
299
      const isNextDirection = direction === Direction.NEXT
300
      const isPrevDirection = direction === Direction.PREV
301
302
303
304
      const activeIndex     = this._getItemIndex(activeElement)
      const lastItemIndex   = this._items.length - 1
      const isGoingToWrap   = isPrevDirection && activeIndex === 0 ||
                              isNextDirection && activeIndex === lastItemIndex
fat's avatar
fat committed
305
306
307
308
309

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

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

XhmikosR's avatar
XhmikosR committed
313
314
      return itemIndex === -1
        ? this._items[this._items.length - 1] : this._items[itemIndex]
fat's avatar
fat committed
315
316
    }

Mark Otto's avatar
Mark Otto committed
317
    _triggerSlideEvent(relatedTarget, eventDirectionName) {
318
319
      const targetIndex = this._getItemIndex(relatedTarget)
      const fromIndex = this._getItemIndex($(this._element).find(Selector.ACTIVE_ITEM)[0])
320
      const slideEvent = $.Event(Event.SLIDE, {
Jacob Thornton's avatar
Jacob Thornton committed
321
        relatedTarget,
322
323
324
        direction: eventDirectionName,
        from: fromIndex,
        to: targetIndex
fat's avatar
fat committed
325
326
327
328
329
330
331
332
333
334
335
336
337
      })

      $(this._element).trigger(slideEvent)

      return slideEvent
    }

    _setActiveIndicatorElement(element) {
      if (this._indicatorsElement) {
        $(this._indicatorsElement)
          .find(Selector.ACTIVE)
          .removeClass(ClassName.ACTIVE)

338
        const nextIndicator = this._indicatorsElement.children[
fat's avatar
fat committed
339
340
341
342
343
344
345
346
347
348
          this._getItemIndex(element)
        ]

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

    _slide(direction, element) {
349
      const activeElement = $(this._element).find(Selector.ACTIVE_ITEM)[0]
350
      const activeElementIndex = this._getItemIndex(activeElement)
351
      const nextElement   = element || activeElement &&
fat's avatar
fat committed
352
        this._getItemByDirection(direction, activeElement)
353
      const nextElementIndex = this._getItemIndex(nextElement)
354
      const isCycling = Boolean(this._interval)
fat's avatar
fat committed
355

Mark Otto's avatar
Mark Otto committed
356
357
358
359
360
361
362
363
364
365
366
367
368
369
      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
370
371
372
373
374
      if (nextElement && $(nextElement).hasClass(ClassName.ACTIVE)) {
        this._isSliding = false
        return
      }

Mark Otto's avatar
Mark Otto committed
375
      const slideEvent = this._triggerSlideEvent(nextElement, eventDirectionName)
fat's avatar
fat committed
376
377
378
379
380
      if (slideEvent.isDefaultPrevented()) {
        return
      }

      if (!activeElement || !nextElement) {
XhmikosR's avatar
XhmikosR committed
381
        // Some weirdness is happening, so we bail
fat's avatar
fat committed
382
383
384
385
386
387
388
389
390
391
392
        return
      }

      this._isSliding = true

      if (isCycling) {
        this.pause()
      }

      this._setActiveIndicatorElement(nextElement)

393
      const slidEvent = $.Event(Event.SLID, {
fat's avatar
fat committed
394
        relatedTarget: nextElement,
395
396
397
        direction: eventDirectionName,
        from: activeElementIndex,
        to: nextElementIndex
fat's avatar
fat committed
398
399
400
401
      })

      if (Util.supportsTransitionEnd() &&
        $(this._element).hasClass(ClassName.SLIDE)) {
Mark Otto's avatar
Mark Otto committed
402
        $(nextElement).addClass(orderClassName)
fat's avatar
fat committed
403
404
405
406
407
408
409
410
411

        Util.reflow(nextElement)

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

        $(activeElement)
          .one(Util.TRANSITION_END, () => {
            $(nextElement)
412
              .removeClass(`${directionalClassName} ${orderClassName}`)
J2TeaM's avatar
J2TeaM committed
413
              .addClass(ClassName.ACTIVE)
fat's avatar
fat committed
414

415
            $(activeElement).removeClass(`${ClassName.ACTIVE} ${orderClassName} ${directionalClassName}`)
fat's avatar
fat committed
416
417
418
419
420

            this._isSliding = false

            setTimeout(() => $(this._element).trigger(slidEvent), 0)
          })
421
          .emulateTransitionEnd(this._transitionDuration)
fat's avatar
fat committed
422
423
424
425
426
427
428
429
430
431
432
433
434
      } 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
435
    // Static
fat's avatar
fat committed
436
437
438

    static _jQueryInterface(config) {
      return this.each(function () {
XhmikosR's avatar
XhmikosR committed
439
        let data = $(this).data(DATA_KEY)
440
441
442
443
        let _config = {
          ...Default,
          ...$(this).data()
        }
fat's avatar
fat committed
444
445

        if (typeof config === 'object') {
446
447
448
449
          _config = {
            ..._config,
            ...config
          }
fat's avatar
fat committed
450
451
        }

452
        const action = typeof config === 'string' ? config : _config.slide
fat's avatar
fat committed
453
454
455
456
457
458

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

Jacob Thornton's avatar
Jacob Thornton committed
459
        if (typeof config === 'number') {
fat's avatar
fat committed
460
          data.to(config)
461
        } else if (typeof action === 'string') {
XhmikosR's avatar
XhmikosR committed
462
          if (typeof data[action] === 'undefined') {
XhmikosR's avatar
XhmikosR committed
463
            throw new TypeError(`No method named "${action}"`)
464
          }
fat's avatar
fat committed
465
466
467
468
469
470
471
472
473
          data[action]()
        } else if (_config.interval) {
          data.pause()
          data.cycle()
        }
      })
    }

    static _dataApiClickHandler(event) {
474
      const selector = Util.getSelectorFromElement(this)
fat's avatar
fat committed
475
476
477
478
479

      if (!selector) {
        return
      }

480
      const target = $(selector)[0]
fat's avatar
fat committed
481
482
483
484
485

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

486
487
488
489
      const config = {
        ...$(target).data(),
        ...$(this).data()
      }
490
      const slideIndex = this.getAttribute('data-slide-to')
Jacob Thornton's avatar
Jacob Thornton committed
491

fat's avatar
fat committed
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
      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
513
    .on(Event.CLICK_DATA_API, Selector.DATA_SLIDE, Carousel._dataApiClickHandler)
fat's avatar
fat committed
514

Jacob Thornton's avatar
Jacob Thornton committed
515
  $(window).on(Event.LOAD_DATA_API, () => {
fat's avatar
fat committed
516
    $(Selector.DATA_RIDE).each(function () {
517
      const $carousel = $(this)
fat's avatar
fat committed
518
519
520
521
522
523
524
525
526
527
      Carousel._jQueryInterface.call($carousel, $carousel.data())
    })
  })

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

XhmikosR's avatar
XhmikosR committed
528
  $.fn[NAME] = Carousel._jQueryInterface
fat's avatar
fat committed
529
  $.fn[NAME].Constructor = Carousel
XhmikosR's avatar
XhmikosR committed
530
  $.fn[NAME].noConflict = function () {
fat's avatar
fat committed
531
532
533
534
535
    $.fn[NAME] = JQUERY_NO_CONFLICT
    return Carousel._jQueryInterface
  }

  return Carousel
536
})($)
fat's avatar
fat committed
537
538

export default Carousel