Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/twbs/bootstrap.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGeoSot <geo.sotis@gmail.com>2021-10-11 17:04:43 +0300
committerGitHub <noreply@github.com>2021-10-11 17:04:43 +0300
commit8ec6c9452286472ddad12d1af59b173ede22b5ac (patch)
tree070494c190d9b33138f6cbfee1bcf2dfe9b9e07c
parentb21c7ccbb7e3d955fd5043aae6d426cef5765dfa (diff)
Extract Carousel's swipe functionality to a separate Class (#32999)
-rw-r--r--.bundlewatch.config.json2
-rw-r--r--js/src/carousel.js91
-rw-r--r--js/src/util/swipe.js122
-rw-r--r--js/tests/unit/carousel.spec.js18
-rw-r--r--js/tests/unit/util/swipe.spec.js263
5 files changed, 417 insertions, 79 deletions
diff --git a/.bundlewatch.config.json b/.bundlewatch.config.json
index 371a7b459b..316976ee9c 100644
--- a/.bundlewatch.config.json
+++ b/.bundlewatch.config.json
@@ -54,7 +54,7 @@
},
{
"path": "./dist/js/bootstrap.min.js",
- "maxSize": "16 kB"
+ "maxSize": "16.25 kB"
}
],
"ci": {
diff --git a/js/src/carousel.js b/js/src/carousel.js
index 161e980c61..f28ee259b3 100644
--- a/js/src/carousel.js
+++ b/js/src/carousel.js
@@ -8,9 +8,9 @@
import {
defineJQueryPlugin,
getElementFromSelector,
+ getNextActiveElement,
isRTL,
isVisible,
- getNextActiveElement,
reflow,
triggerTransitionEnd,
typeCheckConfig
@@ -18,6 +18,7 @@ import {
import EventHandler from './dom/event-handler'
import Manipulator from './dom/manipulator'
import SelectorEngine from './dom/selector-engine'
+import Swipe from './util/swipe'
import BaseComponent from './base-component'
/**
@@ -34,7 +35,6 @@ const DATA_API_KEY = '.data-api'
const ARROW_LEFT_KEY = 'ArrowLeft'
const ARROW_RIGHT_KEY = 'ArrowRight'
const TOUCHEVENT_COMPAT_WAIT = 500 // Time for mouse compat events to fire after touch
-const SWIPE_THRESHOLD = 40
const Default = {
interval: 5000,
@@ -69,11 +69,6 @@ const EVENT_SLID = `slid${EVENT_KEY}`
const EVENT_KEYDOWN = `keydown${EVENT_KEY}`
const EVENT_MOUSEENTER = `mouseenter${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_LOAD_DATA_API = `load${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_NEXT = 'carousel-item-next'
const CLASS_NAME_PREV = 'carousel-item-prev'
-const CLASS_NAME_POINTER_EVENT = 'pointer-event'
const SELECTOR_ACTIVE = '.active'
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_RIDE = '[data-bs-ride="carousel"]'
-const POINTER_TYPE_TOUCH = 'touch'
-const POINTER_TYPE_PEN = 'pen'
-
/**
* ------------------------------------------------------------------------
* Class Definition
@@ -115,14 +106,10 @@ class Carousel extends BaseComponent {
this._isPaused = false
this._isSliding = false
this.touchTimeout = null
- this.touchStartX = 0
- this.touchDeltaX = 0
+ this._swipeHelper = null
this._config = this._getConfig(config)
this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element)
- this._touchSupported = 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0
- this._pointerEvent = Boolean(window.PointerEvent)
-
this._addEventListeners()
}
@@ -214,6 +201,14 @@ class Carousel extends BaseComponent {
this._slide(order, this._items[index])
}
+ dispose() {
+ if (this._swipeHelper) {
+ this._swipeHelper.dispose()
+ }
+
+ super.dispose()
+ }
+
// Private
_getConfig(config) {
@@ -226,24 +221,6 @@ class Carousel extends BaseComponent {
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() {
if (this._config.keyboard) {
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))
}
- if (this._config.touch && this._touchSupported) {
+ if (this._config.touch && Swipe.isSupported()) {
this._addTouchEventListeners()
}
}
_addTouchEventListeners() {
- const hasPointerPenTouch = event => {
- return this._pointerEvent &&
- (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH)
- }
-
- const start = event => {
- 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
+ for (const itemImg of SelectorEngine.find(SELECTOR_ITEM_IMG, this._element)) {
+ EventHandler.on(itemImg, EVENT_DRAG_START, event => event.preventDefault())
}
- const end = event => {
- if (hasPointerPenTouch(event)) {
- this.touchDeltaX = event.clientX - this.touchStartX
- }
-
- this._handleSwipe()
+ const endCallBack = () => {
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
@@ -304,20 +260,13 @@ class Carousel extends BaseComponent {
}
}
- for (const itemImg of SelectorEngine.find(SELECTOR_ITEM_IMG, this._element)) {
- EventHandler.on(itemImg, EVENT_DRAG_START, event => event.preventDefault())
+ const swipeConfig = {
+ leftCallback: () => this._slide(DIRECTION_LEFT),
+ rightCallback: () => this._slide(DIRECTION_RIGHT),
+ endCallback: endCallBack
}
- if (this._pointerEvent) {
- 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))
- }
+ this._swipeHelper = new Swipe(this._element, swipeConfig)
}
_keydown(event) {
diff --git a/js/src/util/swipe.js b/js/src/util/swipe.js
new file mode 100644
index 0000000000..321572eb8b
--- /dev/null
+++ b/js/src/util/swipe.js
@@ -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
diff --git a/js/tests/unit/carousel.spec.js b/js/tests/unit/carousel.spec.js
index 70b9b8f0f5..b048f3a882 100644
--- a/js/tests/unit/carousel.spec.js
+++ b/js/tests/unit/carousel.spec.js
@@ -2,6 +2,7 @@ import Carousel from '../../src/carousel'
import EventHandler from '../../src/dom/event-handler'
import { clearFixture, createEvent, getFixture, jQueryMock } from '../helpers/fixture'
import { isRTL, noop } from '../../src/util/index'
+import Swipe from '../../src/util/swipe'
describe('Carousel', () => {
const { Simulator, PointerEvent } = window
@@ -301,23 +302,24 @@ describe('Carousel', () => {
})
expect(carousel._addTouchEventListeners).not.toHaveBeenCalled()
+ expect(carousel._swipeHelper).toBeNull()
})
it('should not add touch event listeners if touch supported = false', () => {
fixtureEl.innerHTML = '<div></div>'
const carouselEl = fixtureEl.querySelector('div')
+ spyOn(Swipe, 'isSupported').and.returnValue(false)
const carousel = new Carousel(carouselEl)
-
- EventHandler.off(carouselEl, '.bs-carousel')
- carousel._touchSupported = false
+ EventHandler.off(carouselEl, Carousel.EVENT_KEY)
spyOn(carousel, '_addTouchEventListeners')
carousel._addEventListeners()
expect(carousel._addTouchEventListeners).not.toHaveBeenCalled()
+ expect(carousel._swipeHelper).toBeNull()
})
it('should add touch event listeners by default', () => {
@@ -566,7 +568,7 @@ describe('Carousel', () => {
}, () => {
restorePointerEvents()
delete document.documentElement.ontouchstart
- expect(carousel.touchDeltaX).toEqual(0)
+ expect(carousel._swipeHelper._deltaX).toEqual(0)
done()
})
})
@@ -1237,19 +1239,20 @@ describe('Carousel', () => {
const carouselEl = fixtureEl.querySelector('#myCarousel')
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
// to test that touch events are add/removed properly.
document.documentElement.ontouchstart = noop
const carousel = new Carousel(carouselEl)
+ const swipeHelperSpy = spyOn(carousel._swipeHelper, 'dispose').and.callThrough()
const expectedArgs = [
['keydown', jasmine.any(Function), jasmine.any(Boolean)],
['mouseover', 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)],
['pointerup', jasmine.any(Function), jasmine.any(Boolean)]
@@ -1265,7 +1268,8 @@ describe('Carousel', () => {
carousel.dispose()
- expect(removeEventSpy.calls.allArgs()).toEqual(expectedArgs)
+ expect(removeEventSpy).toHaveBeenCalledWith(carouselEl, Carousel.EVENT_KEY)
+ expect(swipeHelperSpy).toHaveBeenCalled()
delete document.documentElement.ontouchstart
})
diff --git a/js/tests/unit/util/swipe.spec.js b/js/tests/unit/util/swipe.spec.js
new file mode 100644
index 0000000000..5690319ffc
--- /dev/null
+++ b/js/tests/unit/util/swipe.spec.js
@@ -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()
+ })
+ })
+})