carousel.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. import Util from './util'
  2. /**
  3. * --------------------------------------------------------------------------
  4. * Bootstrap (v4.0.0-alpha.4): carousel.js
  5. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
  6. * --------------------------------------------------------------------------
  7. */
  8. const Carousel = (($) => {
  9. /**
  10. * ------------------------------------------------------------------------
  11. * Constants
  12. * ------------------------------------------------------------------------
  13. */
  14. const NAME = 'carousel'
  15. const VERSION = '4.0.0-alpha.4'
  16. const DATA_KEY = 'bs.carousel'
  17. const EVENT_KEY = `.${DATA_KEY}`
  18. const DATA_API_KEY = '.data-api'
  19. const JQUERY_NO_CONFLICT = $.fn[NAME]
  20. const TRANSITION_DURATION = 600
  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 Default = {
  24. interval : 5000,
  25. keyboard : true,
  26. slide : false,
  27. pause : 'hover',
  28. wrap : true
  29. }
  30. const DefaultType = {
  31. interval : '(number|boolean)',
  32. keyboard : 'boolean',
  33. slide : '(boolean|string)',
  34. pause : '(string|boolean)',
  35. wrap : 'boolean'
  36. }
  37. const Direction = {
  38. NEXT : 'next',
  39. PREVIOUS : 'prev'
  40. }
  41. const Event = {
  42. SLIDE : `slide${EVENT_KEY}`,
  43. SLID : `slid${EVENT_KEY}`,
  44. KEYDOWN : `keydown${EVENT_KEY}`,
  45. MOUSEENTER : `mouseenter${EVENT_KEY}`,
  46. MOUSELEAVE : `mouseleave${EVENT_KEY}`,
  47. LOAD_DATA_API : `load${EVENT_KEY}${DATA_API_KEY}`,
  48. CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`
  49. }
  50. const ClassName = {
  51. CAROUSEL : 'carousel',
  52. ACTIVE : 'active',
  53. SLIDE : 'slide',
  54. RIGHT : 'right',
  55. LEFT : 'left',
  56. ITEM : 'carousel-item'
  57. }
  58. const Selector = {
  59. ACTIVE : '.active',
  60. ACTIVE_ITEM : '.active.carousel-item',
  61. ITEM : '.carousel-item',
  62. NEXT_PREV : '.next, .prev',
  63. INDICATORS : '.carousel-indicators',
  64. DATA_SLIDE : '[data-slide], [data-slide-to]',
  65. DATA_RIDE : '[data-ride="carousel"]'
  66. }
  67. /**
  68. * ------------------------------------------------------------------------
  69. * Class Definition
  70. * ------------------------------------------------------------------------
  71. */
  72. class Carousel {
  73. constructor(element, config) {
  74. this._items = null
  75. this._interval = null
  76. this._activeElement = null
  77. this._isPaused = false
  78. this._isSliding = false
  79. this._config = this._getConfig(config)
  80. this._element = $(element)[0]
  81. this._indicatorsElement = $(this._element).find(Selector.INDICATORS)[0]
  82. this._addEventListeners()
  83. }
  84. // getters
  85. static get VERSION() {
  86. return VERSION
  87. }
  88. static get Default() {
  89. return Default
  90. }
  91. // public
  92. next() {
  93. if (!this._isSliding) {
  94. this._slide(Direction.NEXT)
  95. }
  96. }
  97. nextWhenVisible() {
  98. // Don't call next when the page isn't visible
  99. if (!document.hidden) {
  100. this.next()
  101. }
  102. }
  103. prev() {
  104. if (!this._isSliding) {
  105. this._slide(Direction.PREVIOUS)
  106. }
  107. }
  108. pause(event) {
  109. if (!event) {
  110. this._isPaused = true
  111. }
  112. if ($(this._element).find(Selector.NEXT_PREV)[0] &&
  113. Util.supportsTransitionEnd()) {
  114. Util.triggerTransitionEnd(this._element)
  115. this.cycle(true)
  116. }
  117. clearInterval(this._interval)
  118. this._interval = null
  119. }
  120. cycle(event) {
  121. if (!event) {
  122. this._isPaused = false
  123. }
  124. if (this._interval) {
  125. clearInterval(this._interval)
  126. this._interval = null
  127. }
  128. if (this._config.interval && !this._isPaused) {
  129. this._interval = setInterval(
  130. $.proxy(document.visibilityState ? this.nextWhenVisible : this.next, this), this._config.interval
  131. )
  132. }
  133. }
  134. to(index) {
  135. this._activeElement = $(this._element).find(Selector.ACTIVE_ITEM)[0]
  136. let activeIndex = this._getItemIndex(this._activeElement)
  137. if (index > (this._items.length - 1) || index < 0) {
  138. return
  139. }
  140. if (this._isSliding) {
  141. $(this._element).one(Event.SLID, () => this.to(index))
  142. return
  143. }
  144. if (activeIndex === index) {
  145. this.pause()
  146. this.cycle()
  147. return
  148. }
  149. let direction = index > activeIndex ?
  150. Direction.NEXT :
  151. Direction.PREVIOUS
  152. this._slide(direction, this._items[index])
  153. }
  154. dispose() {
  155. $(this._element).off(EVENT_KEY)
  156. $.removeData(this._element, DATA_KEY)
  157. this._items = null
  158. this._config = null
  159. this._element = null
  160. this._interval = null
  161. this._isPaused = null
  162. this._isSliding = null
  163. this._activeElement = null
  164. this._indicatorsElement = null
  165. }
  166. // private
  167. _getConfig(config) {
  168. config = $.extend({}, Default, config)
  169. Util.typeCheckConfig(NAME, config, DefaultType)
  170. return config
  171. }
  172. _addEventListeners() {
  173. if (this._config.keyboard) {
  174. $(this._element)
  175. .on(Event.KEYDOWN, $.proxy(this._keydown, this))
  176. }
  177. if (this._config.pause === 'hover' &&
  178. !('ontouchstart' in document.documentElement)) {
  179. $(this._element)
  180. .on(Event.MOUSEENTER, $.proxy(this.pause, this))
  181. .on(Event.MOUSELEAVE, $.proxy(this.cycle, this))
  182. }
  183. }
  184. _keydown(event) {
  185. event.preventDefault()
  186. if (/input|textarea/i.test(event.target.tagName)) {
  187. return
  188. }
  189. switch (event.which) {
  190. case ARROW_LEFT_KEYCODE:
  191. this.prev()
  192. break
  193. case ARROW_RIGHT_KEYCODE:
  194. this.next()
  195. break
  196. default:
  197. return
  198. }
  199. }
  200. _getItemIndex(element) {
  201. this._items = $.makeArray($(element).parent().find(Selector.ITEM))
  202. return this._items.indexOf(element)
  203. }
  204. _getItemByDirection(direction, activeElement) {
  205. let isNextDirection = direction === Direction.NEXT
  206. let isPrevDirection = direction === Direction.PREVIOUS
  207. let activeIndex = this._getItemIndex(activeElement)
  208. let lastItemIndex = (this._items.length - 1)
  209. let isGoingToWrap = (isPrevDirection && activeIndex === 0) ||
  210. (isNextDirection && activeIndex === lastItemIndex)
  211. if (isGoingToWrap && !this._config.wrap) {
  212. return activeElement
  213. }
  214. let delta = direction === Direction.PREVIOUS ? -1 : 1
  215. let itemIndex = (activeIndex + delta) % this._items.length
  216. return itemIndex === -1 ?
  217. this._items[this._items.length - 1] : this._items[itemIndex]
  218. }
  219. _triggerSlideEvent(relatedTarget, directionalClassname) {
  220. let slideEvent = $.Event(Event.SLIDE, {
  221. relatedTarget,
  222. direction: directionalClassname
  223. })
  224. $(this._element).trigger(slideEvent)
  225. return slideEvent
  226. }
  227. _setActiveIndicatorElement(element) {
  228. if (this._indicatorsElement) {
  229. $(this._indicatorsElement)
  230. .find(Selector.ACTIVE)
  231. .removeClass(ClassName.ACTIVE)
  232. let nextIndicator = this._indicatorsElement.children[
  233. this._getItemIndex(element)
  234. ]
  235. if (nextIndicator) {
  236. $(nextIndicator).addClass(ClassName.ACTIVE)
  237. }
  238. }
  239. }
  240. _slide(direction, element) {
  241. let activeElement = $(this._element).find(Selector.ACTIVE_ITEM)[0]
  242. let nextElement = element || activeElement &&
  243. this._getItemByDirection(direction, activeElement)
  244. let isCycling = Boolean(this._interval)
  245. let directionalClassName = direction === Direction.NEXT ?
  246. ClassName.LEFT :
  247. ClassName.RIGHT
  248. if (nextElement && $(nextElement).hasClass(ClassName.ACTIVE)) {
  249. this._isSliding = false
  250. return
  251. }
  252. let slideEvent = this._triggerSlideEvent(nextElement, directionalClassName)
  253. if (slideEvent.isDefaultPrevented()) {
  254. return
  255. }
  256. if (!activeElement || !nextElement) {
  257. // some weirdness is happening, so we bail
  258. return
  259. }
  260. this._isSliding = true
  261. if (isCycling) {
  262. this.pause()
  263. }
  264. this._setActiveIndicatorElement(nextElement)
  265. let slidEvent = $.Event(Event.SLID, {
  266. relatedTarget: nextElement,
  267. direction: directionalClassName
  268. })
  269. if (Util.supportsTransitionEnd() &&
  270. $(this._element).hasClass(ClassName.SLIDE)) {
  271. $(nextElement).addClass(direction)
  272. Util.reflow(nextElement)
  273. $(activeElement).addClass(directionalClassName)
  274. $(nextElement).addClass(directionalClassName)
  275. $(activeElement)
  276. .one(Util.TRANSITION_END, () => {
  277. $(nextElement)
  278. .removeClass(directionalClassName)
  279. .removeClass(direction)
  280. $(nextElement).addClass(ClassName.ACTIVE)
  281. $(activeElement)
  282. .removeClass(ClassName.ACTIVE)
  283. .removeClass(direction)
  284. .removeClass(directionalClassName)
  285. this._isSliding = false
  286. setTimeout(() => $(this._element).trigger(slidEvent), 0)
  287. })
  288. .emulateTransitionEnd(TRANSITION_DURATION)
  289. } else {
  290. $(activeElement).removeClass(ClassName.ACTIVE)
  291. $(nextElement).addClass(ClassName.ACTIVE)
  292. this._isSliding = false
  293. $(this._element).trigger(slidEvent)
  294. }
  295. if (isCycling) {
  296. this.cycle()
  297. }
  298. }
  299. // static
  300. static _jQueryInterface(config) {
  301. return this.each(function () {
  302. let data = $(this).data(DATA_KEY)
  303. let _config = $.extend({}, Default, $(this).data())
  304. if (typeof config === 'object') {
  305. $.extend(_config, config)
  306. }
  307. let action = typeof config === 'string' ? config : _config.slide
  308. if (!data) {
  309. data = new Carousel(this, _config)
  310. $(this).data(DATA_KEY, data)
  311. }
  312. if (typeof config === 'number') {
  313. data.to(config)
  314. } else if (typeof action === 'string') {
  315. if (data[action] === undefined) {
  316. throw new Error(`No method named "${action}"`)
  317. }
  318. data[action]()
  319. } else if (_config.interval) {
  320. data.pause()
  321. data.cycle()
  322. }
  323. })
  324. }
  325. static _dataApiClickHandler(event) {
  326. let selector = Util.getSelectorFromElement(this)
  327. if (!selector) {
  328. return
  329. }
  330. let target = $(selector)[0]
  331. if (!target || !$(target).hasClass(ClassName.CAROUSEL)) {
  332. return
  333. }
  334. let config = $.extend({}, $(target).data(), $(this).data())
  335. let slideIndex = this.getAttribute('data-slide-to')
  336. if (slideIndex) {
  337. config.interval = false
  338. }
  339. Carousel._jQueryInterface.call($(target), config)
  340. if (slideIndex) {
  341. $(target).data(DATA_KEY).to(slideIndex)
  342. }
  343. event.preventDefault()
  344. }
  345. }
  346. /**
  347. * ------------------------------------------------------------------------
  348. * Data Api implementation
  349. * ------------------------------------------------------------------------
  350. */
  351. $(document)
  352. .on(Event.CLICK_DATA_API, Selector.DATA_SLIDE, Carousel._dataApiClickHandler)
  353. $(window).on(Event.LOAD_DATA_API, () => {
  354. $(Selector.DATA_RIDE).each(function () {
  355. let $carousel = $(this)
  356. Carousel._jQueryInterface.call($carousel, $carousel.data())
  357. })
  358. })
  359. /**
  360. * ------------------------------------------------------------------------
  361. * jQuery
  362. * ------------------------------------------------------------------------
  363. */
  364. $.fn[NAME] = Carousel._jQueryInterface
  365. $.fn[NAME].Constructor = Carousel
  366. $.fn[NAME].noConflict = function () {
  367. $.fn[NAME] = JQUERY_NO_CONFLICT
  368. return Carousel._jQueryInterface
  369. }
  370. return Carousel
  371. })(jQuery)
  372. export default Carousel