Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
/**
2
 * --------------------------------------------------------------------------
3
 * Bootstrap (v4.6.2): 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.2'
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
 
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