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
  const NAME                   = 'carousel'
Mark Otto's avatar
Mark Otto committed
19
  const VERSION                = '4.0.0'
20
21
22
23
24
25
26
27
  const DATA_KEY               = 'bs.carousel'
  const EVENT_KEY              = `.${DATA_KEY}`
  const DATA_API_KEY           = '.data-api'
  const JQUERY_NO_CONFLICT     = $.fn[NAME]
  const TRANSITION_DURATION    = 600
  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
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
92
93
94
95
96
97
98
    INDICATORS  : '.carousel-indicators',
    DATA_SLIDE  : '[data-slide], [data-slide-to]',
    DATA_RIDE   : '[data-ride="carousel"]'
  }

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

  class Carousel {
    constructor(element, config) {
      this._items             = null
      this._interval          = null
      this._activeElement     = null

      this._isPaused          = false
      this._isSliding         = false

99
100
      this.touchTimeout       = null

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

      this._addEventListeners()
106
107
    }

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

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

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

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

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

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

fat's avatar
fat committed
135
    prev() {
136
137
      if (!this._isSliding) {
        this._slide(Direction.PREV)
fat's avatar
fat committed
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
167
      }
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    _getItemIndex(element) {
      this._items = $.makeArray($(element).parent().find(Selector.ITEM))
      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
299
      const targetIndex = this._getItemIndex(relatedTarget)
      const fromIndex = this._getItemIndex($(this._element).find(Selector.ACTIVE_ITEM)[0])
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
314
315
316
317
      })

      $(this._element).trigger(slideEvent)

      return slideEvent
    }

    _setActiveIndicatorElement(element) {
      if (this._indicatorsElement) {
        $(this._indicatorsElement)
          .find(Selector.ACTIVE)
          .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).find(Selector.ACTIVE_ITEM)[0]
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
381
      })

      if (Util.supportsTransitionEnd() &&
        $(this._element).hasClass(ClassName.SLIDE)) {
Mark Otto's avatar
Mark Otto committed
382
        $(nextElement).addClass(orderClassName)
fat's avatar
fat committed
383
384
385
386
387
388
389
390
391

        Util.reflow(nextElement)

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

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

395
            $(activeElement).removeClass(`${ClassName.ACTIVE} ${orderClassName} ${directionalClassName}`)
fat's avatar
fat committed
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414

            this._isSliding = false

            setTimeout(() => $(this._element).trigger(slidEvent), 0)
          })
          .emulateTransitionEnd(TRANSITION_DURATION)
      } 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
415
    // Static
fat's avatar
fat committed
416
417
418

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

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

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

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

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

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

      if (!selector) {
        return
      }

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

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

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

fat's avatar
fat committed
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
      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
493
    .on(Event.CLICK_DATA_API, Selector.DATA_SLIDE, Carousel._dataApiClickHandler)
fat's avatar
fat committed
494

Jacob Thornton's avatar
Jacob Thornton committed
495
  $(window).on(Event.LOAD_DATA_API, () => {
fat's avatar
fat committed
496
    $(Selector.DATA_RIDE).each(function () {
497
      const $carousel = $(this)
fat's avatar
fat committed
498
499
500
501
502
503
504
505
506
507
      Carousel._jQueryInterface.call($carousel, $carousel.data())
    })
  })

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

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

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

export default Carousel