carousel.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. import $ from 'jquery'
  2. import Util from './util'
  3. /**
  4. * --------------------------------------------------------------------------
  5. * Bootstrap (v4.1.3): carousel.js
  6. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
  7. * --------------------------------------------------------------------------
  8. */
  9. const Carousel = (($) => {
  10. /**
  11. * ------------------------------------------------------------------------
  12. * Constants
  13. * ------------------------------------------------------------------------
  14. */
  15. const NAME = 'carousel'
  16. const VERSION = '4.1.3'
  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 Default = {
  25. interval : 5000,
  26. keyboard : true,
  27. slide : false,
  28. pause : 'hover',
  29. wrap : true
  30. }
  31. const DefaultType = {
  32. interval : '(number|boolean)',
  33. keyboard : 'boolean',
  34. slide : '(boolean|string)',
  35. pause : '(string|boolean)',
  36. wrap : 'boolean'
  37. }
  38. const Direction = {
  39. NEXT : 'next',
  40. PREV : 'prev',
  41. LEFT : 'left',
  42. RIGHT : 'right'
  43. }
  44. const Event = {
  45. SLIDE : `slide${EVENT_KEY}`,
  46. SLID : `slid${EVENT_KEY}`,
  47. KEYDOWN : `keydown${EVENT_KEY}`,
  48. MOUSEENTER : `mouseenter${EVENT_KEY}`,
  49. MOUSELEAVE : `mouseleave${EVENT_KEY}`,
  50. TOUCHEND : `touchend${EVENT_KEY}`,
  51. LOAD_DATA_API : `load${EVENT_KEY}${DATA_API_KEY}`,
  52. CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`
  53. }
  54. const ClassName = {
  55. CAROUSEL : 'carousel',
  56. ACTIVE : 'active',
  57. SLIDE : 'slide',
  58. RIGHT : 'carousel-item-right',
  59. LEFT : 'carousel-item-left',
  60. NEXT : 'carousel-item-next',
  61. PREV : 'carousel-item-prev',
  62. ITEM : 'carousel-item'
  63. }
  64. const Selector = {
  65. ACTIVE : '.active',
  66. ACTIVE_ITEM : '.active.carousel-item',
  67. ITEM : '.carousel-item',
  68. NEXT_PREV : '.carousel-item-next, .carousel-item-prev',
  69. INDICATORS : '.carousel-indicators',
  70. DATA_SLIDE : '[data-slide], [data-slide-to]',
  71. DATA_RIDE : '[data-ride="carousel"]'
  72. }
  73. /**
  74. * ------------------------------------------------------------------------
  75. * Class Definition
  76. * ------------------------------------------------------------------------
  77. */
  78. class Carousel {
  79. constructor(element, config) {
  80. this._items = null
  81. this._interval = null
  82. this._activeElement = null
  83. this._isPaused = false
  84. this._isSliding = false
  85. this.touchTimeout = null
  86. this._config = this._getConfig(config)
  87. this._element = $(element)[0]
  88. this._indicatorsElement = this._element.querySelector(Selector.INDICATORS)
  89. this._addEventListeners()
  90. }
  91. // Getters
  92. static get VERSION() {
  93. return VERSION
  94. }
  95. static get Default() {
  96. return Default
  97. }
  98. // Public
  99. next() {
  100. if (!this._isSliding) {
  101. this._slide(Direction.NEXT)
  102. }
  103. }
  104. nextWhenVisible() {
  105. // Don't call next when the page isn't visible
  106. // or the carousel or its parent isn't visible
  107. if (!document.hidden &&
  108. ($(this._element).is(':visible') && $(this._element).css('visibility') !== 'hidden')) {
  109. this.next()
  110. }
  111. }
  112. prev() {
  113. if (!this._isSliding) {
  114. this._slide(Direction.PREV)
  115. }
  116. }
  117. pause(event) {
  118. if (!event) {
  119. this._isPaused = true
  120. }
  121. if (this._element.querySelector(Selector.NEXT_PREV)) {
  122. Util.triggerTransitionEnd(this._element)
  123. this.cycle(true)
  124. }
  125. clearInterval(this._interval)
  126. this._interval = null
  127. }
  128. cycle(event) {
  129. if (!event) {
  130. this._isPaused = false
  131. }
  132. if (this._interval) {
  133. clearInterval(this._interval)
  134. this._interval = null
  135. }
  136. if (this._config.interval && !this._isPaused) {
  137. this._interval = setInterval(
  138. (document.visibilityState ? this.nextWhenVisible : this.next).bind(this),
  139. this._config.interval
  140. )
  141. }
  142. }
  143. to(index) {
  144. this._activeElement = this._element.querySelector(Selector.ACTIVE_ITEM)
  145. const activeIndex = this._getItemIndex(this._activeElement)
  146. if (index > this._items.length - 1 || index < 0) {
  147. return
  148. }
  149. if (this._isSliding) {
  150. $(this._element).one(Event.SLID, () => this.to(index))
  151. return
  152. }
  153. if (activeIndex === index) {
  154. this.pause()
  155. this.cycle()
  156. return
  157. }
  158. const direction = index > activeIndex
  159. ? Direction.NEXT
  160. : Direction.PREV
  161. this._slide(direction, this._items[index])
  162. }
  163. dispose() {
  164. $(this._element).off(EVENT_KEY)
  165. $.removeData(this._element, DATA_KEY)
  166. this._items = null
  167. this._config = null
  168. this._element = null
  169. this._interval = null
  170. this._isPaused = null
  171. this._isSliding = null
  172. this._activeElement = null
  173. this._indicatorsElement = null
  174. }
  175. // Private
  176. _getConfig(config) {
  177. config = {
  178. ...Default,
  179. ...config
  180. }
  181. Util.typeCheckConfig(NAME, config, DefaultType)
  182. return config
  183. }
  184. _addEventListeners() {
  185. if (this._config.keyboard) {
  186. $(this._element)
  187. .on(Event.KEYDOWN, (event) => this._keydown(event))
  188. }
  189. if (this._config.pause === 'hover') {
  190. $(this._element)
  191. .on(Event.MOUSEENTER, (event) => this.pause(event))
  192. .on(Event.MOUSELEAVE, (event) => this.cycle(event))
  193. if ('ontouchstart' in document.documentElement) {
  194. // If it's a touch-enabled device, mouseenter/leave are fired as
  195. // part of the mouse compatibility events on first tap - the carousel
  196. // would stop cycling until user tapped out of it;
  197. // here, we listen for touchend, explicitly pause the carousel
  198. // (as if it's the second time we tap on it, mouseenter compat event
  199. // is NOT fired) and after a timeout (to allow for mouse compatibility
  200. // events to fire) we explicitly restart cycling
  201. $(this._element).on(Event.TOUCHEND, () => {
  202. this.pause()
  203. if (this.touchTimeout) {
  204. clearTimeout(this.touchTimeout)
  205. }
  206. this.touchTimeout = setTimeout((event) => this.cycle(event), TOUCHEVENT_COMPAT_WAIT + this._config.interval)
  207. })
  208. }
  209. }
  210. }
  211. _keydown(event) {
  212. if (/input|textarea/i.test(event.target.tagName)) {
  213. return
  214. }
  215. switch (event.which) {
  216. case ARROW_LEFT_KEYCODE:
  217. event.preventDefault()
  218. this.prev()
  219. break
  220. case ARROW_RIGHT_KEYCODE:
  221. event.preventDefault()
  222. this.next()
  223. break
  224. default:
  225. }
  226. }
  227. _getItemIndex(element) {
  228. this._items = element && element.parentNode
  229. ? [].slice.call(element.parentNode.querySelectorAll(Selector.ITEM))
  230. : []
  231. return this._items.indexOf(element)
  232. }
  233. _getItemByDirection(direction, activeElement) {
  234. const isNextDirection = direction === Direction.NEXT
  235. const isPrevDirection = direction === Direction.PREV
  236. const activeIndex = this._getItemIndex(activeElement)
  237. const lastItemIndex = this._items.length - 1
  238. const isGoingToWrap = isPrevDirection && activeIndex === 0 ||
  239. isNextDirection && activeIndex === lastItemIndex
  240. if (isGoingToWrap && !this._config.wrap) {
  241. return activeElement
  242. }
  243. const delta = direction === Direction.PREV ? -1 : 1
  244. const itemIndex = (activeIndex + delta) % this._items.length
  245. return itemIndex === -1
  246. ? this._items[this._items.length - 1] : this._items[itemIndex]
  247. }
  248. _triggerSlideEvent(relatedTarget, eventDirectionName) {
  249. const targetIndex = this._getItemIndex(relatedTarget)
  250. const fromIndex = this._getItemIndex(this._element.querySelector(Selector.ACTIVE_ITEM))
  251. const slideEvent = $.Event(Event.SLIDE, {
  252. relatedTarget,
  253. direction: eventDirectionName,
  254. from: fromIndex,
  255. to: targetIndex
  256. })
  257. $(this._element).trigger(slideEvent)
  258. return slideEvent
  259. }
  260. _setActiveIndicatorElement(element) {
  261. if (this._indicatorsElement) {
  262. const indicators = [].slice.call(this._indicatorsElement.querySelectorAll(Selector.ACTIVE))
  263. $(indicators)
  264. .removeClass(ClassName.ACTIVE)
  265. const nextIndicator = this._indicatorsElement.children[
  266. this._getItemIndex(element)
  267. ]
  268. if (nextIndicator) {
  269. $(nextIndicator).addClass(ClassName.ACTIVE)
  270. }
  271. }
  272. }
  273. _slide(direction, element) {
  274. const activeElement = this._element.querySelector(Selector.ACTIVE_ITEM)
  275. const activeElementIndex = this._getItemIndex(activeElement)
  276. const nextElement = element || activeElement &&
  277. this._getItemByDirection(direction, activeElement)
  278. const nextElementIndex = this._getItemIndex(nextElement)
  279. const isCycling = Boolean(this._interval)
  280. let directionalClassName
  281. let orderClassName
  282. let eventDirectionName
  283. if (direction === Direction.NEXT) {
  284. directionalClassName = ClassName.LEFT
  285. orderClassName = ClassName.NEXT
  286. eventDirectionName = Direction.LEFT
  287. } else {
  288. directionalClassName = ClassName.RIGHT
  289. orderClassName = ClassName.PREV
  290. eventDirectionName = Direction.RIGHT
  291. }
  292. if (nextElement && $(nextElement).hasClass(ClassName.ACTIVE)) {
  293. this._isSliding = false
  294. return
  295. }
  296. const slideEvent = this._triggerSlideEvent(nextElement, eventDirectionName)
  297. if (slideEvent.isDefaultPrevented()) {
  298. return
  299. }
  300. if (!activeElement || !nextElement) {
  301. // Some weirdness is happening, so we bail
  302. return
  303. }
  304. this._isSliding = true
  305. if (isCycling) {
  306. this.pause()
  307. }
  308. this._setActiveIndicatorElement(nextElement)
  309. const slidEvent = $.Event(Event.SLID, {
  310. relatedTarget: nextElement,
  311. direction: eventDirectionName,
  312. from: activeElementIndex,
  313. to: nextElementIndex
  314. })
  315. if ($(this._element).hasClass(ClassName.SLIDE)) {
  316. $(nextElement).addClass(orderClassName)
  317. Util.reflow(nextElement)
  318. $(activeElement).addClass(directionalClassName)
  319. $(nextElement).addClass(directionalClassName)
  320. const transitionDuration = Util.getTransitionDurationFromElement(activeElement)
  321. $(activeElement)
  322. .one(Util.TRANSITION_END, () => {
  323. $(nextElement)
  324. .removeClass(`${directionalClassName} ${orderClassName}`)
  325. .addClass(ClassName.ACTIVE)
  326. $(activeElement).removeClass(`${ClassName.ACTIVE} ${orderClassName} ${directionalClassName}`)
  327. this._isSliding = false
  328. setTimeout(() => $(this._element).trigger(slidEvent), 0)
  329. })
  330. .emulateTransitionEnd(transitionDuration)
  331. } else {
  332. $(activeElement).removeClass(ClassName.ACTIVE)
  333. $(nextElement).addClass(ClassName.ACTIVE)
  334. this._isSliding = false
  335. $(this._element).trigger(slidEvent)
  336. }
  337. if (isCycling) {
  338. this.cycle()
  339. }
  340. }
  341. // Static
  342. static _jQueryInterface(config) {
  343. return this.each(function () {
  344. let data = $(this).data(DATA_KEY)
  345. let _config = {
  346. ...Default,
  347. ...$(this).data()
  348. }
  349. if (typeof config === 'object') {
  350. _config = {
  351. ..._config,
  352. ...config
  353. }
  354. }
  355. const action = typeof config === 'string' ? config : _config.slide
  356. if (!data) {
  357. data = new Carousel(this, _config)
  358. $(this).data(DATA_KEY, data)
  359. }
  360. if (typeof config === 'number') {
  361. data.to(config)
  362. } else if (typeof action === 'string') {
  363. if (typeof data[action] === 'undefined') {
  364. throw new TypeError(`No method named "${action}"`)
  365. }
  366. data[action]()
  367. } else if (_config.interval) {
  368. data.pause()
  369. data.cycle()
  370. }
  371. })
  372. }
  373. static _dataApiClickHandler(event) {
  374. const selector = Util.getSelectorFromElement(this)
  375. if (!selector) {
  376. return
  377. }
  378. const target = $(selector)[0]
  379. if (!target || !$(target).hasClass(ClassName.CAROUSEL)) {
  380. return
  381. }
  382. const config = {
  383. ...$(target).data(),
  384. ...$(this).data()
  385. }
  386. const slideIndex = this.getAttribute('data-slide-to')
  387. if (slideIndex) {
  388. config.interval = false
  389. }
  390. Carousel._jQueryInterface.call($(target), config)
  391. if (slideIndex) {
  392. $(target).data(DATA_KEY).to(slideIndex)
  393. }
  394. event.preventDefault()
  395. }
  396. }
  397. /**
  398. * ------------------------------------------------------------------------
  399. * Data Api implementation
  400. * ------------------------------------------------------------------------
  401. */
  402. $(document)
  403. .on(Event.CLICK_DATA_API, Selector.DATA_SLIDE, Carousel._dataApiClickHandler)
  404. $(window).on(Event.LOAD_DATA_API, () => {
  405. const carousels = [].slice.call(document.querySelectorAll(Selector.DATA_RIDE))
  406. for (let i = 0, len = carousels.length; i < len; i++) {
  407. const $carousel = $(carousels[i])
  408. Carousel._jQueryInterface.call($carousel, $carousel.data())
  409. }
  410. })
  411. /**
  412. * ------------------------------------------------------------------------
  413. * jQuery
  414. * ------------------------------------------------------------------------
  415. */
  416. $.fn[NAME] = Carousel._jQueryInterface
  417. $.fn[NAME].Constructor = Carousel
  418. $.fn[NAME].noConflict = function () {
  419. $.fn[NAME] = JQUERY_NO_CONFLICT
  420. return Carousel._jQueryInterface
  421. }
  422. return Carousel
  423. })($)
  424. export default Carousel