aboutsummaryrefslogtreecommitdiff
path: root/js/src/carousel.js
diff options
context:
space:
mode:
authorBobby <[email protected]>2024-08-16 20:47:33 -0400
committerGitHub <[email protected]>2024-08-16 20:47:33 -0400
commit6b28433d9cfde435be8ec2bd6cf91e6324d08865 (patch)
tree8343c27b8b95ff5639233e81cf157f92e5688466 /js/src/carousel.js
parentd53094ec16ba385faae2973ddee648698b32ab24 (diff)
parent048f56f51460df75e92a2f7b472e1c56baeb68f7 (diff)
downloadbootstrap-main.tar.xz
bootstrap-main.zip
Merge branch 'twbs:main' into mainHEADmain
Diffstat (limited to 'js/src/carousel.js')
-rw-r--r--js/src/carousel.js412
1 files changed, 179 insertions, 233 deletions
diff --git a/js/src/carousel.js b/js/src/carousel.js
index 3589f2206..68d11a32f 100644
--- a/js/src/carousel.js
+++ b/js/src/carousel.js
@@ -1,25 +1,23 @@
/**
* --------------------------------------------------------------------------
- * Bootstrap (v5.1.3): carousel.js
+ * Bootstrap carousel.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
+import BaseComponent from './base-component.js'
+import EventHandler from './dom/event-handler.js'
+import Manipulator from './dom/manipulator.js'
+import SelectorEngine from './dom/selector-engine.js'
import {
defineJQueryPlugin,
- getElementFromSelector,
getNextActiveElement,
isRTL,
isVisible,
reflow,
- triggerTransitionEnd,
- typeCheckConfig
-} from './util/index'
-import EventHandler from './dom/event-handler'
-import Manipulator from './dom/manipulator'
-import SelectorEngine from './dom/selector-engine'
-import Swipe from './util/swipe'
-import BaseComponent from './base-component'
+ triggerTransitionEnd
+} from './util/index.js'
+import Swipe from './util/swipe.js'
/**
* Constants
@@ -57,12 +55,10 @@ const CLASS_NAME_NEXT = 'carousel-item-next'
const CLASS_NAME_PREV = 'carousel-item-prev'
const SELECTOR_ACTIVE = '.active'
-const SELECTOR_ACTIVE_ITEM = '.active.carousel-item'
const SELECTOR_ITEM = '.carousel-item'
+const SELECTOR_ACTIVE_ITEM = SELECTOR_ACTIVE + SELECTOR_ITEM
const SELECTOR_ITEM_IMG = '.carousel-item img'
-const SELECTOR_NEXT_PREV = '.carousel-item-next, .carousel-item-prev'
const SELECTOR_INDICATORS = '.carousel-indicators'
-const SELECTOR_INDICATOR = '[data-bs-target]'
const SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]'
const SELECTOR_DATA_RIDE = '[data-bs-ride="carousel"]'
@@ -74,19 +70,19 @@ const KEY_TO_DIRECTION = {
const Default = {
interval: 5000,
keyboard: true,
- slide: false,
pause: 'hover',
- wrap: true,
- touch: true
+ ride: false,
+ touch: true,
+ wrap: true
}
const DefaultType = {
- interval: '(number|boolean)',
+ interval: '(number|boolean)', // TODO:v6 remove boolean support
keyboard: 'boolean',
- slide: '(boolean|string)',
pause: '(string|boolean)',
- wrap: 'boolean',
- touch: 'boolean'
+ ride: '(boolean|string)',
+ touch: 'boolean',
+ wrap: 'boolean'
}
/**
@@ -95,19 +91,20 @@ const DefaultType = {
class Carousel extends BaseComponent {
constructor(element, config) {
- super(element)
+ super(element, config)
- this._items = null
this._interval = null
this._activeElement = null
- this._isPaused = false
this._isSliding = false
this.touchTimeout = null
this._swipeHelper = null
- this._config = this._getConfig(config)
this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element)
this._addEventListeners()
+
+ if (this._config.ride === CLASS_NAME_CAROUSEL) {
+ this.cycle()
+ }
}
// Getters
@@ -115,6 +112,10 @@ class Carousel extends BaseComponent {
return Default
}
+ static get DefaultType() {
+ return DefaultType
+ }
+
static get NAME() {
return NAME
}
@@ -125,6 +126,7 @@ class Carousel extends BaseComponent {
}
nextWhenVisible() {
+ // FIXME TODO use `document.visibilityState`
// Don't call next when the page isn't visible
// or the carousel or its parent isn't visible
if (!document.hidden && isVisible(this._element)) {
@@ -136,45 +138,37 @@ class Carousel extends BaseComponent {
this._slide(ORDER_PREV)
}
- pause(event) {
- if (!event) {
- this._isPaused = true
- }
-
- if (SelectorEngine.findOne(SELECTOR_NEXT_PREV, this._element)) {
+ pause() {
+ if (this._isSliding) {
triggerTransitionEnd(this._element)
- this.cycle(true)
}
- clearInterval(this._interval)
- this._interval = null
+ this._clearInterval()
}
- cycle(event) {
- if (!event) {
- this._isPaused = false
- }
+ cycle() {
+ this._clearInterval()
+ this._updateInterval()
- if (this._interval) {
- clearInterval(this._interval)
- this._interval = null
- }
+ this._interval = setInterval(() => this.nextWhenVisible(), this._config.interval)
+ }
- if (this._config && this._config.interval && !this._isPaused) {
- this._updateInterval()
+ _maybeEnableCycle() {
+ if (!this._config.ride) {
+ return
+ }
- this._interval = setInterval(
- (document.visibilityState ? this.nextWhenVisible : this.next).bind(this),
- this._config.interval
- )
+ if (this._isSliding) {
+ EventHandler.one(this._element, EVENT_SLID, () => this.cycle())
+ return
}
+
+ this.cycle()
}
to(index) {
- this._activeElement = SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element)
- const activeIndex = this._getItemIndex(this._activeElement)
-
- if (index > this._items.length - 1 || index < 0) {
+ const items = this._getItems()
+ if (index > items.length - 1 || index < 0) {
return
}
@@ -183,17 +177,14 @@ class Carousel extends BaseComponent {
return
}
+ const activeIndex = this._getItemIndex(this._getActive())
if (activeIndex === index) {
- this.pause()
- this.cycle()
return
}
- const order = index > activeIndex ?
- ORDER_NEXT :
- ORDER_PREV
+ const order = index > activeIndex ? ORDER_NEXT : ORDER_PREV
- this._slide(order, this._items[index])
+ this._slide(order, items[index])
}
dispose() {
@@ -205,13 +196,8 @@ class Carousel extends BaseComponent {
}
// Private
- _getConfig(config) {
- config = {
- ...Default,
- ...Manipulator.getDataAttributes(this._element),
- ...(typeof config === 'object' ? config : {})
- }
- typeCheckConfig(NAME, config, DefaultType)
+ _configAfterMerge(config) {
+ config.defaultInterval = config.interval
return config
}
@@ -221,8 +207,8 @@ class Carousel extends BaseComponent {
}
if (this._config.pause === 'hover') {
- EventHandler.on(this._element, EVENT_MOUSEENTER, event => this.pause(event))
- EventHandler.on(this._element, EVENT_MOUSELEAVE, event => this.cycle(event))
+ EventHandler.on(this._element, EVENT_MOUSEENTER, () => this.pause())
+ EventHandler.on(this._element, EVENT_MOUSELEAVE, () => this._maybeEnableCycle())
}
if (this._config.touch && Swipe.isSupported()) {
@@ -231,32 +217,34 @@ class Carousel extends BaseComponent {
}
_addTouchEventListeners() {
- for (const itemImg of SelectorEngine.find(SELECTOR_ITEM_IMG, this._element)) {
- EventHandler.on(itemImg, EVENT_DRAG_START, event => event.preventDefault())
+ for (const img of SelectorEngine.find(SELECTOR_ITEM_IMG, this._element)) {
+ EventHandler.on(img, EVENT_DRAG_START, event => event.preventDefault())
}
const endCallBack = () => {
- if (this._config.pause === 'hover') {
- // If it's a touch-enabled device, mouseenter/leave are fired as
- // 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.pause()
- if (this.touchTimeout) {
- clearTimeout(this.touchTimeout)
- }
+ if (this._config.pause !== 'hover') {
+ return
+ }
+
+ // If it's a touch-enabled device, mouseenter/leave are fired as
+ // 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.touchTimeout = setTimeout(event => this.cycle(event), TOUCHEVENT_COMPAT_WAIT + this._config.interval)
+ this.pause()
+ if (this.touchTimeout) {
+ clearTimeout(this.touchTimeout)
}
+
+ this.touchTimeout = setTimeout(() => this._maybeEnableCycle(), TOUCHEVENT_COMPAT_WAIT + this._config.interval)
}
const swipeConfig = {
- leftCallback: () => this._slide(DIRECTION_LEFT),
- rightCallback: () => this._slide(DIRECTION_RIGHT),
+ leftCallback: () => this._slide(this._directionToOrder(DIRECTION_LEFT)),
+ rightCallback: () => this._slide(this._directionToOrder(DIRECTION_RIGHT)),
endCallback: endCallBack
}
@@ -271,56 +259,34 @@ class Carousel extends BaseComponent {
const direction = KEY_TO_DIRECTION[event.key]
if (direction) {
event.preventDefault()
- this._slide(direction)
+ this._slide(this._directionToOrder(direction))
}
}
_getItemIndex(element) {
- this._items = element && element.parentNode ?
- SelectorEngine.find(SELECTOR_ITEM, element.parentNode) :
- []
-
- return this._items.indexOf(element)
+ return this._getItems().indexOf(element)
}
- _getItemByOrder(order, activeElement) {
- const isNext = order === ORDER_NEXT
- return getNextActiveElement(this._items, activeElement, isNext, this._config.wrap)
- }
-
- _triggerSlideEvent(relatedTarget, eventDirectionName) {
- const targetIndex = this._getItemIndex(relatedTarget)
- const fromIndex = this._getItemIndex(SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element))
-
- return EventHandler.trigger(this._element, EVENT_SLIDE, {
- relatedTarget,
- direction: eventDirectionName,
- from: fromIndex,
- to: targetIndex
- })
- }
+ _setActiveIndicatorElement(index) {
+ if (!this._indicatorsElement) {
+ return
+ }
- _setActiveIndicatorElement(element) {
- if (this._indicatorsElement) {
- const activeIndicator = SelectorEngine.findOne(SELECTOR_ACTIVE, this._indicatorsElement)
+ const activeIndicator = SelectorEngine.findOne(SELECTOR_ACTIVE, this._indicatorsElement)
- activeIndicator.classList.remove(CLASS_NAME_ACTIVE)
- activeIndicator.removeAttribute('aria-current')
+ activeIndicator.classList.remove(CLASS_NAME_ACTIVE)
+ activeIndicator.removeAttribute('aria-current')
- const indicators = SelectorEngine.find(SELECTOR_INDICATOR, this._indicatorsElement)
+ const newActiveIndicator = SelectorEngine.findOne(`[data-bs-slide-to="${index}"]`, this._indicatorsElement)
- for (const indicator of indicators) {
- if (Number.parseInt(indicator.getAttribute('data-bs-slide-to'), 10) === this._getItemIndex(element)) {
- indicator.classList.add(CLASS_NAME_ACTIVE)
- indicator.setAttribute('aria-current', 'true')
- break
- }
- }
+ if (newActiveIndicator) {
+ newActiveIndicator.classList.add(CLASS_NAME_ACTIVE)
+ newActiveIndicator.setAttribute('aria-current', 'true')
}
}
_updateInterval() {
- const element = this._activeElement || SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element)
+ const element = this._activeElement || this._getActive()
if (!element) {
return
@@ -328,103 +294,101 @@ class Carousel extends BaseComponent {
const elementInterval = Number.parseInt(element.getAttribute('data-bs-interval'), 10)
- if (elementInterval) {
- this._config.defaultInterval = this._config.defaultInterval || this._config.interval
- this._config.interval = elementInterval
- } else {
- this._config.interval = this._config.defaultInterval || this._config.interval
- }
+ this._config.interval = elementInterval || this._config.defaultInterval
}
- _slide(directionOrOrder, element) {
- const order = this._directionToOrder(directionOrOrder)
- const activeElement = SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element)
- const activeElementIndex = this._getItemIndex(activeElement)
- const nextElement = element || this._getItemByOrder(order, activeElement)
-
- const nextElementIndex = this._getItemIndex(nextElement)
- const isCycling = Boolean(this._interval)
+ _slide(order, element = null) {
+ if (this._isSliding) {
+ return
+ }
+ const activeElement = this._getActive()
const isNext = order === ORDER_NEXT
- const directionalClassName = isNext ? CLASS_NAME_START : CLASS_NAME_END
- const orderClassName = isNext ? CLASS_NAME_NEXT : CLASS_NAME_PREV
- const eventDirectionName = this._orderToDirection(order)
+ const nextElement = element || getNextActiveElement(this._getItems(), activeElement, isNext, this._config.wrap)
- if (nextElement && nextElement.classList.contains(CLASS_NAME_ACTIVE)) {
- this._isSliding = false
+ if (nextElement === activeElement) {
return
}
- if (this._isSliding) {
- return
+ const nextElementIndex = this._getItemIndex(nextElement)
+
+ const triggerEvent = eventName => {
+ return EventHandler.trigger(this._element, eventName, {
+ relatedTarget: nextElement,
+ direction: this._orderToDirection(order),
+ from: this._getItemIndex(activeElement),
+ to: nextElementIndex
+ })
}
- const slideEvent = this._triggerSlideEvent(nextElement, eventDirectionName)
+ const slideEvent = triggerEvent(EVENT_SLIDE)
+
if (slideEvent.defaultPrevented) {
return
}
if (!activeElement || !nextElement) {
// Some weirdness is happening, so we bail
+ // TODO: change tests that use empty divs to avoid this check
return
}
- this._isSliding = true
+ const isCycling = Boolean(this._interval)
+ this.pause()
- if (isCycling) {
- this.pause()
- }
+ this._isSliding = true
- this._setActiveIndicatorElement(nextElement)
+ this._setActiveIndicatorElement(nextElementIndex)
this._activeElement = nextElement
- const triggerSlidEvent = () => {
- EventHandler.trigger(this._element, EVENT_SLID, {
- relatedTarget: nextElement,
- direction: eventDirectionName,
- from: activeElementIndex,
- to: nextElementIndex
- })
- }
-
- if (this._element.classList.contains(CLASS_NAME_SLIDE)) {
- nextElement.classList.add(orderClassName)
-
- reflow(nextElement)
-
- activeElement.classList.add(directionalClassName)
- nextElement.classList.add(directionalClassName)
-
- const completeCallBack = () => {
- nextElement.classList.remove(directionalClassName, orderClassName)
- nextElement.classList.add(CLASS_NAME_ACTIVE)
+ const directionalClassName = isNext ? CLASS_NAME_START : CLASS_NAME_END
+ const orderClassName = isNext ? CLASS_NAME_NEXT : CLASS_NAME_PREV
- activeElement.classList.remove(CLASS_NAME_ACTIVE, orderClassName, directionalClassName)
+ nextElement.classList.add(orderClassName)
- this._isSliding = false
+ reflow(nextElement)
- setTimeout(triggerSlidEvent, 0)
- }
+ activeElement.classList.add(directionalClassName)
+ nextElement.classList.add(directionalClassName)
- this._queueCallback(completeCallBack, activeElement, true)
- } else {
- activeElement.classList.remove(CLASS_NAME_ACTIVE)
+ const completeCallBack = () => {
+ nextElement.classList.remove(directionalClassName, orderClassName)
nextElement.classList.add(CLASS_NAME_ACTIVE)
+ activeElement.classList.remove(CLASS_NAME_ACTIVE, orderClassName, directionalClassName)
+
this._isSliding = false
- triggerSlidEvent()
+
+ triggerEvent(EVENT_SLID)
}
+ this._queueCallback(completeCallBack, activeElement, this._isAnimated())
+
if (isCycling) {
this.cycle()
}
}
- _directionToOrder(direction) {
- if (![DIRECTION_RIGHT, DIRECTION_LEFT].includes(direction)) {
- return direction
+ _isAnimated() {
+ return this._element.classList.contains(CLASS_NAME_SLIDE)
+ }
+
+ _getActive() {
+ return SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element)
+ }
+
+ _getItems() {
+ return SelectorEngine.find(SELECTOR_ITEM, this._element)
+ }
+
+ _clearInterval() {
+ if (this._interval) {
+ clearInterval(this._interval)
+ this._interval = null
}
+ }
+ _directionToOrder(direction) {
if (isRTL()) {
return direction === DIRECTION_LEFT ? ORDER_PREV : ORDER_NEXT
}
@@ -433,10 +397,6 @@ class Carousel extends BaseComponent {
}
_orderToDirection(order) {
- if (![ORDER_NEXT, ORDER_PREV].includes(order)) {
- return order
- }
-
if (isRTL()) {
return order === ORDER_PREV ? DIRECTION_LEFT : DIRECTION_RIGHT
}
@@ -445,77 +405,63 @@ class Carousel extends BaseComponent {
}
// Static
- static carouselInterface(element, config) {
- const data = Carousel.getOrCreateInstance(element, config)
-
- let { _config } = data
- if (typeof config === 'object') {
- _config = {
- ..._config,
- ...config
- }
- }
-
- const action = typeof config === 'string' ? config : _config.slide
+ static jQueryInterface(config) {
+ return this.each(function () {
+ const data = Carousel.getOrCreateInstance(this, config)
- if (typeof config === 'number') {
- data.to(config)
- } else if (typeof action === 'string') {
- if (typeof data[action] === 'undefined') {
- throw new TypeError(`No method named "${action}"`)
+ if (typeof config === 'number') {
+ data.to(config)
+ return
}
- data[action]()
- } else if (_config.interval && _config.ride) {
- data.pause()
- data.cycle()
- }
- }
+ if (typeof config === 'string') {
+ if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {
+ throw new TypeError(`No method named "${config}"`)
+ }
- static jQueryInterface(config) {
- return this.each(function () {
- Carousel.carouselInterface(this, config)
+ data[config]()
+ }
})
}
+}
- static dataApiClickHandler(event) {
- const target = getElementFromSelector(this)
-
- if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) {
- return
- }
+/**
+ * Data API implementation
+ */
- const config = {
- ...Manipulator.getDataAttributes(target),
- ...Manipulator.getDataAttributes(this)
- }
- const slideIndex = this.getAttribute('data-bs-slide-to')
+EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_SLIDE, function (event) {
+ const target = SelectorEngine.getElementFromSelector(this)
- if (slideIndex) {
- config.interval = false
- }
+ if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) {
+ return
+ }
- Carousel.carouselInterface(target, config)
+ event.preventDefault()
- if (slideIndex) {
- Carousel.getInstance(target).to(slideIndex)
- }
+ const carousel = Carousel.getOrCreateInstance(target)
+ const slideIndex = this.getAttribute('data-bs-slide-to')
- event.preventDefault()
+ if (slideIndex) {
+ carousel.to(slideIndex)
+ carousel._maybeEnableCycle()
+ return
}
-}
-/**
- * Data API implementation
- */
+ if (Manipulator.getDataAttribute(this, 'slide') === 'next') {
+ carousel.next()
+ carousel._maybeEnableCycle()
+ return
+ }
-EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_SLIDE, Carousel.dataApiClickHandler)
+ carousel.prev()
+ carousel._maybeEnableCycle()
+})
EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
const carousels = SelectorEngine.find(SELECTOR_DATA_RIDE)
for (const carousel of carousels) {
- Carousel.carouselInterface(carousel, Carousel.getInstance(carousel))
+ Carousel.getOrCreateInstance(carousel)
}
})