carousel.js 14 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
  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
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
102
103
      this._config             = this._getConfig(config)
      this._element            = $(element)[0]
      this._indicatorsElement  = $(this._element).find(Selector.INDICATORS)[0]

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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
      }
    }

    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(
167
168
          (document.visibilityState ? this.nextWhenVisible : this.next).bind(this),
          this._config.interval
fat's avatar
fat committed
169
170
171
172
173
174
175
        )
      }
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    _getItemByDirection(direction, activeElement) {
278
      const isNextDirection = direction === Direction.NEXT
279
      const isPrevDirection = direction === Direction.PREV
280
281
282
283
      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
285
286
287
288

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

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

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

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

      $(this._element).trigger(slideEvent)

      return slideEvent
    }

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

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

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

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

Mark Otto's avatar
Mark Otto committed
335
336
337
338
339
340
341
342
343
344
345
346
347
348
      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
349
350
351
352
353
      if (nextElement && $(nextElement).hasClass(ClassName.ACTIVE)) {
        this._isSliding = false
        return
      }

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

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

      this._isSliding = true

      if (isCycling) {
        this.pause()
      }

      this._setActiveIndicatorElement(nextElement)

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

      if (Util.supportsTransitionEnd() &&
        $(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, () => {
fat's avatar
fat committed
497
    $(Selector.DATA_RIDE).each(function () {
498
      const $carousel = $(this)
fat's avatar
fat committed
499
500
501
502
503
504
505
506
507
508
      Carousel._jQueryInterface.call($carousel, $carousel.data())
    })
  })

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

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

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

export default Carousel