diff --git a/README.md b/README.md index 17680e642d..8694b686f1 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/) and [HammerJS](https://hammerjs.github.io/), 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/), but not [jQuery](https://jquery.com/). ## Bugs and feature requests diff --git a/_config.yml b/_config.yml index 7fab354119..e073dce9b9 100644 --- a/_config.yml +++ b/_config.yml @@ -58,8 +58,6 @@ 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 299f502d9d..1de65b426d 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,9 +41,8 @@ const rootPath = TEST ? '../js/coverage/dist/' : '../js/dist/' function build(plugin) { console.log(`Building ${plugin} plugin...`) - const external = ['hammerjs', 'jquery', 'popper.js'] + const external = ['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 13b90db1ce..6929097703 100644 --- a/build/generate-sri.js +++ b/build/generate-sri.js @@ -42,10 +42,6 @@ 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 72e3951fa6..c8acf7a9e9 100644 --- a/build/rollup.config.js +++ b/build/rollup.config.js @@ -1,13 +1,12 @@ -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 path = require('path') +const babel = require('rollup-plugin-babel') +const resolve = require('rollup-plugin-node-resolve') +const banner = require('./banner.js') const BUNDLE = process.env.BUNDLE === 'true' -let fileDest = 'bootstrap.js' -const external = ['jquery', 'hammerjs', 'popper.js'] +let fileDest = 'bootstrap.js' +const external = ['jquery', 'popper.js'] const plugins = [ babel({ exclude: 'node_modules/**', // Only transpile our source code @@ -22,22 +21,15 @@ 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' - // We just keep jQuery as external - external.length = 1 + // Remove last entry in external array to bundle Popper + external.pop() delete globals['popper.js'] - delete globals.hammerjs - plugins.push( - commonjs({ - include: 'node_modules/**' - }), - resolve() - ) + plugins.push(resolve()) } module.exports = { diff --git a/js/src/carousel.js b/js/src/carousel.js index b2765ac5a8..5b6209460a 100644 --- a/js/src/carousel.js +++ b/js/src/carousel.js @@ -1,7 +1,3 @@ -import $ from 'jquery' -import Hammer from 'hammerjs' -import Util from './util' - /** * -------------------------------------------------------------------------- * Bootstrap (v4.1.3): carousel.js @@ -9,6 +5,9 @@ import Util from './util' * -------------------------------------------------------------------------- */ +import $ from 'jquery' +import Util from './util' + /** * ------------------------------------------------------------------------ * Constants @@ -24,7 +23,7 @@ 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 SWIPE_THRESHOLD = 40 const Default = { interval : 5000, @@ -58,10 +57,10 @@ const Event = { MOUSEENTER : `mouseenter${EVENT_KEY}`, MOUSELEAVE : `mouseleave${EVENT_KEY}`, TOUCHEND : `touchend${EVENT_KEY}`, + TOUCHSTART : `touchstart${EVENT_KEY}`, + TOUCHMOVE : `touchmove${EVENT_KEY}`, LOAD_DATA_API : `load${EVENT_KEY}${DATA_API_KEY}`, - CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`, - SWIPELEFT : 'swipeleft', - SWIPERIGHT : 'swiperight' + CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}` } const ClassName = { @@ -98,22 +97,13 @@ class Carousel { this._isPaused = false this._isSliding = false this.touchTimeout = null - this.hammer = null + this.touchStartX = 0 + this.touchDeltaX = 0 this._config = this._getConfig(config) this._element = element this._indicatorsElement = this._element.querySelector(Selector.INDICATORS) - this._touchSupported = 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0 - - if (HAMMER_ENABLED && this._touchSupported && this._config.touch) { - this.hammer = new Hammer(this._element, { - recognizers: [[ - Hammer.Swipe, { - direction: Hammer.DIRECTION_HORIZONTAL - } - ]] - }) - } + this._touchSupported = 'ontouchstart' in document.documentElement this._addEventListeners() } @@ -235,22 +225,65 @@ class Carousel { return config } + _handleSwipe() { + const absDeltax = Math.abs(this.touchDeltaX) + + if (absDeltax <= SWIPE_THRESHOLD) { + return + } + + const direction = absDeltax / this.touchDeltaX + + // swipe left + if (direction > 0) { + this.prev() + } + + // swipe right + if (direction < 0) { + this.next() + } + } + _addEventListeners() { if (this._config.keyboard) { $(this._element) .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 (this._touchSupported) { + } + + this._addTouchEventListeners() + } + + _addTouchEventListeners() { + if (!this._touchSupported) { + return + } + + $(this._element).on(Event.TOUCHSTART, (event) => { + this.touchStartX = event.originalEvent.touches[0].pageX + }) + + $(this._element).on(Event.TOUCHMOVE, (event) => { + event.preventDefault() + + // ensure swiping with one touch and not pinching + if (event.originalEvent.touches.length > 1) { + return + } + + this.touchDeltaX = event.originalEvent.touches[0].pageX - this.touchStartX + }) + + $(this._element).on(Event.TOUCHEND, () => { + this._handleSwipe() + + 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 // would stop cycling until user tapped out of it; @@ -258,15 +291,14 @@ class Carousel { // (as if it's the second time we tap on it, mouseenter compat event // is NOT fired) and after a timeout (to allow for mouse compatibility // events to fire) we explicitly restart cycling - $(this._element).on(Event.TOUCHEND, () => { - this.pause() - if (this.touchTimeout) { - clearTimeout(this.touchTimeout) - } - this.touchTimeout = setTimeout((event) => this.cycle(event), TOUCHEVENT_COMPAT_WAIT + this._config.interval) - }) + + this.pause() + if (this.touchTimeout) { + clearTimeout(this.touchTimeout) + } + this.touchTimeout = setTimeout((event) => this.cycle(event), TOUCHEVENT_COMPAT_WAIT + this._config.interval) } - } + }) } _keydown(event) { diff --git a/js/tests/index.html b/js/tests/index.html index 201e15f2a8..1bcdc5380e 100644 --- a/js/tests/index.html +++ b/js/tests/index.html @@ -20,7 +20,6 @@ }()) - diff --git a/js/tests/karma.conf.js b/js/tests/karma.conf.js index c3c64e8850..807f977d96 100644 --- a/js/tests/karma.conf.js +++ b/js/tests/karma.conf.js @@ -12,16 +12,16 @@ const jqueryFile = process.env.USE_OLD_JQUERY ? 'https://code.jquery.com/jquery- const bundle = process.env.BUNDLE === 'true' const browserStack = process.env.BROWSER === 'true' -const plugins = [ - 'karma-qunit', - 'karma-sinon' -] - const frameworks = [ 'qunit', 'sinon' ] +const plugins = [ + 'karma-qunit', + 'karma-sinon' +] + const reporters = ['dots'] const detectBrowsers = { @@ -48,7 +48,6 @@ const customLaunchers = { let files = [ 'node_modules/popper.js/dist/umd/popper.min.js', - 'node_modules/hammerjs/hammer.min.js', 'node_modules/hammer-simulator/index.js' ] diff --git a/js/tests/unit/carousel.js b/js/tests/unit/carousel.js index 0bc52219e4..e416ab20e4 100644 --- a/js/tests/unit/carousel.js +++ b/js/tests/unit/carousel.js @@ -3,8 +3,6 @@ $(function () { window.Carousel = typeof bootstrap !== 'undefined' ? bootstrap.Carousel : Carousel - var touchSupported = 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0 - QUnit.module('carousel plugin') QUnit.test('should be defined on jQuery object', function (assert) { @@ -1009,13 +1007,8 @@ $(function () { }) QUnit.test('should allow swiperight and call prev', function (assert) { - if (!touchSupported) { - assert.expect(0) - - return - } - - assert.expect(2) + Simulator.setType('touch') + assert.expect(3) var done = assert.async() document.documentElement.ontouchstart = $.noop @@ -1035,10 +1028,13 @@ $(function () { $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 done() }) @@ -1049,40 +1045,10 @@ $(function () { }) }) - QUnit.test('should not use HammerJS when touch option is false', function (assert) { - assert.expect(1) - - var $carousel = $('
').appendTo('#qunit-fixture') - $carousel.bootstrapCarousel({ - touch: false - }) - - var carousel = $carousel.data('bs.carousel') - - assert.strictEqual(carousel.hammer, null) - }) - - QUnit.test('should use HammerJS when touch option is true', function (assert) { - assert.expect(1) - - document.documentElement.ontouchstart = $.noop - - var $carousel = $('').appendTo('#qunit-fixture') - $carousel.bootstrapCarousel() - - var carousel = $carousel.data('bs.carousel') - - assert.ok(carousel.hammer !== null) - }) - QUnit.test('should allow swipeleft and call next', function (assert) { - if (!touchSupported) { - assert.expect(0) + assert.expect(3) + Simulator.setType('touch') - return - } - - assert.expect(2) var done = assert.async() document.documentElement.ontouchstart = $.noop @@ -1102,11 +1068,13 @@ $(function () { $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')) - delete document.documentElement.ontouchstart + assert.ok(spy.called) done() }) @@ -1116,4 +1084,25 @@ $(function () { deltaY: 0 }) }) + + QUnit.test('should not allow pinch', function (assert) { + assert.expect(0) + Simulator.setType('touch') + var done = assert.async() + document.documentElement.ontouchstart = $.noop + + var carouselHTML = '' + var $carousel = $(carouselHTML) + $carousel.appendTo('#qunit-fixture') + $carousel.bootstrapCarousel() + + Simulator.gestures.swipe($carousel[0], { + pos: [300, 10], + deltaX: -300, + deltaY: 0, + touches: 2 + }, function () { + done() + }) + }) }) diff --git a/js/tests/visual/carousel.html b/js/tests/visual/carousel.html index cd917caa62..630f870cf4 100644 --- a/js/tests/visual/carousel.html +++ b/js/tests/visual/carousel.html @@ -46,7 +46,6 @@ - - {%- if jekyll.environment == "production" -%} diff --git a/site/docs/4.1/components/carousel.md b/site/docs/4.1/components/carousel.md index 6bfb352069..b568a02cd5 100644 --- a/site/docs/4.1/components/carousel.md +++ b/site/docs/4.1/components/carousel.md @@ -12,8 +12,6 @@ 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). @@ -287,7 +285,7 @@ Options can be passed via data attributes or JavaScript. For data attributes, ap