mirror of
https://github.com/twbs/bootstrap.git
synced 2025-08-11 16:14:04 +02:00
Extract Carousel's swipe functionality to a separate Class (#32999)
This commit is contained in:
@@ -54,7 +54,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "./dist/js/bootstrap.min.js",
|
"path": "./dist/js/bootstrap.min.js",
|
||||||
"maxSize": "16 kB"
|
"maxSize": "16.25 kB"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"ci": {
|
"ci": {
|
||||||
|
@@ -8,9 +8,9 @@
|
|||||||
import {
|
import {
|
||||||
defineJQueryPlugin,
|
defineJQueryPlugin,
|
||||||
getElementFromSelector,
|
getElementFromSelector,
|
||||||
|
getNextActiveElement,
|
||||||
isRTL,
|
isRTL,
|
||||||
isVisible,
|
isVisible,
|
||||||
getNextActiveElement,
|
|
||||||
reflow,
|
reflow,
|
||||||
triggerTransitionEnd,
|
triggerTransitionEnd,
|
||||||
typeCheckConfig
|
typeCheckConfig
|
||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
import EventHandler from './dom/event-handler'
|
import EventHandler from './dom/event-handler'
|
||||||
import Manipulator from './dom/manipulator'
|
import Manipulator from './dom/manipulator'
|
||||||
import SelectorEngine from './dom/selector-engine'
|
import SelectorEngine from './dom/selector-engine'
|
||||||
|
import Swipe from './util/swipe'
|
||||||
import BaseComponent from './base-component'
|
import BaseComponent from './base-component'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,7 +35,6 @@ const DATA_API_KEY = '.data-api'
|
|||||||
const ARROW_LEFT_KEY = 'ArrowLeft'
|
const ARROW_LEFT_KEY = 'ArrowLeft'
|
||||||
const ARROW_RIGHT_KEY = 'ArrowRight'
|
const ARROW_RIGHT_KEY = 'ArrowRight'
|
||||||
const TOUCHEVENT_COMPAT_WAIT = 500 // Time for mouse compat events to fire after touch
|
const TOUCHEVENT_COMPAT_WAIT = 500 // Time for mouse compat events to fire after touch
|
||||||
const SWIPE_THRESHOLD = 40
|
|
||||||
|
|
||||||
const Default = {
|
const Default = {
|
||||||
interval: 5000,
|
interval: 5000,
|
||||||
@@ -69,11 +69,6 @@ const EVENT_SLID = `slid${EVENT_KEY}`
|
|||||||
const EVENT_KEYDOWN = `keydown${EVENT_KEY}`
|
const EVENT_KEYDOWN = `keydown${EVENT_KEY}`
|
||||||
const EVENT_MOUSEENTER = `mouseenter${EVENT_KEY}`
|
const EVENT_MOUSEENTER = `mouseenter${EVENT_KEY}`
|
||||||
const EVENT_MOUSELEAVE = `mouseleave${EVENT_KEY}`
|
const EVENT_MOUSELEAVE = `mouseleave${EVENT_KEY}`
|
||||||
const EVENT_TOUCHSTART = `touchstart${EVENT_KEY}`
|
|
||||||
const EVENT_TOUCHMOVE = `touchmove${EVENT_KEY}`
|
|
||||||
const EVENT_TOUCHEND = `touchend${EVENT_KEY}`
|
|
||||||
const EVENT_POINTERDOWN = `pointerdown${EVENT_KEY}`
|
|
||||||
const EVENT_POINTERUP = `pointerup${EVENT_KEY}`
|
|
||||||
const EVENT_DRAG_START = `dragstart${EVENT_KEY}`
|
const EVENT_DRAG_START = `dragstart${EVENT_KEY}`
|
||||||
const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`
|
const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`
|
||||||
const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
|
const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
|
||||||
@@ -85,7 +80,6 @@ const CLASS_NAME_END = 'carousel-item-end'
|
|||||||
const CLASS_NAME_START = 'carousel-item-start'
|
const CLASS_NAME_START = 'carousel-item-start'
|
||||||
const CLASS_NAME_NEXT = 'carousel-item-next'
|
const CLASS_NAME_NEXT = 'carousel-item-next'
|
||||||
const CLASS_NAME_PREV = 'carousel-item-prev'
|
const CLASS_NAME_PREV = 'carousel-item-prev'
|
||||||
const CLASS_NAME_POINTER_EVENT = 'pointer-event'
|
|
||||||
|
|
||||||
const SELECTOR_ACTIVE = '.active'
|
const SELECTOR_ACTIVE = '.active'
|
||||||
const SELECTOR_ACTIVE_ITEM = '.active.carousel-item'
|
const SELECTOR_ACTIVE_ITEM = '.active.carousel-item'
|
||||||
@@ -97,9 +91,6 @@ const SELECTOR_INDICATOR = '[data-bs-target]'
|
|||||||
const SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]'
|
const SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]'
|
||||||
const SELECTOR_DATA_RIDE = '[data-bs-ride="carousel"]'
|
const SELECTOR_DATA_RIDE = '[data-bs-ride="carousel"]'
|
||||||
|
|
||||||
const POINTER_TYPE_TOUCH = 'touch'
|
|
||||||
const POINTER_TYPE_PEN = 'pen'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ------------------------------------------------------------------------
|
* ------------------------------------------------------------------------
|
||||||
* Class Definition
|
* Class Definition
|
||||||
@@ -115,14 +106,10 @@ class Carousel extends BaseComponent {
|
|||||||
this._isPaused = false
|
this._isPaused = false
|
||||||
this._isSliding = false
|
this._isSliding = false
|
||||||
this.touchTimeout = null
|
this.touchTimeout = null
|
||||||
this.touchStartX = 0
|
this._swipeHelper = null
|
||||||
this.touchDeltaX = 0
|
|
||||||
|
|
||||||
this._config = this._getConfig(config)
|
this._config = this._getConfig(config)
|
||||||
this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element)
|
this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element)
|
||||||
this._touchSupported = 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0
|
|
||||||
this._pointerEvent = Boolean(window.PointerEvent)
|
|
||||||
|
|
||||||
this._addEventListeners()
|
this._addEventListeners()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,6 +201,14 @@ class Carousel extends BaseComponent {
|
|||||||
this._slide(order, this._items[index])
|
this._slide(order, this._items[index])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
if (this._swipeHelper) {
|
||||||
|
this._swipeHelper.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
super.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
// Private
|
// Private
|
||||||
|
|
||||||
_getConfig(config) {
|
_getConfig(config) {
|
||||||
@@ -226,24 +221,6 @@ class Carousel extends BaseComponent {
|
|||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleSwipe() {
|
|
||||||
const absDeltax = Math.abs(this.touchDeltaX)
|
|
||||||
|
|
||||||
if (absDeltax <= SWIPE_THRESHOLD) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const direction = absDeltax / this.touchDeltaX
|
|
||||||
|
|
||||||
this.touchDeltaX = 0
|
|
||||||
|
|
||||||
if (!direction) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this._slide(direction > 0 ? DIRECTION_RIGHT : DIRECTION_LEFT)
|
|
||||||
}
|
|
||||||
|
|
||||||
_addEventListeners() {
|
_addEventListeners() {
|
||||||
if (this._config.keyboard) {
|
if (this._config.keyboard) {
|
||||||
EventHandler.on(this._element, EVENT_KEYDOWN, event => this._keydown(event))
|
EventHandler.on(this._element, EVENT_KEYDOWN, event => this._keydown(event))
|
||||||
@@ -254,38 +231,17 @@ class Carousel extends BaseComponent {
|
|||||||
EventHandler.on(this._element, EVENT_MOUSELEAVE, event => this.cycle(event))
|
EventHandler.on(this._element, EVENT_MOUSELEAVE, event => this.cycle(event))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._config.touch && this._touchSupported) {
|
if (this._config.touch && Swipe.isSupported()) {
|
||||||
this._addTouchEventListeners()
|
this._addTouchEventListeners()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_addTouchEventListeners() {
|
_addTouchEventListeners() {
|
||||||
const hasPointerPenTouch = event => {
|
for (const itemImg of SelectorEngine.find(SELECTOR_ITEM_IMG, this._element)) {
|
||||||
return this._pointerEvent &&
|
EventHandler.on(itemImg, EVENT_DRAG_START, event => event.preventDefault())
|
||||||
(event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const start = event => {
|
const endCallBack = () => {
|
||||||
if (hasPointerPenTouch(event)) {
|
|
||||||
this.touchStartX = event.clientX
|
|
||||||
} else if (!this._pointerEvent) {
|
|
||||||
this.touchStartX = event.touches[0].clientX
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const move = event => {
|
|
||||||
// ensure swiping with one touch and not pinching
|
|
||||||
this.touchDeltaX = event.touches && event.touches.length > 1 ?
|
|
||||||
0 :
|
|
||||||
event.touches[0].clientX - this.touchStartX
|
|
||||||
}
|
|
||||||
|
|
||||||
const end = event => {
|
|
||||||
if (hasPointerPenTouch(event)) {
|
|
||||||
this.touchDeltaX = event.clientX - this.touchStartX
|
|
||||||
}
|
|
||||||
|
|
||||||
this._handleSwipe()
|
|
||||||
if (this._config.pause === 'hover') {
|
if (this._config.pause === 'hover') {
|
||||||
// If it's a touch-enabled device, mouseenter/leave are fired as
|
// If it's a touch-enabled device, mouseenter/leave are fired as
|
||||||
// part of the mouse compatibility events on first tap - the carousel
|
// part of the mouse compatibility events on first tap - the carousel
|
||||||
@@ -304,20 +260,13 @@ class Carousel extends BaseComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const itemImg of SelectorEngine.find(SELECTOR_ITEM_IMG, this._element)) {
|
const swipeConfig = {
|
||||||
EventHandler.on(itemImg, EVENT_DRAG_START, event => event.preventDefault())
|
leftCallback: () => this._slide(DIRECTION_LEFT),
|
||||||
|
rightCallback: () => this._slide(DIRECTION_RIGHT),
|
||||||
|
endCallback: endCallBack
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._pointerEvent) {
|
this._swipeHelper = new Swipe(this._element, swipeConfig)
|
||||||
EventHandler.on(this._element, EVENT_POINTERDOWN, event => start(event))
|
|
||||||
EventHandler.on(this._element, EVENT_POINTERUP, event => end(event))
|
|
||||||
|
|
||||||
this._element.classList.add(CLASS_NAME_POINTER_EVENT)
|
|
||||||
} else {
|
|
||||||
EventHandler.on(this._element, EVENT_TOUCHSTART, event => start(event))
|
|
||||||
EventHandler.on(this._element, EVENT_TOUCHMOVE, event => move(event))
|
|
||||||
EventHandler.on(this._element, EVENT_TOUCHEND, event => end(event))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_keydown(event) {
|
_keydown(event) {
|
||||||
|
122
js/src/util/swipe.js
Normal file
122
js/src/util/swipe.js
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import EventHandler from '../dom/event-handler'
|
||||||
|
import { execute, typeCheckConfig } from './index'
|
||||||
|
|
||||||
|
const EVENT_KEY = '.bs.swipe'
|
||||||
|
const EVENT_TOUCHSTART = `touchstart${EVENT_KEY}`
|
||||||
|
const EVENT_TOUCHMOVE = `touchmove${EVENT_KEY}`
|
||||||
|
const EVENT_TOUCHEND = `touchend${EVENT_KEY}`
|
||||||
|
const EVENT_POINTERDOWN = `pointerdown${EVENT_KEY}`
|
||||||
|
const EVENT_POINTERUP = `pointerup${EVENT_KEY}`
|
||||||
|
const POINTER_TYPE_TOUCH = 'touch'
|
||||||
|
const POINTER_TYPE_PEN = 'pen'
|
||||||
|
const CLASS_NAME_POINTER_EVENT = 'pointer-event'
|
||||||
|
const SWIPE_THRESHOLD = 40
|
||||||
|
const NAME = 'swipe'
|
||||||
|
|
||||||
|
const Default = {
|
||||||
|
leftCallback: null,
|
||||||
|
rightCallback: null,
|
||||||
|
endCallback: null
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaultType = {
|
||||||
|
leftCallback: '(function|null)',
|
||||||
|
rightCallback: '(function|null)',
|
||||||
|
endCallback: '(function|null)'
|
||||||
|
}
|
||||||
|
|
||||||
|
class Swipe {
|
||||||
|
constructor(element, config) {
|
||||||
|
this._element = element
|
||||||
|
|
||||||
|
if (!element || !Swipe.isSupported()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this._config = this._getConfig(config)
|
||||||
|
this._deltaX = 0
|
||||||
|
this._supportPointerEvents = Boolean(window.PointerEvent)
|
||||||
|
this._initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
EventHandler.off(this._element, EVENT_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
_start(event) {
|
||||||
|
if (!this._supportPointerEvents) {
|
||||||
|
this._deltaX = event.touches[0].clientX
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._eventIsPointerPenTouch(event)) {
|
||||||
|
this._deltaX = event.clientX
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_end(event) {
|
||||||
|
if (this._eventIsPointerPenTouch(event)) {
|
||||||
|
this._deltaX = event.clientX - this._deltaX
|
||||||
|
}
|
||||||
|
|
||||||
|
this._handleSwipe()
|
||||||
|
execute(this._config.endCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
_move(event) {
|
||||||
|
this._deltaX = event.touches && event.touches.length > 1 ?
|
||||||
|
0 :
|
||||||
|
event.touches[0].clientX - this._deltaX
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleSwipe() {
|
||||||
|
const absDeltaX = Math.abs(this._deltaX)
|
||||||
|
|
||||||
|
if (absDeltaX <= SWIPE_THRESHOLD) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const direction = absDeltaX / this._deltaX
|
||||||
|
|
||||||
|
this._deltaX = 0
|
||||||
|
|
||||||
|
if (!direction) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(direction > 0 ? this._config.rightCallback : this._config.leftCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
_initEvents() {
|
||||||
|
if (this._supportPointerEvents) {
|
||||||
|
EventHandler.on(this._element, EVENT_POINTERDOWN, event => this._start(event))
|
||||||
|
EventHandler.on(this._element, EVENT_POINTERUP, event => this._end(event))
|
||||||
|
|
||||||
|
this._element.classList.add(CLASS_NAME_POINTER_EVENT)
|
||||||
|
} else {
|
||||||
|
EventHandler.on(this._element, EVENT_TOUCHSTART, event => this._start(event))
|
||||||
|
EventHandler.on(this._element, EVENT_TOUCHMOVE, event => this._move(event))
|
||||||
|
EventHandler.on(this._element, EVENT_TOUCHEND, event => this._end(event))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_getConfig(config) {
|
||||||
|
config = {
|
||||||
|
...Default,
|
||||||
|
...(typeof config === 'object' ? config : {})
|
||||||
|
}
|
||||||
|
typeCheckConfig(NAME, config, DefaultType)
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
_eventIsPointerPenTouch(event) {
|
||||||
|
return this._supportPointerEvents && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH)
|
||||||
|
}
|
||||||
|
|
||||||
|
static isSupported() {
|
||||||
|
return 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Swipe
|
@@ -2,6 +2,7 @@ import Carousel from '../../src/carousel'
|
|||||||
import EventHandler from '../../src/dom/event-handler'
|
import EventHandler from '../../src/dom/event-handler'
|
||||||
import { clearFixture, createEvent, getFixture, jQueryMock } from '../helpers/fixture'
|
import { clearFixture, createEvent, getFixture, jQueryMock } from '../helpers/fixture'
|
||||||
import { isRTL, noop } from '../../src/util/index'
|
import { isRTL, noop } from '../../src/util/index'
|
||||||
|
import Swipe from '../../src/util/swipe'
|
||||||
|
|
||||||
describe('Carousel', () => {
|
describe('Carousel', () => {
|
||||||
const { Simulator, PointerEvent } = window
|
const { Simulator, PointerEvent } = window
|
||||||
@@ -301,23 +302,24 @@ describe('Carousel', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
expect(carousel._addTouchEventListeners).not.toHaveBeenCalled()
|
expect(carousel._addTouchEventListeners).not.toHaveBeenCalled()
|
||||||
|
expect(carousel._swipeHelper).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not add touch event listeners if touch supported = false', () => {
|
it('should not add touch event listeners if touch supported = false', () => {
|
||||||
fixtureEl.innerHTML = '<div></div>'
|
fixtureEl.innerHTML = '<div></div>'
|
||||||
|
|
||||||
const carouselEl = fixtureEl.querySelector('div')
|
const carouselEl = fixtureEl.querySelector('div')
|
||||||
|
spyOn(Swipe, 'isSupported').and.returnValue(false)
|
||||||
|
|
||||||
const carousel = new Carousel(carouselEl)
|
const carousel = new Carousel(carouselEl)
|
||||||
|
EventHandler.off(carouselEl, Carousel.EVENT_KEY)
|
||||||
EventHandler.off(carouselEl, '.bs-carousel')
|
|
||||||
carousel._touchSupported = false
|
|
||||||
|
|
||||||
spyOn(carousel, '_addTouchEventListeners')
|
spyOn(carousel, '_addTouchEventListeners')
|
||||||
|
|
||||||
carousel._addEventListeners()
|
carousel._addEventListeners()
|
||||||
|
|
||||||
expect(carousel._addTouchEventListeners).not.toHaveBeenCalled()
|
expect(carousel._addTouchEventListeners).not.toHaveBeenCalled()
|
||||||
|
expect(carousel._swipeHelper).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should add touch event listeners by default', () => {
|
it('should add touch event listeners by default', () => {
|
||||||
@@ -566,7 +568,7 @@ describe('Carousel', () => {
|
|||||||
}, () => {
|
}, () => {
|
||||||
restorePointerEvents()
|
restorePointerEvents()
|
||||||
delete document.documentElement.ontouchstart
|
delete document.documentElement.ontouchstart
|
||||||
expect(carousel.touchDeltaX).toEqual(0)
|
expect(carousel._swipeHelper._deltaX).toEqual(0)
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -1237,19 +1239,20 @@ describe('Carousel', () => {
|
|||||||
|
|
||||||
const carouselEl = fixtureEl.querySelector('#myCarousel')
|
const carouselEl = fixtureEl.querySelector('#myCarousel')
|
||||||
const addEventSpy = spyOn(carouselEl, 'addEventListener').and.callThrough()
|
const addEventSpy = spyOn(carouselEl, 'addEventListener').and.callThrough()
|
||||||
const removeEventSpy = spyOn(carouselEl, 'removeEventListener').and.callThrough()
|
const removeEventSpy = spyOn(EventHandler, 'off').and.callThrough()
|
||||||
|
|
||||||
// Headless browser does not support touch events, so need to fake it
|
// Headless browser does not support touch events, so need to fake it
|
||||||
// to test that touch events are add/removed properly.
|
// to test that touch events are add/removed properly.
|
||||||
document.documentElement.ontouchstart = noop
|
document.documentElement.ontouchstart = noop
|
||||||
|
|
||||||
const carousel = new Carousel(carouselEl)
|
const carousel = new Carousel(carouselEl)
|
||||||
|
const swipeHelperSpy = spyOn(carousel._swipeHelper, 'dispose').and.callThrough()
|
||||||
|
|
||||||
const expectedArgs = [
|
const expectedArgs = [
|
||||||
['keydown', jasmine.any(Function), jasmine.any(Boolean)],
|
['keydown', jasmine.any(Function), jasmine.any(Boolean)],
|
||||||
['mouseover', jasmine.any(Function), jasmine.any(Boolean)],
|
['mouseover', jasmine.any(Function), jasmine.any(Boolean)],
|
||||||
['mouseout', jasmine.any(Function), jasmine.any(Boolean)],
|
['mouseout', jasmine.any(Function), jasmine.any(Boolean)],
|
||||||
...(carousel._pointerEvent ?
|
...(carousel._swipeHelper._supportPointerEvents ?
|
||||||
[
|
[
|
||||||
['pointerdown', jasmine.any(Function), jasmine.any(Boolean)],
|
['pointerdown', jasmine.any(Function), jasmine.any(Boolean)],
|
||||||
['pointerup', jasmine.any(Function), jasmine.any(Boolean)]
|
['pointerup', jasmine.any(Function), jasmine.any(Boolean)]
|
||||||
@@ -1265,7 +1268,8 @@ describe('Carousel', () => {
|
|||||||
|
|
||||||
carousel.dispose()
|
carousel.dispose()
|
||||||
|
|
||||||
expect(removeEventSpy.calls.allArgs()).toEqual(expectedArgs)
|
expect(removeEventSpy).toHaveBeenCalledWith(carouselEl, Carousel.EVENT_KEY)
|
||||||
|
expect(swipeHelperSpy).toHaveBeenCalled()
|
||||||
|
|
||||||
delete document.documentElement.ontouchstart
|
delete document.documentElement.ontouchstart
|
||||||
})
|
})
|
||||||
|
263
js/tests/unit/util/swipe.spec.js
Normal file
263
js/tests/unit/util/swipe.spec.js
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
import { clearFixture, getFixture } from '../../helpers/fixture'
|
||||||
|
import EventHandler from '../../../src/dom/event-handler'
|
||||||
|
import Swipe from '../../../src/util/swipe'
|
||||||
|
import { noop } from '../../../src/util'
|
||||||
|
|
||||||
|
describe('Swipe', () => {
|
||||||
|
const { Simulator, PointerEvent } = window
|
||||||
|
const originWinPointerEvent = PointerEvent
|
||||||
|
const supportPointerEvent = Boolean(PointerEvent)
|
||||||
|
|
||||||
|
let fixtureEl
|
||||||
|
let swipeEl
|
||||||
|
const clearPointerEvents = () => {
|
||||||
|
window.PointerEvent = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const restorePointerEvents = () => {
|
||||||
|
window.PointerEvent = originWinPointerEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
// The headless browser does not support touch events, so we need to fake it
|
||||||
|
// in order to test that touch events are added properly
|
||||||
|
const defineDocumentElementOntouchstart = () => {
|
||||||
|
document.documentElement.ontouchstart = noop
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteDocumentElementOntouchstart = () => {
|
||||||
|
delete document.documentElement.ontouchstart
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockSwipeGesture = (element, options = {}, type = 'touch') => {
|
||||||
|
Simulator.setType(type)
|
||||||
|
const _options = { deltaX: 0, deltaY: 0, ...options }
|
||||||
|
|
||||||
|
Simulator.gestures.swipe(element, _options)
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixtureEl = getFixture()
|
||||||
|
const cssStyle = [
|
||||||
|
'<style>',
|
||||||
|
' #fixture .pointer-event {',
|
||||||
|
' touch-action: pan-y;',
|
||||||
|
' }',
|
||||||
|
' #fixture div {',
|
||||||
|
' width: 300px;',
|
||||||
|
' height: 300px;',
|
||||||
|
' }',
|
||||||
|
'</style>'
|
||||||
|
].join('')
|
||||||
|
|
||||||
|
fixtureEl.innerHTML = '<div id="swipeEl"></div>' + cssStyle
|
||||||
|
swipeEl = fixtureEl.querySelector('div')
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
clearFixture()
|
||||||
|
deleteDocumentElementOntouchstart()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('constructor', () => {
|
||||||
|
it('should add touch event listeners by default', () => {
|
||||||
|
defineDocumentElementOntouchstart()
|
||||||
|
|
||||||
|
spyOn(Swipe.prototype, '_initEvents').and.callThrough()
|
||||||
|
const swipe = new Swipe(swipeEl)
|
||||||
|
expect(swipe._initEvents).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not add touch event listeners if touch is not supported', () => {
|
||||||
|
spyOn(Swipe, 'isSupported').and.returnValue(false)
|
||||||
|
|
||||||
|
spyOn(Swipe.prototype, '_initEvents').and.callThrough()
|
||||||
|
const swipe = new Swipe(swipeEl)
|
||||||
|
|
||||||
|
expect(swipe._initEvents).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Config', () => {
|
||||||
|
it('Test leftCallback', done => {
|
||||||
|
const spyRight = jasmine.createSpy('spy')
|
||||||
|
clearPointerEvents()
|
||||||
|
defineDocumentElementOntouchstart()
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
|
new Swipe(swipeEl, {
|
||||||
|
leftCallback: () => {
|
||||||
|
expect(spyRight).not.toHaveBeenCalled()
|
||||||
|
restorePointerEvents()
|
||||||
|
done()
|
||||||
|
},
|
||||||
|
rightCallback: spyRight
|
||||||
|
})
|
||||||
|
|
||||||
|
mockSwipeGesture(swipeEl, {
|
||||||
|
pos: [300, 10],
|
||||||
|
deltaX: -300
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Test rightCallback', done => {
|
||||||
|
const spyLeft = jasmine.createSpy('spy')
|
||||||
|
clearPointerEvents()
|
||||||
|
defineDocumentElementOntouchstart()
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
|
new Swipe(swipeEl, {
|
||||||
|
rightCallback: () => {
|
||||||
|
expect(spyLeft).not.toHaveBeenCalled()
|
||||||
|
restorePointerEvents()
|
||||||
|
done()
|
||||||
|
},
|
||||||
|
leftCallback: spyLeft
|
||||||
|
})
|
||||||
|
|
||||||
|
mockSwipeGesture(swipeEl, {
|
||||||
|
pos: [10, 10],
|
||||||
|
deltaX: 300
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Test endCallback', done => {
|
||||||
|
clearPointerEvents()
|
||||||
|
defineDocumentElementOntouchstart()
|
||||||
|
let isFirstTime = true
|
||||||
|
|
||||||
|
const callback = () => {
|
||||||
|
if (isFirstTime) {
|
||||||
|
isFirstTime = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
expect().nothing()
|
||||||
|
restorePointerEvents()
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
|
new Swipe(swipeEl, {
|
||||||
|
endCallback: callback
|
||||||
|
})
|
||||||
|
mockSwipeGesture(swipeEl, {
|
||||||
|
pos: [10, 10],
|
||||||
|
deltaX: 300
|
||||||
|
})
|
||||||
|
|
||||||
|
mockSwipeGesture(swipeEl, {
|
||||||
|
pos: [300, 10],
|
||||||
|
deltaX: -300
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Functionality on PointerEvents', () => {
|
||||||
|
it('should allow swipeRight and call "rightCallback" with pointer events', done => {
|
||||||
|
if (!supportPointerEvent) {
|
||||||
|
expect().nothing()
|
||||||
|
done()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = '#fixture .pointer-event { touch-action: none !important; }'
|
||||||
|
fixtureEl.innerHTML += style
|
||||||
|
|
||||||
|
defineDocumentElementOntouchstart()
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
|
new Swipe(swipeEl, {
|
||||||
|
rightCallback: () => {
|
||||||
|
deleteDocumentElementOntouchstart()
|
||||||
|
expect().nothing()
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
mockSwipeGesture(swipeEl, { deltaX: 300 }, 'pointer')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should allow swipeLeft and call "leftCallback" with pointer events', done => {
|
||||||
|
if (!supportPointerEvent) {
|
||||||
|
expect().nothing()
|
||||||
|
done()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = '#fixture .pointer-event { touch-action: none !important; }'
|
||||||
|
fixtureEl.innerHTML += style
|
||||||
|
|
||||||
|
defineDocumentElementOntouchstart()
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
|
new Swipe(swipeEl, {
|
||||||
|
leftCallback: () => {
|
||||||
|
expect().nothing()
|
||||||
|
deleteDocumentElementOntouchstart()
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
mockSwipeGesture(swipeEl, {
|
||||||
|
pos: [300, 10],
|
||||||
|
deltaX: -300
|
||||||
|
}, 'pointer')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Dispose', () => {
|
||||||
|
it('should call EventHandler.off', () => {
|
||||||
|
defineDocumentElementOntouchstart()
|
||||||
|
spyOn(EventHandler, 'off').and.callThrough()
|
||||||
|
const swipe = new Swipe(swipeEl)
|
||||||
|
|
||||||
|
swipe.dispose()
|
||||||
|
expect(EventHandler.off).toHaveBeenCalledWith(swipeEl, '.bs.swipe')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should destroy', () => {
|
||||||
|
const addEventSpy = spyOn(fixtureEl, 'addEventListener').and.callThrough()
|
||||||
|
const removeEventSpy = spyOn(fixtureEl, 'removeEventListener').and.callThrough()
|
||||||
|
defineDocumentElementOntouchstart()
|
||||||
|
|
||||||
|
const swipe = new Swipe(fixtureEl)
|
||||||
|
|
||||||
|
const expectedArgs =
|
||||||
|
swipe._supportPointerEvents ?
|
||||||
|
[
|
||||||
|
['pointerdown', jasmine.any(Function), jasmine.any(Boolean)],
|
||||||
|
['pointerup', jasmine.any(Function), jasmine.any(Boolean)]
|
||||||
|
] :
|
||||||
|
[
|
||||||
|
['touchstart', jasmine.any(Function), jasmine.any(Boolean)],
|
||||||
|
['touchmove', jasmine.any(Function), jasmine.any(Boolean)],
|
||||||
|
['touchend', jasmine.any(Function), jasmine.any(Boolean)]
|
||||||
|
]
|
||||||
|
|
||||||
|
expect(addEventSpy.calls.allArgs()).toEqual(expectedArgs)
|
||||||
|
|
||||||
|
swipe.dispose()
|
||||||
|
|
||||||
|
expect(removeEventSpy.calls.allArgs()).toEqual(expectedArgs)
|
||||||
|
|
||||||
|
delete document.documentElement.ontouchstart
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('"isSupported" static', () => {
|
||||||
|
it('should return "true" if "touchstart" exists in document element)', () => {
|
||||||
|
Object.defineProperty(window.navigator, 'maxTouchPoints', () => 0)
|
||||||
|
defineDocumentElementOntouchstart()
|
||||||
|
|
||||||
|
expect(Swipe.isSupported()).toBeTrue()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return "false" if "touchstart" not exists in document element and "navigator.maxTouchPoints" are zero (0)', () => {
|
||||||
|
Object.defineProperty(window.navigator, 'maxTouchPoints', () => 0)
|
||||||
|
deleteDocumentElementOntouchstart()
|
||||||
|
|
||||||
|
if ('ontouchstart' in document.documentElement) {
|
||||||
|
expect().nothing()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(Swipe.isSupported()).toBeFalse()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
Reference in New Issue
Block a user