carousel.js 16 KB

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