Skip to content

Commit

Permalink
Add touch support on our carousel with Hammer
Browse files Browse the repository at this point in the history
  • Loading branch information
Johann-S committed Apr 10, 2018
1 parent 0871d69 commit 5823397
Show file tree
Hide file tree
Showing 12 changed files with 633 additions and 4 deletions.
2 changes: 2 additions & 0 deletions _config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ cdn:
jquery_hash: "sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
popper: "https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.0/umd/popper.min.js"
popper_hash: "sha384-cs/chFZiN24E4KMATLdqdvsezGxaGsi4hLGOzlXwp5UZB1LY//20VyM2taTB4QvJ"
hammer: "https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.min.js"
hammer_hash: "sha384-Cs3dgUx6+jDxxuqHvVH8Onpyj2LF1gKZurLDlhqzuJmUqVYMJ0THTWpxK5Z086Zm"

toc:
min_level: 2
Expand Down
2 changes: 2 additions & 0 deletions _includes/scripts.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<script>window.jQuery || document.write('<script src="{{ site.baseurl }}/assets/js/vendor/jquery-slim.min.js"><\/script>')</script>

<script src="{{ site.baseurl }}/assets/js/vendor/popper.min.js"{% if site.github %} integrity="{{ site.cdn.popper_hash }}" crossorigin="anonymous"{% endif %}></script>
<script src="{{ site.cdn.hammer }}"{% if site.github %} integrity="{{ site.cdn.hammer_hash }}" crossorigin="anonymous"{% endif %}></script>


{%- if site.github -%}
<script src="{{ site.baseurl }}/dist/js/bootstrap.min.js" integrity="{{ site.cdn.js_hash }}" crossorigin="anonymous"></script>
Expand Down
3 changes: 2 additions & 1 deletion build/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const BUNDLE = process.env.BUNDLE === 'true'
const year = new Date().getFullYear()

let fileDest = 'bootstrap.js'
const external = ['jquery', 'popper.js']
const external = ['jquery', 'hammerjs', 'popper.js']
const plugins = [
babel({
exclude: 'node_modules/**', // Only transpile our source code
Expand All @@ -24,6 +24,7 @@ const plugins = [
]
const globals = {
jquery: 'jQuery', // Ensure we use jQuery which is always available even in noConflict mode
hammerjs: 'Hammer',
'popper.js': 'Popper'
}

Expand Down
2 changes: 2 additions & 0 deletions docs/4.1/components/carousel.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ The carousel is a slideshow for cycling through a series of content, built with

In browsers where the [Page Visibility API](https://www.w3.org/TR/page-visibility/) is supported, the carousel will avoid sliding when the webpage is not visible to the user (such as when the browser tab is inactive, the browser window is minimized, etc.).

The carousel can handle swipe events (left and right) only if you include [HammerJS]({{ site.cdn.hammer }} before Bootstrap.

Please be aware that nested carousels are not supported, and carousels are generally not compliant with accessibility standards.

Lastly, if you're building our JavaScript from source, it [requires `util.js`]({{ site.baseurl }}/docs/{{ site.docs_version }}/getting-started/javascript/#util).
Expand Down
35 changes: 33 additions & 2 deletions js/src/carousel.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import $ from 'jquery'
import Hammer from 'hammerjs'
import Util from './util'

/**
Expand All @@ -24,6 +25,7 @@ const Carousel = (($) => {
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 HAMMER_ENABLED = typeof Hammer !== 'undefined'

const Default = {
interval : 5000,
Expand Down Expand Up @@ -56,7 +58,9 @@ const Carousel = (($) => {
MOUSELEAVE : `mouseleave${EVENT_KEY}`,
TOUCHEND : `touchend${EVENT_KEY}`,
LOAD_DATA_API : `load${EVENT_KEY}${DATA_API_KEY}`,
CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`
CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`,
SWIPELEFT : 'swipeleft',
SWIPERIGHT : 'swiperight'
}

const ClassName = {
Expand All @@ -71,6 +75,7 @@ const Carousel = (($) => {
}

const Selector = {
CAROUSEL : `.${ClassName.CAROUSEL}`,
ACTIVE : '.active',
ACTIVE_ITEM : '.active.carousel-item',
ITEM : '.carousel-item',
Expand All @@ -96,11 +101,23 @@ const Carousel = (($) => {
this._isSliding = false

this.touchTimeout = null
this.hammer = null

this._config = this._getConfig(config)
this._element = $(element)[0]
this._indicatorsElement = $(this._element).find(Selector.INDICATORS)[0]

if (HAMMER_ENABLED) {
this.hammer = new Hammer(this._element, {
recognizers: [[
Hammer.Swipe,
{
direction: Hammer.DIRECTION_HORIZONTAL
}
]]
})
}

this._addEventListeners()
}

Expand Down Expand Up @@ -222,16 +239,22 @@ const Carousel = (($) => {
}

_addEventListeners() {
const touchSupported = 'ontouchstart' in document.documentElement
if (this._config.keyboard) {
$(this._element)
.on(Event.KEYDOWN, (event) => this._keydown(event))
}

if (touchSupported && this.hammer) {
this.hammer.on(Event.SWIPELEFT, () => this.next())
this.hammer.on(Event.SWIPERIGHT, () => this.prev())
}

if (this._config.pause === 'hover') {
$(this._element)
.on(Event.MOUSEENTER, (event) => this.pause(event))
.on(Event.MOUSELEAVE, (event) => this.cycle(event))
if ('ontouchstart' in document.documentElement) {
if (touchSupported) {
// 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;
Expand Down Expand Up @@ -488,6 +511,14 @@ const Carousel = (($) => {
* ------------------------------------------------------------------------
*/

if (HAMMER_ENABLED) {
$(document).find(Selector.CAROUSEL)
.not(Selector.DATA_RIDE)
.each(function () {
Carousel._jQueryInterface.call($(this), $(this).data())
})
}

$(document)
.on(Event.CLICK_DATA_API, Selector.DATA_SLIDE, Carousel._dataApiClickHandler)

Expand Down
4 changes: 4 additions & 0 deletions js/tests/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
}())
</script>
<script src="../../assets/js/vendor/popper.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.min.js"></script>

<!-- QUnit -->
<link rel="stylesheet" href="../../node_modules/qunitjs/qunit/qunit.css" media="screen">
Expand All @@ -28,6 +29,9 @@
<!-- Sinon -->
<script src="../../node_modules/sinon/pkg/sinon-no-sourcemaps.js"></script>

<!-- Hammer simulator -->
<script src="vendor/hammer-simulator.js"></script>

<script>
// Disable jQuery event aliases to ensure we don't accidentally use any of them
[
Expand Down
2 changes: 2 additions & 0 deletions js/tests/karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ module.exports = (config) => {
files: [
jqueryFile,
'assets/js/vendor/popper.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.min.js',
'js/tests/vendor/hammer-simulator.js',
'js/coverage/dist/util.js',
'js/coverage/dist/tooltip.js',
'js/coverage/dist/!(util|index|tooltip).js', // include all of our js/dist files except util.js, index.js and tooltip.js
Expand Down
3 changes: 2 additions & 1 deletion js/tests/unit/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"sinon": false,
"Util": false,
"Alert": false,
"Button": false
"Button": false,
"Simulator": false
},
"parserOptions": {
"ecmaVersion": 5,
Expand Down
72 changes: 72 additions & 0 deletions js/tests/unit/carousel.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ $(function () {
afterEach: function () {
$.fn.carousel = $.fn.bootstrapCarousel
delete $.fn.bootstrapCarousel
$('.carousel').remove()
}
})

Expand Down Expand Up @@ -940,4 +941,75 @@ $(function () {
}, 80)
}, 80)
})

QUnit.test('should allow swiperight and call prev', function (assert) {
assert.expect(2)
var done = assert.async()
document.documentElement.ontouchstart = $.noop

var carouselHTML =
'<div class="carousel" data-interval="false">' +
' <div class="carousel-inner">' +
' <div id="item" class="carousel-item">' +
' <img alt="">' +
' </div>' +
' <div class="carousel-item active">' +
' <img alt="">' +
' </div>' +
' </div>' +
'</div>'

var $carousel = $(carouselHTML)
$carousel.appendTo('#qunit-fixture')
var $item = $('#item')
$carousel.bootstrapCarousel()

$carousel.one('slid.bs.carousel', function () {
assert.ok(true, 'slid event fired')
assert.ok($item.hasClass('active'))
delete document.documentElement.ontouchstart
done()
})

Simulator.gestures.swipe($carousel[0], {
deltaX: 300,
deltaY: 0
})
})

QUnit.test('should allow swipeleft and call next', function (assert) {
assert.expect(2)
var done = assert.async()
document.documentElement.ontouchstart = $.noop

var carouselHTML =
'<div class="carousel" data-interval="false">' +
' <div class="carousel-inner">' +
' <div id="item" class="carousel-item active">' +
' <img alt="">' +
' </div>' +
' <div class="carousel-item">' +
' <img alt="">' +
' </div>' +
' </div>' +
'</div>'

var $carousel = $(carouselHTML)
$carousel.appendTo('#qunit-fixture')
var $item = $('#item')
$carousel.bootstrapCarousel()

$carousel.one('slid.bs.carousel', function () {
assert.ok(true, 'slid event fired')
assert.ok(!$item.hasClass('active'))
delete document.documentElement.ontouchstart
done()
})

Simulator.gestures.swipe($carousel[0], {
pos: [300, 10],
deltaX: -300,
deltaY: 0
})
})
})
Loading

0 comments on commit 5823397

Please sign in to comment.