diff --git a/README.md b/README.md index 8694b686f1..17680e642d 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ bootstrap/ └── bootstrap.min.js.map ``` -We provide compiled CSS and JS (`bootstrap.*`), as well as compiled and minified CSS and JS (`bootstrap.min.*`). [source maps](https://developers.google.com/web/tools/chrome-devtools/debug/readability/source-maps) (`bootstrap.*.map`) are available for use with certain browsers' developer tools. Bundled JS files (`bootstrap.bundle.js` and minified `bootstrap.bundle.min.js`) include [Popper](https://popper.js.org/), but not [jQuery](https://jquery.com/). +We provide compiled CSS and JS (`bootstrap.*`), as well as compiled and minified CSS and JS (`bootstrap.min.*`). [source maps](https://developers.google.com/web/tools/chrome-devtools/debug/readability/source-maps) (`bootstrap.*.map`) are available for use with certain browsers' developer tools. Bundled JS files (`bootstrap.bundle.js` and minified `bootstrap.bundle.min.js`) include [Popper](https://popper.js.org/) and [HammerJS](https://hammerjs.github.io/), but not [jQuery](https://jquery.com/). ## Bugs and feature requests diff --git a/_config.yml b/_config.yml index e073dce9b9..7fab354119 100644 --- a/_config.yml +++ b/_config.yml @@ -58,6 +58,8 @@ cdn: jquery_hash: "sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" popper: "https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.4/umd/popper.min.js" popper_hash: "sha384-GM0Y80ecpwKxF1D5XCrGanKusGDy9WW0O2sSM84neB4iFhvKp3fwnoIRnPsQcN1R" + hammer: "https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.min.js" + hammer_hash: "sha384-Cs3dgUx6+jDxxuqHvVH8Onpyj2LF1gKZurLDlhqzuJmUqVYMJ0THTWpxK5Z086Zm" toc: min_level: 2 diff --git a/build/build-plugins.js b/build/build-plugins.js index 1de65b426d..299f502d9d 100644 --- a/build/build-plugins.js +++ b/build/build-plugins.js @@ -5,10 +5,10 @@ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) */ -const path = require('path') -const rollup = require('rollup') -const babel = require('rollup-plugin-babel') -const banner = require('./banner.js') +const path = require('path') +const rollup = require('rollup') +const babel = require('rollup-plugin-babel') +const banner = require('./banner.js') const TEST = process.env.NODE_ENV === 'test' const plugins = [ @@ -41,8 +41,9 @@ const rootPath = TEST ? '../js/coverage/dist/' : '../js/dist/' function build(plugin) { console.log(`Building ${plugin} plugin...`) - const external = ['jquery', 'popper.js'] + const external = ['hammerjs', 'jquery', 'popper.js'] const globals = { + hammerjs: 'Hammer', jquery: 'jQuery', // Ensure we use jQuery which is always available even in noConflict mode 'popper.js': 'Popper' } diff --git a/build/generate-sri.js b/build/generate-sri.js index 6929097703..13b90db1ce 100644 --- a/build/generate-sri.js +++ b/build/generate-sri.js @@ -42,6 +42,10 @@ const files = [ { file: 'node_modules/popper.js/dist/umd/popper.min.js', configPropertyName: 'popper_hash' + }, + { + file: 'node_modules/hammerjs/hammer.min.js', + configPropertyName: 'hammer_hash' } ] diff --git a/build/rollup.config.js b/build/rollup.config.js index c8acf7a9e9..72e3951fa6 100644 --- a/build/rollup.config.js +++ b/build/rollup.config.js @@ -1,12 +1,13 @@ -const path = require('path') -const babel = require('rollup-plugin-babel') -const resolve = require('rollup-plugin-node-resolve') -const banner = require('./banner.js') +const path = require('path') +const babel = require('rollup-plugin-babel') +const resolve = require('rollup-plugin-node-resolve') +const commonjs = require('rollup-plugin-commonjs') +const banner = require('./banner.js') const BUNDLE = process.env.BUNDLE === 'true' -let fileDest = 'bootstrap.js' -const external = ['jquery', 'popper.js'] +let fileDest = 'bootstrap.js' +const external = ['jquery', 'hammerjs', 'popper.js'] const plugins = [ babel({ exclude: 'node_modules/**', // Only transpile our source code @@ -21,15 +22,22 @@ const plugins = [ ] const globals = { jquery: 'jQuery', // Ensure we use jQuery which is always available even in noConflict mode + hammerjs: 'Hammer', 'popper.js': 'Popper' } if (BUNDLE) { fileDest = 'bootstrap.bundle.js' - // Remove last entry in external array to bundle Popper - external.pop() + // We just keep jQuery as external + external.length = 1 delete globals['popper.js'] - plugins.push(resolve()) + delete globals.hammerjs + plugins.push( + commonjs({ + include: 'node_modules/**' + }), + resolve() + ) } module.exports = { diff --git a/js/src/carousel.js b/js/src/carousel.js index fcc78af6f1..b2765ac5a8 100644 --- a/js/src/carousel.js +++ b/js/src/carousel.js @@ -1,4 +1,5 @@ import $ from 'jquery' +import Hammer from 'hammerjs' import Util from './util' /** @@ -23,13 +24,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 HAMMER_ENABLED = typeof Hammer !== 'undefined' const Default = { interval : 5000, keyboard : true, slide : false, pause : 'hover', - wrap : true + wrap : true, + touch : true } const DefaultType = { @@ -37,7 +40,8 @@ const DefaultType = { keyboard : 'boolean', slide : '(boolean|string)', pause : '(string|boolean)', - wrap : 'boolean' + wrap : 'boolean', + touch : 'boolean' } const Direction = { @@ -55,7 +59,9 @@ const Event = { 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 = { @@ -84,21 +90,30 @@ const Selector = { * Class Definition * ------------------------------------------------------------------------ */ - class Carousel { constructor(element, config) { - this._items = null - this._interval = null - this._activeElement = null + this._items = null + this._interval = null + this._activeElement = null + this._isPaused = false + this._isSliding = false + this.touchTimeout = null + this.hammer = null - this._isPaused = false - this._isSliding = false + 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.touchTimeout = null - - this._config = this._getConfig(config) - this._element = $(element)[0] - this._indicatorsElement = this._element.querySelector(Selector.INDICATORS) + if (HAMMER_ENABLED && this._touchSupported && this._config.touch) { + this.hammer = new Hammer(this._element, { + recognizers: [[ + Hammer.Swipe, { + direction: Hammer.DIRECTION_HORIZONTAL + } + ]] + }) + } this._addEventListeners() } @@ -226,11 +241,16 @@ class Carousel { .on(Event.KEYDOWN, (event) => this._keydown(event)) } + if (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 (this._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; diff --git a/js/tests/index.html b/js/tests/index.html index ce4a0e3081..201e15f2a8 100644 --- a/js/tests/index.html +++ b/js/tests/index.html @@ -20,6 +20,7 @@ }()) + @@ -28,6 +29,9 @@ + + + + + {%- if jekyll.environment == "production" -%} diff --git a/site/docs/4.1/components/carousel.md b/site/docs/4.1/components/carousel.md index 543b06430a..6bfb352069 100644 --- a/site/docs/4.1/components/carousel.md +++ b/site/docs/4.1/components/carousel.md @@ -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 supports swipe gestures (left and right) using [HammerJS]({{ site.cdn.hammer }}). For this to function correctly you need to include HammerJS before Bootstrap or use `bootstrap.bundle.min.js` / `bootstrap.bundle.js` which contains HammerJS. + 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). @@ -281,6 +283,12 @@ Options can be passed via data attributes or JavaScript. For data attributes, ap