talos

Форк
0
600 строк · 15.6 Кб
1
/**
2
 * --------------------------------------------------------------------------
3
 * Bootstrap (v4.6.1): carousel.js
4
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
5
 * --------------------------------------------------------------------------
6
 */
7

8
import $ from 'jquery'
9
import Util from './util'
10

11
/**
12
 * Constants
13
 */
14

15
const NAME = 'carousel'
16
const VERSION = '4.6.1'
17
const DATA_KEY = 'bs.carousel'
18
const EVENT_KEY = `.${DATA_KEY}`
19
const DATA_API_KEY = '.data-api'
20
const JQUERY_NO_CONFLICT = $.fn[NAME]
21
const ARROW_LEFT_KEYCODE = 37 // KeyboardEvent.which value for left arrow key
22
const ARROW_RIGHT_KEYCODE = 39 // KeyboardEvent.which value for right arrow key
23
const TOUCHEVENT_COMPAT_WAIT = 500 // Time for mouse compat events to fire after touch
24
const SWIPE_THRESHOLD = 40
25

26
const CLASS_NAME_CAROUSEL = 'carousel'
27
const CLASS_NAME_ACTIVE = 'active'
28
const CLASS_NAME_SLIDE = 'slide'
29
const CLASS_NAME_RIGHT = 'carousel-item-right'
30
const CLASS_NAME_LEFT = 'carousel-item-left'
31
const CLASS_NAME_NEXT = 'carousel-item-next'
32
const CLASS_NAME_PREV = 'carousel-item-prev'
33
const CLASS_NAME_POINTER_EVENT = 'pointer-event'
34

35
const DIRECTION_NEXT = 'next'
36
const DIRECTION_PREV = 'prev'
37
const DIRECTION_LEFT = 'left'
38
const DIRECTION_RIGHT = 'right'
39

40
const EVENT_SLIDE = `slide${EVENT_KEY}`
41
const EVENT_SLID = `slid${EVENT_KEY}`
42
const EVENT_KEYDOWN = `keydown${EVENT_KEY}`
43
const EVENT_MOUSEENTER = `mouseenter${EVENT_KEY}`
44
const EVENT_MOUSELEAVE = `mouseleave${EVENT_KEY}`
45
const EVENT_TOUCHSTART = `touchstart${EVENT_KEY}`
46
const EVENT_TOUCHMOVE = `touchmove${EVENT_KEY}`
47
const EVENT_TOUCHEND = `touchend${EVENT_KEY}`
48
const EVENT_POINTERDOWN = `pointerdown${EVENT_KEY}`
49
const EVENT_POINTERUP = `pointerup${EVENT_KEY}`
50
const EVENT_DRAG_START = `dragstart${EVENT_KEY}`
51
const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`
52
const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
53

54
const SELECTOR_ACTIVE = '.active'
55
const SELECTOR_ACTIVE_ITEM = '.active.carousel-item'
56
const SELECTOR_ITEM = '.carousel-item'
57
const SELECTOR_ITEM_IMG = '.carousel-item img'
58
const SELECTOR_NEXT_PREV = '.carousel-item-next, .carousel-item-prev'
59
const SELECTOR_INDICATORS = '.carousel-indicators'
60
const SELECTOR_DATA_SLIDE = '[data-slide], [data-slide-to]'
61
const SELECTOR_DATA_RIDE = '[data-ride="carousel"]'
62

63
const Default = {
64
  interval: 5000,
65
  keyboard: true,
66
  slide: false,
67
  pause: 'hover',
68
  wrap: true,
69
  touch: true
70
}
71

72
const DefaultType = {
73
  interval: '(number|boolean)',
74
  keyboard: 'boolean',
75
  slide: '(boolean|string)',
76
  pause: '(string|boolean)',
77
  wrap: 'boolean',
78
  touch: 'boolean'
79
}
80

81
const PointerType = {
82
  TOUCH: 'touch',
83
  PEN: 'pen'
84
}
85

86
/**
87
 * Class definition
88
 */
89

90
class Carousel {
91
  constructor(element, config) {
92
    this._items = null
93
    this._interval = null
94
    this._activeElement = null
95
    this._isPaused = false
96
    this._isSliding = false
97
    this.touchTimeout = null
98
    this.touchStartX = 0
99
    this.touchDeltaX = 0
100

101
    this._config = this._getConfig(config)
102
    this._element = element
103
    this._indicatorsElement = this._element.querySelector(SELECTOR_INDICATORS)
104
    this._touchSupported = 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0
105
    this._pointerEvent = Boolean(window.PointerEvent || window.MSPointerEvent)
106

107
    this._addEventListeners()
108
  }
109

110
  // Getters
111
  static get VERSION() {
112
    return VERSION
113
  }
114

115
  static get Default() {
116
    return Default
117
  }
118

119
  // Public
120
  next() {
121
    if (!this._isSliding) {
122
      this._slide(DIRECTION_NEXT)
123
    }
124
  }
125

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

136
  prev() {
137
    if (!this._isSliding) {
138
      this._slide(DIRECTION_PREV)
139
    }
140
  }
141

142
  pause(event) {
143
    if (!event) {
144
      this._isPaused = true
145
    }
146

147
    if (this._element.querySelector(SELECTOR_NEXT_PREV)) {
148
      Util.triggerTransitionEnd(this._element)
149
      this.cycle(true)
150
    }
151

152
    clearInterval(this._interval)
153
    this._interval = null
154
  }
155

156
  cycle(event) {
157
    if (!event) {
158
      this._isPaused = false
159
    }
160

161
    if (this._interval) {
162
      clearInterval(this._interval)
163
      this._interval = null
164
    }
165

166
    if (this._config.interval && !this._isPaused) {
167
      this._updateInterval()
168

169
      this._interval = setInterval(
170
        (document.visibilityState ? this.nextWhenVisible : this.next).bind(this),
171
        this._config.interval
172
      )
173
    }
174
  }
175

176
  to(index) {
177
    this._activeElement = this._element.querySelector(SELECTOR_ACTIVE_ITEM)
178

179
    const activeIndex = this._getItemIndex(this._activeElement)
180

181
    if (index > this._items.length - 1 || index < 0) {
182
      return
183
    }
184

185
    if (this._isSliding) {
186
      $(this._element).one(EVENT_SLID, () => this.to(index))
187
      return
188
    }
189

190
    if (activeIndex === index) {
191
      this.pause()
192
      this.cycle()
193
      return
194
    }
195

196
    const direction = index > activeIndex ?
197
      DIRECTION_NEXT :
198
      DIRECTION_PREV
199

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

203
  dispose() {
204
    $(this._element).off(EVENT_KEY)
205
    $.removeData(this._element, DATA_KEY)
206

207
    this._items = null
208
    this._config = null
209
    this._element = null
210
    this._interval = null
211
    this._isPaused = null
212
    this._isSliding = null
213
    this._activeElement = null
214
    this._indicatorsElement = null
215
  }
216

217
  // Private
218
  _getConfig(config) {
219
    config = {
220
      ...Default,
221
      ...config
222
    }
223
    Util.typeCheckConfig(NAME, config, DefaultType)
224
    return config
225
  }
226

227
  _handleSwipe() {
228
    const absDeltax = Math.abs(this.touchDeltaX)
229

230
    if (absDeltax <= SWIPE_THRESHOLD) {
231
      return
232
    }
233

234
    const direction = absDeltax / this.touchDeltaX
235

236
    this.touchDeltaX = 0
237

238
    // swipe left
239
    if (direction > 0) {
240
      this.prev()
241
    }
242

243
    // swipe right
244
    if (direction < 0) {
245
      this.next()
246
    }
247
  }
248

249
  _addEventListeners() {
250
    if (this._config.keyboard) {
251
      $(this._element).on(EVENT_KEYDOWN, event => this._keydown(event))
252
    }
253

254
    if (this._config.pause === 'hover') {
255
      $(this._element)
256
        .on(EVENT_MOUSEENTER, event => this.pause(event))
257
        .on(EVENT_MOUSELEAVE, event => this.cycle(event))
258
    }
259

260
    if (this._config.touch) {
261
      this._addTouchEventListeners()
262
    }
263
  }
264

265
  _addTouchEventListeners() {
266
    if (!this._touchSupported) {
267
      return
268
    }
269

270
    const start = event => {
271
      if (this._pointerEvent && PointerType[event.originalEvent.pointerType.toUpperCase()]) {
272
        this.touchStartX = event.originalEvent.clientX
273
      } else if (!this._pointerEvent) {
274
        this.touchStartX = event.originalEvent.touches[0].clientX
275
      }
276
    }
277

278
    const move = event => {
279
      // ensure swiping with one touch and not pinching
280
      this.touchDeltaX = event.originalEvent.touches && event.originalEvent.touches.length > 1 ?
281
        0 :
282
        event.originalEvent.touches[0].clientX - this.touchStartX
283
    }
284

285
    const end = event => {
286
      if (this._pointerEvent && PointerType[event.originalEvent.pointerType.toUpperCase()]) {
287
        this.touchDeltaX = event.originalEvent.clientX - this.touchStartX
288
      }
289

290
      this._handleSwipe()
291
      if (this._config.pause === 'hover') {
292
        // If it's a touch-enabled device, mouseenter/leave are fired as
293
        // part of the mouse compatibility events on first tap - the carousel
294
        // would stop cycling until user tapped out of it;
295
        // here, we listen for touchend, explicitly pause the carousel
296
        // (as if it's the second time we tap on it, mouseenter compat event
297
        // is NOT fired) and after a timeout (to allow for mouse compatibility
298
        // events to fire) we explicitly restart cycling
299

300
        this.pause()
301
        if (this.touchTimeout) {
302
          clearTimeout(this.touchTimeout)
303
        }
304

305
        this.touchTimeout = setTimeout(event => this.cycle(event), TOUCHEVENT_COMPAT_WAIT + this._config.interval)
306
      }
307
    }
308

309
    $(this._element.querySelectorAll(SELECTOR_ITEM_IMG))
310
      .on(EVENT_DRAG_START, e => e.preventDefault())
311

312
    if (this._pointerEvent) {
313
      $(this._element).on(EVENT_POINTERDOWN, event => start(event))
314
      $(this._element).on(EVENT_POINTERUP, event => end(event))
315

316
      this._element.classList.add(CLASS_NAME_POINTER_EVENT)
317
    } else {
318
      $(this._element).on(EVENT_TOUCHSTART, event => start(event))
319
      $(this._element).on(EVENT_TOUCHMOVE, event => move(event))
320
      $(this._element).on(EVENT_TOUCHEND, event => end(event))
321
    }
322
  }
323

324
  _keydown(event) {
325
    if (/input|textarea/i.test(event.target.tagName)) {
326
      return
327
    }
328

329
    switch (event.which) {
330
      case ARROW_LEFT_KEYCODE:
331
        event.preventDefault()
332
        this.prev()
333
        break
334
      case ARROW_RIGHT_KEYCODE:
335
        event.preventDefault()
336
        this.next()
337
        break
338
      default:
339
    }
340
  }
341

342
  _getItemIndex(element) {
343
    this._items = element && element.parentNode ?
344
      [].slice.call(element.parentNode.querySelectorAll(SELECTOR_ITEM)) :
345
      []
346
    return this._items.indexOf(element)
347
  }
348

349
  _getItemByDirection(direction, activeElement) {
350
    const isNextDirection = direction === DIRECTION_NEXT
351
    const isPrevDirection = direction === DIRECTION_PREV
352
    const activeIndex = this._getItemIndex(activeElement)
353
    const lastItemIndex = this._items.length - 1
354
    const isGoingToWrap = isPrevDirection && activeIndex === 0 ||
355
                            isNextDirection && activeIndex === lastItemIndex
356

357
    if (isGoingToWrap && !this._config.wrap) {
358
      return activeElement
359
    }
360

361
    const delta = direction === DIRECTION_PREV ? -1 : 1
362
    const itemIndex = (activeIndex + delta) % this._items.length
363

364
    return itemIndex === -1 ?
365
      this._items[this._items.length - 1] : this._items[itemIndex]
366
  }
367

368
  _triggerSlideEvent(relatedTarget, eventDirectionName) {
369
    const targetIndex = this._getItemIndex(relatedTarget)
370
    const fromIndex = this._getItemIndex(this._element.querySelector(SELECTOR_ACTIVE_ITEM))
371
    const slideEvent = $.Event(EVENT_SLIDE, {
372
      relatedTarget,
373
      direction: eventDirectionName,
374
      from: fromIndex,
375
      to: targetIndex
376
    })
377

378
    $(this._element).trigger(slideEvent)
379

380
    return slideEvent
381
  }
382

383
  _setActiveIndicatorElement(element) {
384
    if (this._indicatorsElement) {
385
      const indicators = [].slice.call(this._indicatorsElement.querySelectorAll(SELECTOR_ACTIVE))
386
      $(indicators).removeClass(CLASS_NAME_ACTIVE)
387

388
      const nextIndicator = this._indicatorsElement.children[
389
        this._getItemIndex(element)
390
      ]
391

392
      if (nextIndicator) {
393
        $(nextIndicator).addClass(CLASS_NAME_ACTIVE)
394
      }
395
    }
396
  }
397

398
  _updateInterval() {
399
    const element = this._activeElement || this._element.querySelector(SELECTOR_ACTIVE_ITEM)
400

401
    if (!element) {
402
      return
403
    }
404

405
    const elementInterval = parseInt(element.getAttribute('data-interval'), 10)
406

407
    if (elementInterval) {
408
      this._config.defaultInterval = this._config.defaultInterval || this._config.interval
409
      this._config.interval = elementInterval
410
    } else {
411
      this._config.interval = this._config.defaultInterval || this._config.interval
412
    }
413
  }
414

415
  _slide(direction, element) {
416
    const activeElement = this._element.querySelector(SELECTOR_ACTIVE_ITEM)
417
    const activeElementIndex = this._getItemIndex(activeElement)
418
    const nextElement = element || activeElement &&
419
      this._getItemByDirection(direction, activeElement)
420
    const nextElementIndex = this._getItemIndex(nextElement)
421
    const isCycling = Boolean(this._interval)
422

423
    let directionalClassName
424
    let orderClassName
425
    let eventDirectionName
426

427
    if (direction === DIRECTION_NEXT) {
428
      directionalClassName = CLASS_NAME_LEFT
429
      orderClassName = CLASS_NAME_NEXT
430
      eventDirectionName = DIRECTION_LEFT
431
    } else {
432
      directionalClassName = CLASS_NAME_RIGHT
433
      orderClassName = CLASS_NAME_PREV
434
      eventDirectionName = DIRECTION_RIGHT
435
    }
436

437
    if (nextElement && $(nextElement).hasClass(CLASS_NAME_ACTIVE)) {
438
      this._isSliding = false
439
      return
440
    }
441

442
    const slideEvent = this._triggerSlideEvent(nextElement, eventDirectionName)
443
    if (slideEvent.isDefaultPrevented()) {
444
      return
445
    }
446

447
    if (!activeElement || !nextElement) {
448
      // Some weirdness is happening, so we bail
449
      return
450
    }
451

452
    this._isSliding = true
453

454
    if (isCycling) {
455
      this.pause()
456
    }
457

458
    this._setActiveIndicatorElement(nextElement)
459
    this._activeElement = nextElement
460

461
    const slidEvent = $.Event(EVENT_SLID, {
462
      relatedTarget: nextElement,
463
      direction: eventDirectionName,
464
      from: activeElementIndex,
465
      to: nextElementIndex
466
    })
467

468
    if ($(this._element).hasClass(CLASS_NAME_SLIDE)) {
469
      $(nextElement).addClass(orderClassName)
470

471
      Util.reflow(nextElement)
472

473
      $(activeElement).addClass(directionalClassName)
474
      $(nextElement).addClass(directionalClassName)
475

476
      const transitionDuration = Util.getTransitionDurationFromElement(activeElement)
477

478
      $(activeElement)
479
        .one(Util.TRANSITION_END, () => {
480
          $(nextElement)
481
            .removeClass(`${directionalClassName} ${orderClassName}`)
482
            .addClass(CLASS_NAME_ACTIVE)
483

484
          $(activeElement).removeClass(`${CLASS_NAME_ACTIVE} ${orderClassName} ${directionalClassName}`)
485

486
          this._isSliding = false
487

488
          setTimeout(() => $(this._element).trigger(slidEvent), 0)
489
        })
490
        .emulateTransitionEnd(transitionDuration)
491
    } else {
492
      $(activeElement).removeClass(CLASS_NAME_ACTIVE)
493
      $(nextElement).addClass(CLASS_NAME_ACTIVE)
494

495
      this._isSliding = false
496
      $(this._element).trigger(slidEvent)
497
    }
498

499
    if (isCycling) {
500
      this.cycle()
501
    }
502
  }
503

504
  // Static
505
  static _jQueryInterface(config) {
506
    return this.each(function () {
507
      let data = $(this).data(DATA_KEY)
508
      let _config = {
509
        ...Default,
510
        ...$(this).data()
511
      }
512

513
      if (typeof config === 'object') {
514
        _config = {
515
          ..._config,
516
          ...config
517
        }
518
      }
519

520
      const action = typeof config === 'string' ? config : _config.slide
521

522
      if (!data) {
523
        data = new Carousel(this, _config)
524
        $(this).data(DATA_KEY, data)
525
      }
526

527
      if (typeof config === 'number') {
528
        data.to(config)
529
      } else if (typeof action === 'string') {
530
        if (typeof data[action] === 'undefined') {
531
          throw new TypeError(`No method named "${action}"`)
532
        }
533

534
        data[action]()
535
      } else if (_config.interval && _config.ride) {
536
        data.pause()
537
        data.cycle()
538
      }
539
    })
540
  }
541

542
  static _dataApiClickHandler(event) {
543
    const selector = Util.getSelectorFromElement(this)
544

545
    if (!selector) {
546
      return
547
    }
548

549
    const target = $(selector)[0]
550

551
    if (!target || !$(target).hasClass(CLASS_NAME_CAROUSEL)) {
552
      return
553
    }
554

555
    const config = {
556
      ...$(target).data(),
557
      ...$(this).data()
558
    }
559
    const slideIndex = this.getAttribute('data-slide-to')
560

561
    if (slideIndex) {
562
      config.interval = false
563
    }
564

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

567
    if (slideIndex) {
568
      $(target).data(DATA_KEY).to(slideIndex)
569
    }
570

571
    event.preventDefault()
572
  }
573
}
574

575
/**
576
 * Data API implementation
577
 */
578

579
$(document).on(EVENT_CLICK_DATA_API, SELECTOR_DATA_SLIDE, Carousel._dataApiClickHandler)
580

581
$(window).on(EVENT_LOAD_DATA_API, () => {
582
  const carousels = [].slice.call(document.querySelectorAll(SELECTOR_DATA_RIDE))
583
  for (let i = 0, len = carousels.length; i < len; i++) {
584
    const $carousel = $(carousels[i])
585
    Carousel._jQueryInterface.call($carousel, $carousel.data())
586
  }
587
})
588

589
/**
590
 * jQuery
591
 */
592

593
$.fn[NAME] = Carousel._jQueryInterface
594
$.fn[NAME].Constructor = Carousel
595
$.fn[NAME].noConflict = () => {
596
  $.fn[NAME] = JQUERY_NO_CONFLICT
597
  return Carousel._jQueryInterface
598
}
599

600
export default Carousel
601

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.