source: imaps-frontend/node_modules/bootstrap/js/src/carousel.js

main
Last change on this file was d565449, checked in by stefan toskovski <stefantoska84@…>, 4 weeks ago

Update repo after prototype presentation

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