From 735c374e9c366446afb30e20592ac1d796c7bb69 Mon Sep 17 00:00:00 2001 From: Johann-S Date: Sun, 14 Oct 2018 23:10:13 +0200 Subject: [PATCH] use pointer events if available --- js/src/carousel.js | 77 +++++++++++++++++------ js/tests/unit/carousel.js | 125 +++++++++++++++++++++++++++++++++++++- package.json | 2 +- scss/_carousel.scss | 4 ++ 4 files changed, 185 insertions(+), 23 deletions(-) diff --git a/js/src/carousel.js b/js/src/carousel.js index 5b6209460a..f0ad83bb0b 100644 --- a/js/src/carousel.js +++ b/js/src/carousel.js @@ -56,22 +56,28 @@ const Event = { KEYDOWN : `keydown${EVENT_KEY}`, MOUSEENTER : `mouseenter${EVENT_KEY}`, MOUSELEAVE : `mouseleave${EVENT_KEY}`, - TOUCHEND : `touchend${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}`, 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 = { @@ -84,6 +90,11 @@ const Selector = { DATA_RIDE : '[data-ride="carousel"]' } +const PointerType = { + TOUCH : 'touch', + PEN : 'pen' +} + /** * ------------------------------------------------------------------------ * Class Definition @@ -103,7 +114,8 @@ class Carousel { this._config = this._getConfig(config) this._element = element this._indicatorsElement = this._element.querySelector(Selector.INDICATORS) - this._touchSupported = 'ontouchstart' in document.documentElement + this._touchSupported = 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0 + this._pointerEvent = Boolean(window.PointerEvent || window.MSPointerEvent) this._addEventListeners() } @@ -265,22 +277,35 @@ class Carousel { return } - $(this._element).on(Event.TOUCHSTART, (event) => { - this.touchStartX = event.originalEvent.touches[0].pageX - }) + const start = (event) => { + event.preventDefault() + const originEvent = event.originalEvent - $(this._element).on(Event.TOUCHMOVE, (event) => { + if (this._pointerEvent && (originEvent.pointerType === PointerType.TOUCH || originEvent.pointerType === PointerType.PEN)) { + this.touchStartX = originEvent.clientX + } else { + this.touchStartX = originEvent.touches[0].pageX + } + } + + const move = (event) => { event.preventDefault() // ensure swiping with one touch and not pinching - if (event.originalEvent.touches.length > 1) { + if (event.originalEvent.touches && event.originalEvent.touches.length > 1) { return } - this.touchDeltaX = event.originalEvent.touches[0].pageX - this.touchStartX - }) + if (!this._pointerEvent) { + this.touchDeltaX = event.originalEvent.touches[0].pageX - this.touchStartX + } + } + + const end = (event) => { + if (this._pointerEvent) { + this.touchDeltaX = event.originalEvent.clientX - this.touchStartX + } - $(this._element).on(Event.TOUCHEND, () => { this._handleSwipe() if (this._config.pause === 'hover') { @@ -298,7 +323,21 @@ class Carousel { } this.touchTimeout = setTimeout((event) => this.cycle(event), TOUCHEVENT_COMPAT_WAIT + this._config.interval) } - }) + } + + if (this._pointerEvent) { + $(this._element).on(Event.POINTERDOWN, (event) => start(event)) + $(this._element).on(Event.POINTERMOVE, (event) => move(event)) + $(this._element).on(Event.POINTERUP, (event) => end(event)) + $(this._element).on(Event.POINTERLEAVE, (event) => end(event)) + $(this._element).on(Event.POINTERCANCEL, (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/unit/carousel.js b/js/tests/unit/carousel.js index e416ab20e4..68c28aec5e 100644 --- a/js/tests/unit/carousel.js +++ b/js/tests/unit/carousel.js @@ -3,6 +3,26 @@ $(function () { window.Carousel = typeof bootstrap !== 'undefined' ? bootstrap.Carousel : Carousel + var originWinPointerEvent = window.PointerEvent + var originMsPointerEvent = window.MSPointerEvent + var supportPointerEvent = Boolean(window.PointerEvent || window.MSPointerEvent) + + function clearPointerEvents() { + window.PointerEvent = null + window.MSPointerEvent = null + } + + function restorePointerEvents() { + window.PointerEvent = originWinPointerEvent + window.MSPointerEvent = originMsPointerEvent + } + + var stylesCarousel = [ + '' + ].join('') + QUnit.module('carousel plugin') QUnit.test('should be defined on jQuery object', function (assert) { @@ -1006,8 +1026,55 @@ $(function () { }, 80) }) - QUnit.test('should allow swiperight and call prev', function (assert) { + QUnit.test('should allow swiperight and call prev with pointer events', function (assert) { + if (!supportPointerEvent) { + assert.expect(0) + return + } + + Simulator.setType('pointer') + assert.expect(3) + var $styles = $(stylesCarousel).appendTo('head') + var done = assert.async() + document.documentElement.ontouchstart = $.noop + + var carouselHTML = + '' + + var $carousel = $(carouselHTML) + $carousel.appendTo('#qunit-fixture') + var $item = $('#item') + $carousel.bootstrapCarousel() + var carousel = $carousel.data('bs.carousel') + var spy = sinon.spy(carousel, 'prev') + + $carousel.one('slid.bs.carousel', function () { + assert.ok(true, 'slid event fired') + assert.ok($item.hasClass('active')) + assert.ok(spy.called) + delete document.documentElement.ontouchstart + $styles.remove() + done() + }) + + Simulator.gestures.swipe($carousel[0], { + deltaX: 300, + deltaY: 0 + }) + }) + + QUnit.test('should allow swiperight and call prev with touch events', function (assert) { Simulator.setType('touch') + clearPointerEvents() assert.expect(3) var done = assert.async() document.documentElement.ontouchstart = $.noop @@ -1036,6 +1103,7 @@ $(function () { assert.ok($item.hasClass('active')) assert.ok(spy.called) delete document.documentElement.ontouchstart + restorePointerEvents() done() }) @@ -1045,8 +1113,56 @@ $(function () { }) }) - QUnit.test('should allow swipeleft and call next', function (assert) { + QUnit.test('should allow swipeleft and call next with pointer events', function (assert) { + if (!supportPointerEvent) { + assert.expect(0) + return + } + assert.expect(3) + Simulator.setType('pointer') + + var $styles = $(stylesCarousel).appendTo('head') + var done = assert.async() + document.documentElement.ontouchstart = $.noop + + var carouselHTML = + '' + + var $carousel = $(carouselHTML) + $carousel.appendTo('#qunit-fixture') + var $item = $('#item') + $carousel.bootstrapCarousel() + var carousel = $carousel.data('bs.carousel') + var spy = sinon.spy(carousel, 'next') + + $carousel.one('slid.bs.carousel', function () { + assert.ok(true, 'slid event fired') + assert.ok(!$item.hasClass('active')) + assert.ok(spy.called) + $styles.remove() + done() + }) + + Simulator.gestures.swipe($carousel[0], { + pos: [300, 10], + deltaX: -300, + deltaY: 0 + }) + }) + + QUnit.test('should allow swipeleft and call next with touch events', function (assert) { + assert.expect(3) + clearPointerEvents() Simulator.setType('touch') var done = assert.async() @@ -1075,6 +1191,7 @@ $(function () { assert.ok(true, 'slid event fired') assert.ok(!$item.hasClass('active')) assert.ok(spy.called) + restorePointerEvents() done() }) @@ -1085,8 +1202,9 @@ $(function () { }) }) - QUnit.test('should not allow pinch', function (assert) { + QUnit.test('should not allow pinch with touch events', function (assert) { assert.expect(0) + clearPointerEvents() Simulator.setType('touch') var done = assert.async() document.documentElement.ontouchstart = $.noop @@ -1102,6 +1220,7 @@ $(function () { deltaY: 0, touches: 2 }, function () { + restorePointerEvents() done() }) }) diff --git a/package.json b/package.json index c7302c490d..39e883d18b 100644 --- a/package.json +++ b/package.json @@ -190,7 +190,7 @@ }, { "path": "./dist/js/bootstrap.js", - "maxSize": "21 kB" + "maxSize": "22 kB" }, { "path": "./dist/js/bootstrap.min.js", diff --git a/scss/_carousel.scss b/scss/_carousel.scss index 97e792ea72..7d3728b40d 100644 --- a/scss/_carousel.scss +++ b/scss/_carousel.scss @@ -12,6 +12,10 @@ position: relative; } +.carousel.pointer-event { + touch-action: pan-x; +} + .carousel-inner { position: relative; width: 100%;