Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
/**
2
 * --------------------------------------------------------------------------
1441 ariadna 3
 * Bootstrap carousel.js
1 efrain 4
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
5
 * --------------------------------------------------------------------------
6
 */
7
 
1441 ariadna 8
import BaseComponent from './base-component'
9
import EventHandler from './dom/event-handler'
10
import Manipulator from './dom/manipulator'
11
import SelectorEngine from './dom/selector-engine'
12
import {
13
  defineJQueryPlugin,
14
  getNextActiveElement,
15
  isRTL,
16
  isVisible,
17
  reflow,
18
  triggerTransitionEnd
19
} from './util/index'
20
import Swipe from './util/swipe'
1 efrain 21
 
22
/**
23
 * Constants
24
 */
25
 
26
const NAME = 'carousel'
27
const DATA_KEY = 'bs.carousel'
28
const EVENT_KEY = `.${DATA_KEY}`
29
const DATA_API_KEY = '.data-api'
1441 ariadna 30
 
31
const ARROW_LEFT_KEY = 'ArrowLeft'
32
const ARROW_RIGHT_KEY = 'ArrowRight'
1 efrain 33
const TOUCHEVENT_COMPAT_WAIT = 500 // Time for mouse compat events to fire after touch
34
 
1441 ariadna 35
const ORDER_NEXT = 'next'
36
const ORDER_PREV = 'prev'
1 efrain 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_DRAG_START = `dragstart${EVENT_KEY}`
46
const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`
47
const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
48
 
1441 ariadna 49
const CLASS_NAME_CAROUSEL = 'carousel'
50
const CLASS_NAME_ACTIVE = 'active'
51
const CLASS_NAME_SLIDE = 'slide'
52
const CLASS_NAME_END = 'carousel-item-end'
53
const CLASS_NAME_START = 'carousel-item-start'
54
const CLASS_NAME_NEXT = 'carousel-item-next'
55
const CLASS_NAME_PREV = 'carousel-item-prev'
56
 
1 efrain 57
const SELECTOR_ACTIVE = '.active'
58
const SELECTOR_ITEM = '.carousel-item'
1441 ariadna 59
const SELECTOR_ACTIVE_ITEM = SELECTOR_ACTIVE + SELECTOR_ITEM
1 efrain 60
const SELECTOR_ITEM_IMG = '.carousel-item img'
61
const SELECTOR_INDICATORS = '.carousel-indicators'
1441 ariadna 62
const SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]'
63
const SELECTOR_DATA_RIDE = '[data-bs-ride="carousel"]'
1 efrain 64
 
1441 ariadna 65
const KEY_TO_DIRECTION = {
66
  [ARROW_LEFT_KEY]: DIRECTION_RIGHT,
67
  [ARROW_RIGHT_KEY]: DIRECTION_LEFT
68
}
69
 
1 efrain 70
const Default = {
71
  interval: 5000,
72
  keyboard: true,
73
  pause: 'hover',
1441 ariadna 74
  ride: false,
75
  touch: true,
76
  wrap: true
1 efrain 77
}
78
 
79
const DefaultType = {
1441 ariadna 80
  interval: '(number|boolean)', // TODO:v6 remove boolean support
1 efrain 81
  keyboard: 'boolean',
82
  pause: '(string|boolean)',
1441 ariadna 83
  ride: '(boolean|string)',
84
  touch: 'boolean',
85
  wrap: 'boolean'
1 efrain 86
}
87
 
88
/**
89
 * Class definition
90
 */
91
 
1441 ariadna 92
class Carousel extends BaseComponent {
1 efrain 93
  constructor(element, config) {
1441 ariadna 94
    super(element, config)
95
 
1 efrain 96
    this._interval = null
97
    this._activeElement = null
98
    this._isSliding = false
99
    this.touchTimeout = null
1441 ariadna 100
    this._swipeHelper = null
1 efrain 101
 
1441 ariadna 102
    this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element)
103
    this._addEventListeners()
1 efrain 104
 
1441 ariadna 105
    if (this._config.ride === CLASS_NAME_CAROUSEL) {
106
      this.cycle()
107
    }
1 efrain 108
  }
109
 
110
  // Getters
111
  static get Default() {
112
    return Default
113
  }
114
 
1441 ariadna 115
  static get DefaultType() {
116
    return DefaultType
117
  }
118
 
119
  static get NAME() {
120
    return NAME
121
  }
122
 
1 efrain 123
  // Public
124
  next() {
1441 ariadna 125
    this._slide(ORDER_NEXT)
1 efrain 126
  }
127
 
128
  nextWhenVisible() {
1441 ariadna 129
    // FIXME TODO use `document.visibilityState`
1 efrain 130
    // Don't call next when the page isn't visible
131
    // or the carousel or its parent isn't visible
1441 ariadna 132
    if (!document.hidden && isVisible(this._element)) {
1 efrain 133
      this.next()
134
    }
135
  }
136
 
137
  prev() {
1441 ariadna 138
    this._slide(ORDER_PREV)
1 efrain 139
  }
140
 
1441 ariadna 141
  pause() {
142
    if (this._isSliding) {
143
      triggerTransitionEnd(this._element)
1 efrain 144
    }
145
 
1441 ariadna 146
    this._clearInterval()
147
  }
1 efrain 148
 
1441 ariadna 149
  cycle() {
150
    this._clearInterval()
151
    this._updateInterval()
152
 
153
    this._interval = setInterval(() => this.nextWhenVisible(), this._config.interval)
1 efrain 154
  }
155
 
1441 ariadna 156
  _maybeEnableCycle() {
157
    if (!this._config.ride) {
158
      return
1 efrain 159
    }
160
 
1441 ariadna 161
    if (this._isSliding) {
162
      EventHandler.one(this._element, EVENT_SLID, () => this.cycle())
163
      return
1 efrain 164
    }
165
 
1441 ariadna 166
    this.cycle()
1 efrain 167
  }
168
 
169
  to(index) {
1441 ariadna 170
    const items = this._getItems()
171
    if (index > items.length - 1 || index < 0) {
1 efrain 172
      return
173
    }
174
 
175
    if (this._isSliding) {
1441 ariadna 176
      EventHandler.one(this._element, EVENT_SLID, () => this.to(index))
1 efrain 177
      return
178
    }
179
 
1441 ariadna 180
    const activeIndex = this._getItemIndex(this._getActive())
1 efrain 181
    if (activeIndex === index) {
182
      return
183
    }
184
 
1441 ariadna 185
    const order = index > activeIndex ? ORDER_NEXT : ORDER_PREV
1 efrain 186
 
1441 ariadna 187
    this._slide(order, items[index])
1 efrain 188
  }
189
 
190
  dispose() {
1441 ariadna 191
    if (this._swipeHelper) {
192
      this._swipeHelper.dispose()
193
    }
1 efrain 194
 
1441 ariadna 195
    super.dispose()
1 efrain 196
  }
197
 
198
  // Private
1441 ariadna 199
  _configAfterMerge(config) {
200
    config.defaultInterval = config.interval
1 efrain 201
    return config
202
  }
203
 
204
  _addEventListeners() {
205
    if (this._config.keyboard) {
1441 ariadna 206
      EventHandler.on(this._element, EVENT_KEYDOWN, event => this._keydown(event))
1 efrain 207
    }
208
 
209
    if (this._config.pause === 'hover') {
1441 ariadna 210
      EventHandler.on(this._element, EVENT_MOUSEENTER, () => this.pause())
211
      EventHandler.on(this._element, EVENT_MOUSELEAVE, () => this._maybeEnableCycle())
1 efrain 212
    }
213
 
1441 ariadna 214
    if (this._config.touch && Swipe.isSupported()) {
1 efrain 215
      this._addTouchEventListeners()
216
    }
217
  }
218
 
219
  _addTouchEventListeners() {
1441 ariadna 220
    for (const img of SelectorEngine.find(SELECTOR_ITEM_IMG, this._element)) {
221
      EventHandler.on(img, EVENT_DRAG_START, event => event.preventDefault())
1 efrain 222
    }
223
 
1441 ariadna 224
    const endCallBack = () => {
225
      if (this._config.pause !== 'hover') {
226
        return
1 efrain 227
      }
228
 
1441 ariadna 229
      // If it's a touch-enabled device, mouseenter/leave are fired as
230
      // part of the mouse compatibility events on first tap - the carousel
231
      // would stop cycling until user tapped out of it;
232
      // here, we listen for touchend, explicitly pause the carousel
233
      // (as if it's the second time we tap on it, mouseenter compat event
234
      // is NOT fired) and after a timeout (to allow for mouse compatibility
235
      // events to fire) we explicitly restart cycling
1 efrain 236
 
1441 ariadna 237
      this.pause()
238
      if (this.touchTimeout) {
239
        clearTimeout(this.touchTimeout)
1 efrain 240
      }
241
 
1441 ariadna 242
      this.touchTimeout = setTimeout(() => this._maybeEnableCycle(), TOUCHEVENT_COMPAT_WAIT + this._config.interval)
243
    }
1 efrain 244
 
1441 ariadna 245
    const swipeConfig = {
246
      leftCallback: () => this._slide(this._directionToOrder(DIRECTION_LEFT)),
247
      rightCallback: () => this._slide(this._directionToOrder(DIRECTION_RIGHT)),
248
      endCallback: endCallBack
1 efrain 249
    }
250
 
1441 ariadna 251
    this._swipeHelper = new Swipe(this._element, swipeConfig)
1 efrain 252
  }
253
 
254
  _keydown(event) {
255
    if (/input|textarea/i.test(event.target.tagName)) {
256
      return
257
    }
258
 
1441 ariadna 259
    const direction = KEY_TO_DIRECTION[event.key]
260
    if (direction) {
261
      event.preventDefault()
262
      this._slide(this._directionToOrder(direction))
1 efrain 263
    }
264
  }
265
 
266
  _getItemIndex(element) {
1441 ariadna 267
    return this._getItems().indexOf(element)
1 efrain 268
  }
269
 
1441 ariadna 270
  _setActiveIndicatorElement(index) {
271
    if (!this._indicatorsElement) {
272
      return
1 efrain 273
    }
274
 
1441 ariadna 275
    const activeIndicator = SelectorEngine.findOne(SELECTOR_ACTIVE, this._indicatorsElement)
1 efrain 276
 
1441 ariadna 277
    activeIndicator.classList.remove(CLASS_NAME_ACTIVE)
278
    activeIndicator.removeAttribute('aria-current')
1 efrain 279
 
1441 ariadna 280
    const newActiveIndicator = SelectorEngine.findOne(`[data-bs-slide-to="${index}"]`, this._indicatorsElement)
1 efrain 281
 
1441 ariadna 282
    if (newActiveIndicator) {
283
      newActiveIndicator.classList.add(CLASS_NAME_ACTIVE)
284
      newActiveIndicator.setAttribute('aria-current', 'true')
1 efrain 285
    }
286
  }
287
 
288
  _updateInterval() {
1441 ariadna 289
    const element = this._activeElement || this._getActive()
1 efrain 290
 
291
    if (!element) {
292
      return
293
    }
294
 
1441 ariadna 295
    const elementInterval = Number.parseInt(element.getAttribute('data-bs-interval'), 10)
1 efrain 296
 
1441 ariadna 297
    this._config.interval = elementInterval || this._config.defaultInterval
1 efrain 298
  }
299
 
1441 ariadna 300
  _slide(order, element = null) {
301
    if (this._isSliding) {
302
      return
303
    }
1 efrain 304
 
1441 ariadna 305
    const activeElement = this._getActive()
306
    const isNext = order === ORDER_NEXT
307
    const nextElement = element || getNextActiveElement(this._getItems(), activeElement, isNext, this._config.wrap)
1 efrain 308
 
1441 ariadna 309
    if (nextElement === activeElement) {
310
      return
1 efrain 311
    }
312
 
1441 ariadna 313
    const nextElementIndex = this._getItemIndex(nextElement)
314
 
315
    const triggerEvent = eventName => {
316
      return EventHandler.trigger(this._element, eventName, {
317
        relatedTarget: nextElement,
318
        direction: this._orderToDirection(order),
319
        from: this._getItemIndex(activeElement),
320
        to: nextElementIndex
321
      })
1 efrain 322
    }
323
 
1441 ariadna 324
    const slideEvent = triggerEvent(EVENT_SLIDE)
325
 
326
    if (slideEvent.defaultPrevented) {
1 efrain 327
      return
328
    }
329
 
330
    if (!activeElement || !nextElement) {
331
      // Some weirdness is happening, so we bail
1441 ariadna 332
      // TODO: change tests that use empty divs to avoid this check
1 efrain 333
      return
334
    }
335
 
1441 ariadna 336
    const isCycling = Boolean(this._interval)
337
    this.pause()
338
 
1 efrain 339
    this._isSliding = true
340
 
1441 ariadna 341
    this._setActiveIndicatorElement(nextElementIndex)
1 efrain 342
    this._activeElement = nextElement
343
 
1441 ariadna 344
    const directionalClassName = isNext ? CLASS_NAME_START : CLASS_NAME_END
345
    const orderClassName = isNext ? CLASS_NAME_NEXT : CLASS_NAME_PREV
1 efrain 346
 
1441 ariadna 347
    nextElement.classList.add(orderClassName)
1 efrain 348
 
1441 ariadna 349
    reflow(nextElement)
1 efrain 350
 
1441 ariadna 351
    activeElement.classList.add(directionalClassName)
352
    nextElement.classList.add(directionalClassName)
1 efrain 353
 
1441 ariadna 354
    const completeCallBack = () => {
355
      nextElement.classList.remove(directionalClassName, orderClassName)
356
      nextElement.classList.add(CLASS_NAME_ACTIVE)
1 efrain 357
 
1441 ariadna 358
      activeElement.classList.remove(CLASS_NAME_ACTIVE, orderClassName, directionalClassName)
1 efrain 359
 
1441 ariadna 360
      this._isSliding = false
1 efrain 361
 
1441 ariadna 362
      triggerEvent(EVENT_SLID)
363
    }
1 efrain 364
 
1441 ariadna 365
    this._queueCallback(completeCallBack, activeElement, this._isAnimated())
1 efrain 366
 
367
    if (isCycling) {
368
      this.cycle()
369
    }
370
  }
371
 
1441 ariadna 372
  _isAnimated() {
373
    return this._element.classList.contains(CLASS_NAME_SLIDE)
374
  }
1 efrain 375
 
1441 ariadna 376
  _getActive() {
377
    return SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element)
378
  }
1 efrain 379
 
1441 ariadna 380
  _getItems() {
381
    return SelectorEngine.find(SELECTOR_ITEM, this._element)
382
  }
1 efrain 383
 
1441 ariadna 384
  _clearInterval() {
385
    if (this._interval) {
386
      clearInterval(this._interval)
387
      this._interval = null
388
    }
1 efrain 389
  }
390
 
1441 ariadna 391
  _directionToOrder(direction) {
392
    if (isRTL()) {
393
      return direction === DIRECTION_LEFT ? ORDER_PREV : ORDER_NEXT
1 efrain 394
    }
395
 
1441 ariadna 396
    return direction === DIRECTION_LEFT ? ORDER_NEXT : ORDER_PREV
397
  }
1 efrain 398
 
1441 ariadna 399
  _orderToDirection(order) {
400
    if (isRTL()) {
401
      return order === ORDER_PREV ? DIRECTION_LEFT : DIRECTION_RIGHT
1 efrain 402
    }
403
 
1441 ariadna 404
    return order === ORDER_PREV ? DIRECTION_RIGHT : DIRECTION_LEFT
405
  }
1 efrain 406
 
1441 ariadna 407
  // Static
408
  static jQueryInterface(config) {
409
    return this.each(function () {
410
      const data = Carousel.getOrCreateInstance(this, config)
1 efrain 411
 
1441 ariadna 412
      if (typeof config === 'number') {
413
        data.to(config)
414
        return
415
      }
1 efrain 416
 
1441 ariadna 417
      if (typeof config === 'string') {
418
        if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {
419
          throw new TypeError(`No method named "${config}"`)
420
        }
1 efrain 421
 
1441 ariadna 422
        data[config]()
423
      }
424
    })
1 efrain 425
  }
426
}
427
 
428
/**
429
 * Data API implementation
430
 */
431
 
1441 ariadna 432
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_SLIDE, function (event) {
433
  const target = SelectorEngine.getElementFromSelector(this)
1 efrain 434
 
1441 ariadna 435
  if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) {
436
    return
1 efrain 437
  }
1441 ariadna 438
 
439
  event.preventDefault()
440
 
441
  const carousel = Carousel.getOrCreateInstance(target)
442
  const slideIndex = this.getAttribute('data-bs-slide-to')
443
 
444
  if (slideIndex) {
445
    carousel.to(slideIndex)
446
    carousel._maybeEnableCycle()
447
    return
448
  }
449
 
450
  if (Manipulator.getDataAttribute(this, 'slide') === 'next') {
451
    carousel.next()
452
    carousel._maybeEnableCycle()
453
    return
454
  }
455
 
456
  carousel.prev()
457
  carousel._maybeEnableCycle()
1 efrain 458
})
459
 
1441 ariadna 460
EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
461
  const carousels = SelectorEngine.find(SELECTOR_DATA_RIDE)
462
 
463
  for (const carousel of carousels) {
464
    Carousel.getOrCreateInstance(carousel)
465
  }
466
})
467
 
1 efrain 468
/**
469
 * jQuery
470
 */
471
 
1441 ariadna 472
defineJQueryPlugin(Carousel)
1 efrain 473
 
474
export default Carousel