Layout von Websiten mit Bootstrap und Foundation
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

carousel.js 16KB

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