diff --git a/js/src/carousel.js b/js/src/carousel.js
index fcc78af6f104..989390aa0a46 100644
--- a/js/src/carousel.js
+++ b/js/src/carousel.js
@@ -1,6 +1,3 @@
-import $ from 'jquery'
-import Util from './util'
-
/**
* --------------------------------------------------------------------------
* Bootstrap (v4.1.3): carousel.js
@@ -8,6 +5,9 @@ import Util from './util'
* --------------------------------------------------------------------------
*/
+import $ from 'jquery'
+import Util from './util'
+
/**
* ------------------------------------------------------------------------
* Constants
@@ -23,13 +23,15 @@ const JQUERY_NO_CONFLICT = $.fn[NAME]
const ARROW_LEFT_KEYCODE = 37 // KeyboardEvent.which value for left arrow key
const ARROW_RIGHT_KEYCODE = 39 // KeyboardEvent.which value for right arrow key
const TOUCHEVENT_COMPAT_WAIT = 500 // Time for mouse compat events to fire after touch
+const SWIPE_THRESHOLD = 40
const Default = {
interval : 5000,
keyboard : true,
slide : false,
pause : 'hover',
- wrap : true
+ wrap : true,
+ touch : true
}
const DefaultType = {
@@ -37,7 +39,8 @@ const DefaultType = {
keyboard : 'boolean',
slide : '(boolean|string)',
pause : '(string|boolean)',
- wrap : 'boolean'
+ wrap : 'boolean',
+ touch : 'boolean'
}
const Direction = {
@@ -53,52 +56,68 @@ const Event = {
KEYDOWN : `keydown${EVENT_KEY}`,
MOUSEENTER : `mouseenter${EVENT_KEY}`,
MOUSELEAVE : `mouseleave${EVENT_KEY}`,
+ TOUCHSTART : `touchstart${EVENT_KEY}`,
+ TOUCHMOVE : `touchmove${EVENT_KEY}`,
TOUCHEND : `touchend${EVENT_KEY}`,
+ POINTERDOWN : `pointerdown${EVENT_KEY}`,
+ POINTERMOVE : `pointermove${EVENT_KEY}`,
+ POINTERUP : `pointerup${EVENT_KEY}`,
+ POINTERLEAVE : `pointerleave${EVENT_KEY}`,
+ POINTERCANCEL : `pointercancel${EVENT_KEY}`,
+ DRAG_START : `dragstart${EVENT_KEY}`,
LOAD_DATA_API : `load${EVENT_KEY}${DATA_API_KEY}`,
CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`
}
const ClassName = {
- CAROUSEL : 'carousel',
- ACTIVE : 'active',
- SLIDE : 'slide',
- RIGHT : 'carousel-item-right',
- LEFT : 'carousel-item-left',
- NEXT : 'carousel-item-next',
- PREV : 'carousel-item-prev',
- ITEM : 'carousel-item'
+ CAROUSEL : 'carousel',
+ ACTIVE : 'active',
+ SLIDE : 'slide',
+ RIGHT : 'carousel-item-right',
+ LEFT : 'carousel-item-left',
+ NEXT : 'carousel-item-next',
+ PREV : 'carousel-item-prev',
+ ITEM : 'carousel-item',
+ POINTER_EVENT : 'pointer-event'
}
const Selector = {
ACTIVE : '.active',
ACTIVE_ITEM : '.active.carousel-item',
ITEM : '.carousel-item',
+ ITEM_IMG : '.carousel-item img',
NEXT_PREV : '.carousel-item-next, .carousel-item-prev',
INDICATORS : '.carousel-indicators',
DATA_SLIDE : '[data-slide], [data-slide-to]',
DATA_RIDE : '[data-ride="carousel"]'
}
+const PointerType = {
+ TOUCH : 'touch',
+ PEN : 'pen'
+}
+
/**
* ------------------------------------------------------------------------
* Class Definition
* ------------------------------------------------------------------------
*/
-
class Carousel {
constructor(element, config) {
- this._items = null
- this._interval = null
- this._activeElement = null
-
- this._isPaused = false
- this._isSliding = false
-
- this.touchTimeout = null
-
- this._config = this._getConfig(config)
- this._element = $(element)[0]
- this._indicatorsElement = this._element.querySelector(Selector.INDICATORS)
+ this._items = null
+ this._interval = null
+ this._activeElement = null
+ this._isPaused = false
+ this._isSliding = false
+ this.touchTimeout = null
+ this.touchStartX = 0
+ this.touchDeltaX = 0
+
+ this._config = this._getConfig(config)
+ this._element = element
+ this._indicatorsElement = this._element.querySelector(Selector.INDICATORS)
+ this._touchSupported = 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0
+ this._pointerEvent = Boolean(window.PointerEvent || window.MSPointerEvent)
this._addEventListeners()
}
@@ -220,6 +239,26 @@ class Carousel {
return config
}
+ _handleSwipe() {
+ const absDeltax = Math.abs(this.touchDeltaX)
+
+ if (absDeltax <= SWIPE_THRESHOLD) {
+ return
+ }
+
+ const direction = absDeltax / this.touchDeltaX
+
+ // swipe left
+ if (direction > 0) {
+ this.prev()
+ }
+
+ // swipe right
+ if (direction < 0) {
+ this.next()
+ }
+ }
+
_addEventListeners() {
if (this._config.keyboard) {
$(this._element)
@@ -230,7 +269,46 @@ class Carousel {
$(this._element)
.on(Event.MOUSEENTER, (event) => this.pause(event))
.on(Event.MOUSELEAVE, (event) => this.cycle(event))
- if ('ontouchstart' in document.documentElement) {
+ }
+
+ this._addTouchEventListeners()
+ }
+
+ _addTouchEventListeners() {
+ if (!this._touchSupported) {
+ return
+ }
+
+ const start = (event) => {
+ if (this._pointerEvent && (event.originalEvent.pointerType === PointerType.TOUCH || event.originalEvent.pointerType === PointerType.PEN)) {
+ this.touchStartX = event.originalEvent.clientX
+ } else if (!this._pointerEvent) {
+ event.preventDefault()
+ this.touchStartX = event.originalEvent.touches[0].clientX
+ }
+ }
+
+ const move = (event) => {
+ if (!this._pointerEvent) {
+ event.preventDefault()
+
+ // ensure swiping with one touch and not pinching
+ if (event.originalEvent.touches && event.originalEvent.touches.length > 1) {
+ this.touchDeltaX = 0
+ } else {
+ this.touchDeltaX = event.originalEvent.touches[0].clientX - this.touchStartX
+ }
+ }
+ }
+
+ const end = (event) => {
+ if (this._pointerEvent && (event.originalEvent.pointerType === PointerType.TOUCH || event.originalEvent.pointerType === PointerType.PEN)) {
+ this.touchDeltaX = event.originalEvent.clientX - this.touchStartX
+ }
+
+ this._handleSwipe()
+
+ 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;
@@ -238,15 +316,26 @@ class 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._element).on(Event.TOUCHEND, () => {
- this.pause()
- if (this.touchTimeout) {
- clearTimeout(this.touchTimeout)
- }
- this.touchTimeout = setTimeout((event) => this.cycle(event), TOUCHEVENT_COMPAT_WAIT + this._config.interval)
- })
+
+ this.pause()
+ if (this.touchTimeout) {
+ clearTimeout(this.touchTimeout)
+ }
+ this.touchTimeout = setTimeout((event) => this.cycle(event), TOUCHEVENT_COMPAT_WAIT + this._config.interval)
}
}
+
+ $(this._element.querySelectorAll(Selector.ITEM_IMG)).on(Event.DRAG_START, (e) => e.preventDefault())
+ if (this._pointerEvent) {
+ $(this._element).on(Event.POINTERDOWN, (event) => start(event))
+ $(this._element).on(Event.POINTERUP, (event) => end(event))
+
+ this._element.classList.add(ClassName.POINTER_EVENT)
+ } else {
+ $(this._element).on(Event.TOUCHSTART, (event) => start(event))
+ $(this._element).on(Event.TOUCHMOVE, (event) => move(event))
+ $(this._element).on(Event.TOUCHEND, (event) => end(event))
+ }
}
_keydown(event) {
diff --git a/js/tests/index.html b/js/tests/index.html
index ce4a0e3081a3..1bcdc5380e2e 100644
--- a/js/tests/index.html
+++ b/js/tests/index.html
@@ -28,6 +28,9 @@
+
+
+