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 true Whether the carousel should cycle continuously or have hard stops. + + touch + boolean + true + Whether the carousel should handle touch event and allow swipe left/right. + diff --git a/site/docs/4.1/getting-started/contents.md b/site/docs/4.1/getting-started/contents.md index fd38e2fba5..0c210d0438 100644 --- a/site/docs/4.1/getting-started/contents.md +++ b/site/docs/4.1/getting-started/contents.md @@ -38,7 +38,7 @@ bootstrap/ └── bootstrap.min.js.map {% endhighlight %} -This is the most basic form of Bootstrap: precompiled files for quick drop-in usage in nearly any web project. 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/javascript/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/). +This is the most basic form of Bootstrap: precompiled files for quick drop-in usage in nearly any web project. 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/javascript/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/). ## CSS files diff --git a/site/docs/4.1/getting-started/introduction.md b/site/docs/4.1/getting-started/introduction.md index 55ced2f9c3..0ad7973010 100644 --- a/site/docs/4.1/getting-started/introduction.md +++ b/site/docs/4.1/getting-started/introduction.md @@ -37,7 +37,7 @@ We use [jQuery's slim build](https://blog.jquery.com/2016/06/09/jquery-3-0-final Curious which components explicitly require jQuery, our JS, and Popper.js? Click the show components link below. If you're at all unsure about the general page structure, keep reading for an example page template. -Our `bootstrap.bundle.js` and `bootstrap.bundle.min.js` include [Popper](https://popper.js.org/), but not [jQuery](https://jquery.com/). For more information about what's included in Bootstrap, please see our [contents]({{ site.baseurl }}/docs/{{ site.docs_version }}/getting-started/contents/#precompiled-bootstrap) section. +Our `bootstrap.bundle.js` and `bootstrap.bundle.min.js` include [Popper](https://popper.js.org/) and [HammerJS](https://hammerjs.github.io/), but not [jQuery](https://jquery.com/). For more information about what's included in Bootstrap, please see our [contents]({{ site.baseurl }}/docs/{{ site.docs_version }}/getting-started/contents/#precompiled-bootstrap) section.
Show components requiring JavaScript