diff --git a/src/lib/viewers/media/MediaBaseViewer.js b/src/lib/viewers/media/MediaBaseViewer.js index 390b05dfe..1f376a887 100644 --- a/src/lib/viewers/media/MediaBaseViewer.js +++ b/src/lib/viewers/media/MediaBaseViewer.js @@ -92,9 +92,10 @@ const EMIT_WAIT_TIME_IN_MILLIS = 100; this.mediaUrl = this.createContentUrlWithAuthParams(template); this.mediaEl.addEventListener('loadeddata', this.loadeddataHandler); this.mediaEl.addEventListener('error', this.errorHandler); + this.mediaEl.setAttribute('title', this.options.file.name); if (Browser.isIOS()) { - // iOS doesn't fire loadeddata event till some data loads + // iOS doesn't fire loadeddata event until some data loads // Adding autoplay helps with that and itself won't autoplay. // https://webkit.org/blog/6784/new-video-policies-for-ios/ this.mediaEl.autoplay = true; @@ -155,6 +156,7 @@ const EMIT_WAIT_TIME_IN_MILLIS = 100; * Handles media element loading errors. * * @private + * @param {Error} err - error object * @emits error * @return {void} */ @@ -263,6 +265,7 @@ const EMIT_WAIT_TIME_IN_MILLIS = 100; * * @private * @param {double} time - Time in seconds + * @return {void} */ setMediaTime(time) { this.mediaEl.currentTime = time; @@ -273,6 +276,7 @@ const EMIT_WAIT_TIME_IN_MILLIS = 100; * * @private * @param {number} volume - Must be a number between [0,1], per HTML5 spec + * @return {void} */ setVolume(volume) { cache.set(MEDIA_VOLUME_CACHE_KEY, volume); diff --git a/src/lib/viewers/media/Scrubber.js b/src/lib/viewers/media/Scrubber.js index 3abdea085..a4870aa7f 100644 --- a/src/lib/viewers/media/Scrubber.js +++ b/src/lib/viewers/media/Scrubber.js @@ -1,10 +1,12 @@ import autobind from 'autobind-decorator'; import EventEmitter from 'events'; import scrubberTemplate from './Scrubber.html'; +import Browser from '../../Browser'; const MIN_VALUE = 0; const MAX_VALUE = 1; const CLASS_SCRUBBER_HOVER = 'bp-media-scrubber-hover'; +const CLASS_SCRUBBER_TOUCH = 'bp-media-scrubber-touch'; @autobind class Scrubber extends EventEmitter { /** @@ -56,14 +58,21 @@ const CLASS_SCRUBBER_HOVER = 'bp-media-scrubber-hover'; this.playedEl = this.scrubberEl.querySelector('.bp-media-scrubber-played'); this.handleEl = this.scrubberEl.querySelector('.bp-media-scrubber-handle'); + this.hasTouch = Browser.hasTouch(); + // Set the provided initial values this.setConvertedValue(convertedValue); this.setBufferedValue(bufferedValue); this.setValue(value); - this.playedEl.addEventListener('mousedown', this.mouseDownHandler); - this.convertedEl.addEventListener('mousedown', this.mouseDownHandler); - this.handleEl.addEventListener('mousedown', this.mouseDownHandler); + this.playedEl.addEventListener('mousedown', this.pointerDownHandler); + this.convertedEl.addEventListener('mousedown', this.pointerDownHandler); + this.handleEl.addEventListener('mousedown', this.pointerDownHandler); + + if (this.hasTouch) { + this.scrubberContainerEl.addEventListener('touchstart', this.pointerDownHandler); + this.scrubberWrapperEl.classList.add(CLASS_SCRUBBER_TOUCH); + } } /** @@ -74,9 +83,11 @@ const CLASS_SCRUBBER_HOVER = 'bp-media-scrubber-hover'; destroy() { this.removeAllListeners(); this.destroyDocumentHandlers(); - this.playedEl.removeEventListener('mousedown', this.mouseDownHandler); - this.convertedEl.removeEventListener('mousedown', this.mouseDownHandler); - this.handleEl.removeEventListener('mousedown', this.mouseDownHandler); + this.playedEl.removeEventListener('mousedown', this.pointerDownHandler); + this.convertedEl.removeEventListener('mousedown', this.pointerDownHandler); + this.handleEl.removeEventListener('mousedown', this.pointerDownHandler); + this.scrubberContainerEl.removeEventListener('touchstart', this.pointerDownHandler); + this.scrubberContainerEl = undefined; this.scrubberWrapperEl = undefined; this.scrubberEl = undefined; @@ -100,8 +111,8 @@ const CLASS_SCRUBBER_HOVER = 'bp-media-scrubber-hover'; /** * Set aria-valuenow and aria-valuetext attributes * - * @param {number} ariaValuenow - * @param {string} ariaValuetext + * @param {number} ariaValuenow - value for aria 'aria-valuenow' + * @param {string} ariaValuetext - value for aria 'aria-valuetext' * @return {void} */ setAriaValues(ariaValuenow, ariaValuetext) { @@ -191,7 +202,18 @@ const CLASS_SCRUBBER_HOVER = 'bp-media-scrubber-hover'; * @return {void} */ scrubbingHandler(event) { - const pageX = event.pageX; + // Stops vertical scrolling when scrubbing + event.preventDefault(); + + let pageX = event.pageX; + + // Android Chrome fires both mousedown events and touchstart events. The touch start event + // does not include pageX, but pageX can be found in the touches list which is present for + // touch events across all browsers. + if (event.touches) { + pageX = event.touches[0].pageX; + } + const newValue = this.computeScrubberPosition(pageX); this.setValue(newValue); @@ -207,7 +229,7 @@ const CLASS_SCRUBBER_HOVER = 'bp-media-scrubber-hover'; * @param {Event} event - the instance of the class * @return {void} */ - mouseDownHandler(event) { + pointerDownHandler(event) { // If this is not a left click, then ignore // If this is a CTRL or CMD click, then ignore if ((typeof event.button !== 'number' || event.button < 2) && !event.ctrlKey && !event.metaKey) { @@ -215,11 +237,17 @@ const CLASS_SCRUBBER_HOVER = 'bp-media-scrubber-hover'; // All events are attached to the document so that the user doesn't have to keep the mouse // over the scrubber bar and has wiggle room. If the wiggling causes the mouse to leave // the whole view (embed use case) then we stop tracking all events. - document.addEventListener('mouseup', this.mouseUpHandler); - document.addEventListener('mouseleave', this.mouseUpHandler); + document.addEventListener('mouseup', this.pointerUpHandler); + document.addEventListener('mouseleave', this.pointerUpHandler); document.addEventListener('mousemove', this.scrubbingHandler); - this.scrubberWrapperEl.classList.add(CLASS_SCRUBBER_HOVER); + if (this.hasTouch) { + document.addEventListener('touchmove', this.scrubbingHandler); + document.addEventListener('touchend', this.pointerUpHandler); + } else { + this.scrubberWrapperEl.classList.add(CLASS_SCRUBBER_HOVER); + } + event.preventDefault(); } } @@ -230,7 +258,7 @@ const CLASS_SCRUBBER_HOVER = 'bp-media-scrubber-hover'; * @private * @return {void} */ - mouseUpHandler() { + pointerUpHandler() { this.scrubberWrapperEl.classList.remove(CLASS_SCRUBBER_HOVER); this.destroyDocumentHandlers(); } @@ -242,8 +270,13 @@ const CLASS_SCRUBBER_HOVER = 'bp-media-scrubber-hover'; */ destroyDocumentHandlers() { document.removeEventListener('mousemove', this.scrubbingHandler); - document.removeEventListener('mouseup', this.mouseUpHandler); - document.removeEventListener('mouseleave', this.mouseUpHandler); + document.removeEventListener('mouseup', this.pointerUpHandler); + document.removeEventListener('mouseleave', this.pointerUpHandler); + + if (this.hasTouch) { + document.removeEventListener('touchmove', this.scrubbingHandler); + document.removeEventListener('touchend', this.pointerUpHandler); + } } /** diff --git a/src/lib/viewers/media/Scrubber.scss b/src/lib/viewers/media/Scrubber.scss index c7a146fa9..d308db5eb 100644 --- a/src/lib/viewers/media/Scrubber.scss +++ b/src/lib/viewers/media/Scrubber.scss @@ -78,3 +78,14 @@ .bp-media-scrubber-container .bp-media-scrubber-hover .bp-media-scrubber-underlay { transform: scaleY(2); } + +.bp-media-scrubber-container .bp-media-scrubber-touch .bp-media-scrubber-handle { + transform: scale(1); +} + +.bp-media-scrubber-container .bp-media-scrubber-touch .bp-media-scrubber-converted, +.bp-media-scrubber-container .bp-media-scrubber-touch .bp-media-scrubber-buffered, +.bp-media-scrubber-container .bp-media-scrubber-touch .bp-media-scrubber-played, +.bp-media-scrubber-container .bp-media-scrubber-touch .bp-media-scrubber-underlay { + transform: scaleY(2); +} diff --git a/src/lib/viewers/media/__tests__/Scrubber-test.js b/src/lib/viewers/media/__tests__/Scrubber-test.js index aa058e54c..c0e4dbdf9 100644 --- a/src/lib/viewers/media/__tests__/Scrubber-test.js +++ b/src/lib/viewers/media/__tests__/Scrubber-test.js @@ -119,17 +119,35 @@ describe('lib/viewers/media/Scrubber', () => { }); describe('scrubbingHandler()', () => { - it('should adjust the scrubber value to the current scrubber handle position value in the video', () => { + beforeEach(() => { stubs.setValue = sandbox.stub(scrubber, 'setValue'); stubs.emit = sandbox.stub(scrubber, 'emit'); stubs.scrubberPosition = sandbox.stub(scrubber, 'computeScrubberPosition').returns(0.5); + stubs.event = { + pageX: 50, + preventDefault: sandbox.stub() + }; + }); + it('should adjust the scrubber value to the current scrubber handle position value in the video', () => { + scrubber.scrubbingHandler(stubs.event); - scrubber.scrubbingHandler({ pageX: 50 }); - + expect(stubs.event.preventDefault).to.be.called; expect(stubs.scrubberPosition).to.be.calledWith(50); expect(stubs.setValue).to.be.calledWith(0.5); expect(stubs.emit).to.be.calledWith('valuechange'); }); + + it('should use the touch list if the event contains touches', () => { + stubs.event.touches = [ + { + pageX: 55 + } + ]; + + scrubber.scrubbingHandler(stubs.event); + + expect(stubs.scrubberPosition).to.be.calledWith(55); + }); }); describe('computeScrubberPosition()', () => { @@ -167,7 +185,7 @@ describe('lib/viewers/media/Scrubber', () => { }); }); - describe('mouseDownHandler()', () => { + describe('pointerDownHandler()', () => { beforeEach(() => { stubs.scrub = sandbox.stub(scrubber, 'scrubbingHandler'); stubs.event = { @@ -179,35 +197,48 @@ describe('lib/viewers/media/Scrubber', () => { }); it('should ignore if event is not a left click', () => { - scrubber.mouseDownHandler(stubs.event); + scrubber.pointerDownHandler(stubs.event); expect(stubs.scrub).to.not.be.called; }); it('should ignore if event is a CTRL click', () => { stubs.event.ctrlKey = ''; - scrubber.mouseDownHandler(stubs.event); + scrubber.pointerDownHandler(stubs.event); expect(stubs.scrub).to.not.be.called; }); it('should ignore if event is a CMD click', () => { stubs.event.metaKey = ''; - scrubber.mouseDownHandler(stubs.event); + scrubber.pointerDownHandler(stubs.event); expect(stubs.scrub).to.not.be.called; }); it('should set the mouse move state to true and calls the mouse action handler', () => { + scrubber.hasTouch = false; stubs.event.button = 1; - scrubber.mouseDownHandler(stubs.event); + scrubber.pointerDownHandler(stubs.event); expect(stubs.scrub).to.be.calledWith(stubs.event); expect(scrubber.scrubberWrapperEl).to.have.class(CLASS_SCRUBBER_HOVER); }); + + it('should add touch events if the browser has touch', () => { + stubs.event.button = 1; + scrubber.hasTouch = true; + stubs.addEventListener = sandbox.stub(document, 'addEventListener'); + + scrubber.pointerDownHandler(stubs.event); + + expect(stubs.addEventListener).to.be.calledWith('touchmove', scrubber.scrubbingHandler); + expect(stubs.addEventListener).to.be.calledWith('touchend', scrubber.pointerUpHandler); + expect(scrubber.scrubberWrapperEl).to.not.have.class(CLASS_SCRUBBER_HOVER); + }); }); - describe('mouseUpHandler()', () => { + describe('pointerUpHandler()', () => { it('should set the mouse move state to false thus stopping mouse action handling', () => { stubs.destroy = sandbox.stub(scrubber, 'destroyDocumentHandlers'); - scrubber.mouseUpHandler(stubs.event); + scrubber.pointerUpHandler(stubs.event); expect(stubs.destroy).to.be.called; expect(scrubber.scrubberWrapperEl).to.not.have.class(CLASS_SCRUBBER_HOVER); }); @@ -219,8 +250,18 @@ describe('lib/viewers/media/Scrubber', () => { scrubber.destroyDocumentHandlers(); expect(stubs.remove).to.be.calledWith('mousemove', scrubber.scrubbingHandler); - expect(stubs.remove).to.be.calledWith('mouseup', scrubber.mouseUpHandler); - expect(stubs.remove).to.be.calledWith('mouseleave', scrubber.mouseUpHandler); + expect(stubs.remove).to.be.calledWith('mouseup', scrubber.pointerUpHandler); + expect(stubs.remove).to.be.calledWith('mouseleave', scrubber.pointerUpHandler); + }); + + it('should remove touch events if the browser has touch', () => { + scrubber.hasTouch = true; + stubs.remove = sandbox.stub(document, 'removeEventListener'); + + scrubber.destroyDocumentHandlers(); + + expect(stubs.remove).to.be.calledWith('touchmove', scrubber.scrubbingHandler); + expect(stubs.remove).to.be.calledWith('touchend', scrubber.pointerUpHandler); }); }); diff --git a/src/lib/viewers/media/_mediaBase.scss b/src/lib/viewers/media/_mediaBase.scss index 32b94505e..5f939c3cc 100644 --- a/src/lib/viewers/media/_mediaBase.scss +++ b/src/lib/viewers/media/_mediaBase.scss @@ -26,6 +26,7 @@ .bp-media-container { outline: 0 none; position: relative; + user-select: none; // Prevents copy paste dialog from appearing on mobile } .bp-media-play-button {