diff options
author | Sam Potts <sam@potts.es> | 2018-07-30 01:13:12 +1000 |
---|---|---|
committer | Sam Potts <sam@potts.es> | 2018-07-30 01:13:12 +1000 |
commit | 599b33e55fb4c0aec78cd0895bcd13f3fed12ad2 (patch) | |
tree | 52750660bd566202abd23b672640bd4a65abaed9 /src/js/listeners.js | |
parent | 3a8332bdb30e1f64047a0f8a3e60647f0473301b (diff) | |
download | plyr-599b33e55fb4c0aec78cd0895bcd13f3fed12ad2.tar.lz plyr-599b33e55fb4c0aec78cd0895bcd13f3fed12ad2.tar.xz plyr-599b33e55fb4c0aec78cd0895bcd13f3fed12ad2.zip |
Click to play fix, poster fix, iOS controls fixes
Diffstat (limited to 'src/js/listeners.js')
-rw-r--r-- | src/js/listeners.js | 432 |
1 files changed, 223 insertions, 209 deletions
diff --git a/src/js/listeners.js b/src/js/listeners.js index 0b803454..e19894ba 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -4,10 +4,12 @@ import controls from './controls'; import ui from './ui'; +import { repaint } from './utils/animation'; import browser from './utils/browser'; import { getElement, getElements, + hasClass, matches, toggleClass, toggleHidden, @@ -30,6 +32,7 @@ class Listeners { // Handle key presses handleKey(event) { + const { player } = this; const code = event.keyCode ? event.keyCode : event.which; const pressed = event.type === 'keydown'; const repeat = pressed && code === this.lastKey; @@ -48,7 +51,7 @@ class Listeners { // Seek by the number keys const seekByKey = () => { // Divide the max duration into 10th's and times by the number value - this.player.currentTime = this.player.duration / 10 * (code - 48); + player.currentTime = player.duration / 10 * (code - 48); }; // Handle the key on keydown @@ -59,8 +62,8 @@ class Listeners { // and any that accept key input http://webaim.org/techniques/keyboard/ const focused = document.activeElement; if (is.element(focused)) { - const { editable } = this.player.config.selectors; - const { seek } = this.player.elements.inputs; + const { editable } = player.config.selectors; + const { seek } = player.elements.inputs; if (focused !== seek && matches(focused, editable)) { return; @@ -126,52 +129,52 @@ class Listeners { case 75: // Space and K key if (!repeat) { - this.player.togglePlay(); + player.togglePlay(); } break; case 38: // Arrow up - this.player.increaseVolume(0.1); + player.increaseVolume(0.1); break; case 40: // Arrow down - this.player.decreaseVolume(0.1); + player.decreaseVolume(0.1); break; case 77: // M key if (!repeat) { - this.player.muted = !this.player.muted; + player.muted = !player.muted; } break; case 39: // Arrow forward - this.player.forward(); + player.forward(); break; case 37: // Arrow back - this.player.rewind(); + player.rewind(); break; case 70: // F key - this.player.fullscreen.toggle(); + player.fullscreen.toggle(); break; case 67: // C key if (!repeat) { - this.player.toggleCaptions(); + player.toggleCaptions(); } break; case 76: // L key - this.player.loop = !this.player.loop; + player.loop = !player.loop; break; /* case 73: @@ -193,11 +196,11 @@ class Listeners { // Escape is handle natively when in full screen // So we only need to worry about non native if ( - !this.player.fullscreen.enabled && - this.player.fullscreen.active && + !player.fullscreen.enabled && + player.fullscreen.active && code === 27 ) { - this.player.fullscreen.toggle(); + player.fullscreen.toggle(); } // Store last code for next cycle @@ -214,17 +217,21 @@ class Listeners { // Device is touch enabled firstTouch() { - this.player.touch = true; + const { player } = this; + + player.touch = true; // Add touch class toggleClass( - this.player.elements.container, - this.player.config.classNames.isTouch, + player.elements.container, + player.config.classNames.isTouch, true, ); } setTabFocus(event) { + const { player } = this; + clearTimeout(this.focusTimer); // Ignore any key other than tab @@ -239,8 +246,8 @@ class Listeners { // Remove current classes const removeCurrent = () => { - const className = this.player.config.classNames.tabFocus; - const current = getElements.call(this.player, `.${className}`); + const className = player.config.classNames.tabFocus; + const current = getElements.call(player, `.${className}`); toggleClass(current, className, false); }; @@ -257,18 +264,17 @@ class Listeners { // Delay the adding of classname until the focus has changed // This event fires before the focusin event - this.focusTimer = setTimeout(() => { const focused = document.activeElement; // Ignore if current focus element isn't inside the player - if (!this.player.elements.container.contains(focused)) { + if (!player.elements.container.contains(focused)) { return; } toggleClass( document.activeElement, - this.player.config.classNames.tabFocus, + player.config.classNames.tabFocus, true, ); }, 10); @@ -276,10 +282,12 @@ class Listeners { // Global window & document listeners global(toggle = true) { + const { player } = this; + // Keyboard shortcuts - if (this.player.config.keyboard.global) { + if (player.config.keyboard.global) { toggleListener.call( - this.player, + player, window, 'keydown keyup', this.handleKey, @@ -290,7 +298,7 @@ class Listeners { // Click anywhere closes menu toggleListener.call( - this.player, + player, document.body, 'click', this.toggleMenu, @@ -298,11 +306,11 @@ class Listeners { ); // Detect touch by events - once.call(this.player, document.body, 'touchstart', this.firstTouch); + once.call(player, document.body, 'touchstart', this.firstTouch); // Tab focus detection toggleListener.call( - this.player, + player, document.body, 'keydown focus blur', this.setTabFocus, @@ -314,14 +322,13 @@ class Listeners { // Container listeners container() { + const { player } = this; + // Keyboard shortcuts - if ( - !this.player.config.keyboard.global && - this.player.config.keyboard.focused - ) { + if (!player.config.keyboard.global && player.config.keyboard.focused) { on.call( - this.player, - this.player.elements.container, + player, + player.elements.container, 'keydown keyup', this.handleKey, false, @@ -330,11 +337,11 @@ class Listeners { // Toggle controls on mouse events and entering fullscreen on.call( - this.player, - this.player.elements.container, + player, + player.elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', event => { - const { controls } = this.player.elements; + const { controls } = player.elements; // Remove button states for fullscreen if (event.type === 'enterfullscreen') { @@ -350,17 +357,17 @@ class Listeners { let delay = 0; if (show) { - ui.toggleControls.call(this.player, true); + ui.toggleControls.call(player, true); // Use longer timeout for touch devices - delay = this.player.touch ? 3000 : 2000; + delay = player.touch ? 3000 : 2000; } // Clear timer - clearTimeout(this.player.timers.controls); + clearTimeout(player.timers.controls); // Set new timer to prevent flicker when seeking - this.player.timers.controls = setTimeout( - () => ui.toggleControls.call(this.player, false), + player.timers.controls = setTimeout( + () => ui.toggleControls.call(player, false), delay, ); }, @@ -369,100 +376,89 @@ class Listeners { // Listen for media events media() { + const { player } = this; + // Time change on media - on.call( - this.player, - this.player.media, - 'timeupdate seeking seeked', - event => controls.timeUpdate.call(this.player, event), + on.call(player, player.media, 'timeupdate seeking seeked', event => + controls.timeUpdate.call(player, event), ); // Display duration on.call( - this.player, - this.player.media, + player, + player.media, 'durationchange loadeddata loadedmetadata', - event => controls.durationUpdate.call(this.player, event), + event => controls.durationUpdate.call(player, event), ); // Check for audio tracks on load // We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point - on.call(this.player, this.player.media, 'canplay', () => { - toggleHidden(this.player.elements.volume, !this.player.hasAudio); - toggleHidden( - this.player.elements.buttons.mute, - !this.player.hasAudio, - ); + on.call(player, player.media, 'canplay', () => { + toggleHidden(player.elements.volume, !player.hasAudio); + toggleHidden(player.elements.buttons.mute, !player.hasAudio); }); // Handle the media finishing - on.call(this.player, this.player.media, 'ended', () => { + on.call(player, player.media, 'ended', () => { // Show poster on end - if ( - this.player.isHTML5 && - this.player.isVideo && - this.player.config.resetOnEnd - ) { + if (player.isHTML5 && player.isVideo && player.config.resetOnEnd) { // Restart - this.player.restart(); + player.restart(); } }); // Check for buffer progress on.call( - this.player, - this.player.media, + player, + player.media, 'progress playing seeking seeked', - event => controls.updateProgress.call(this.player, event), + event => controls.updateProgress.call(player, event), ); // Handle volume changes - on.call(this.player, this.player.media, 'volumechange', event => - controls.updateVolume.call(this.player, event), + on.call(player, player.media, 'volumechange', event => + controls.updateVolume.call(player, event), ); // Handle play/pause on.call( - this.player, - this.player.media, + player, + player.media, 'playing play pause ended emptied timeupdate', - event => ui.checkPlaying.call(this.player, event), + event => ui.checkPlaying.call(player, event), ); // Loading state - on.call( - this.player, - this.player.media, - 'waiting canplay seeked playing', - event => ui.checkLoading.call(this.player, event), + on.call(player, player.media, 'waiting canplay seeked playing', event => + ui.checkLoading.call(player, event), ); // If autoplay, then load advertisement if required // TODO: Show some sort of loading state while the ad manager loads else there's a delay before ad shows - on.call(this.player, this.player.media, 'playing', () => { - if (!this.player.ads) { + on.call(player, player.media, 'playing', () => { + if (!player.ads) { return; } // If ads are enabled, wait for them first - if (this.player.ads.enabled && !this.player.ads.initialized) { + if (player.ads.enabled && !player.ads.initialized) { // Wait for manager response - this.player.ads.managerPromise - .then(() => this.player.ads.play()) - .catch(() => this.player.play()); + player.ads.managerPromise + .then(() => player.ads.play()) + .catch(() => player.play()); } }); // Click video if ( - this.player.supported.ui && - this.player.config.clickToPlay && - !this.player.isAudio + player.supported.ui && + player.config.clickToPlay && + !player.isAudio ) { // Re-fetch the wrapper const wrapper = getElement.call( - this.player, - `.${this.player.config.classNames.video}`, + player, + `.${player.config.classNames.video}`, ); // Bail if there's no wrapper (this should never happen) @@ -471,32 +467,49 @@ class Listeners { } // On click play, pause ore restart - on.call(this.player, wrapper, 'click', () => { - // Touch devices will just show controls (if we're hiding controls) - if ( - this.player.config.hideControls && - this.player.touch && - !this.player.paused - ) { - return; - } + on.call( + player, + player.elements.container, + 'click touchstart', + event => { + const targets = [player.elements.container, wrapper]; + + // Ignore if click if not container or in video wrapper + if ( + !targets.includes(event.target) && + !wrapper.contains(event.target) + ) { + return; + } - if (this.player.paused) { - this.player.play(); - } else if (this.player.ended) { - this.player.restart(); - this.player.play(); - } else { - this.player.pause(); - } - }); + // First touch on touch devices will just show controls (if we're hiding controls) + // If controls are shown then it'll toggle like a pointer device + if ( + player.config.hideControls && + player.touch && + hasClass( + player.elements.container, + player.config.classNames.hideControls, + ) + ) { + return; + } + + if (player.ended) { + player.restart(); + player.play(); + } else { + player.togglePlay(); + } + }, + ); } // Disable right click - if (this.player.supported.ui && this.player.config.disableContextMenu) { + if (player.supported.ui && player.config.disableContextMenu) { on.call( - this.player, - this.player.elements.wrapper, + player, + player.elements.wrapper, 'contextmenu', event => { event.preventDefault(); @@ -506,34 +519,34 @@ class Listeners { } // Volume change - on.call(this.player, this.player.media, 'volumechange', () => { + on.call(player, player.media, 'volumechange', () => { // Save to storage - this.player.storage.set({ - volume: this.player.volume, - muted: this.player.muted, + player.storage.set({ + volume: player.volume, + muted: player.muted, }); }); // Speed change - on.call(this.player, this.player.media, 'ratechange', () => { + on.call(player, player.media, 'ratechange', () => { // Update UI - controls.updateSetting.call(this.player, 'speed'); + controls.updateSetting.call(player, 'speed'); // Save to storage - this.player.storage.set({ speed: this.player.speed }); + player.storage.set({ speed: player.speed }); }); // Quality request - on.call(this.player, this.player.media, 'qualityrequested', event => { + on.call(player, player.media, 'qualityrequested', event => { // Save to storage - this.player.storage.set({ quality: event.detail.quality }); + player.storage.set({ quality: event.detail.quality }); }); // Quality change - on.call(this.player, this.player.media, 'qualitychange', event => { + on.call(player, player.media, 'qualitychange', event => { // Update UI controls.updateSetting.call( - this.player, + player, 'quality', null, event.detail.quality, @@ -542,21 +555,21 @@ class Listeners { // Proxy events to container // Bubble up key events for Edge - const proxyEvents = this.player.config.events + const proxyEvents = player.config.events .concat(['keyup', 'keydown']) .join(' '); - on.call(this.player, this.player.media, proxyEvents, event => { + on.call(player, player.media, proxyEvents, event => { let { detail = {} } = event; // Get error details from media if (event.type === 'error') { - detail = this.player.media.error; + detail = player.media.error; } triggerEvent.call( - this.player, - this.player.elements.container, + player, + player.elements.container, event.type, true, detail, @@ -566,28 +579,30 @@ class Listeners { // Run default and custom handlers proxy(event, defaultHandler, customHandlerKey) { - const customHandler = this.player.config.listeners[customHandlerKey]; + const { player } = this; + const customHandler = player.config.listeners[customHandlerKey]; const hasCustomHandler = is.function(customHandler); let returned = true; // Execute custom handler if (hasCustomHandler) { - returned = customHandler.call(this.player, event); + returned = customHandler.call(player, event); } // Only call default handler if not prevented in custom handler if (returned && is.function(defaultHandler)) { - defaultHandler.call(this.player, event); + defaultHandler.call(player, event); } } // Trigger custom and default handlers bind(element, type, defaultHandler, customHandlerKey, passive = true) { - const customHandler = this.player.config.listeners[customHandlerKey]; + const { player } = this; + const customHandler = player.config.listeners[customHandlerKey]; const hasCustomHandler = is.function(customHandler); on.call( - this.player, + player, element, type, event => this.proxy(event, defaultHandler, customHandlerKey), @@ -597,91 +612,93 @@ class Listeners { // Listen for control events controls() { + const { player } = this; + // IE doesn't support input event, so we fallback to change const inputEvent = browser.isIE ? 'change' : 'input'; // Play/pause toggle - if (this.player.elements.buttons.play) { - Array.from(this.player.elements.buttons.play).forEach(button => { - this.bind(button, 'click', this.player.togglePlay, 'play'); + if (player.elements.buttons.play) { + Array.from(player.elements.buttons.play).forEach(button => { + this.bind(button, 'click', player.togglePlay, 'play'); }); } // Pause this.bind( - this.player.elements.buttons.restart, + player.elements.buttons.restart, 'click', - this.player.restart, + player.restart, 'restart', ); // Rewind this.bind( - this.player.elements.buttons.rewind, + player.elements.buttons.rewind, 'click', - this.player.rewind, + player.rewind, 'rewind', ); // Rewind this.bind( - this.player.elements.buttons.fastForward, + player.elements.buttons.fastForward, 'click', - this.player.forward, + player.forward, 'fastForward', ); // Mute toggle this.bind( - this.player.elements.buttons.mute, + player.elements.buttons.mute, 'click', () => { - this.player.muted = !this.player.muted; + player.muted = !player.muted; }, 'mute', ); // Captions toggle - this.bind(this.player.elements.buttons.captions, 'click', () => - this.player.toggleCaptions(), + this.bind(player.elements.buttons.captions, 'click', () => + player.toggleCaptions(), ); // Fullscreen toggle this.bind( - this.player.elements.buttons.fullscreen, + player.elements.buttons.fullscreen, 'click', () => { - this.player.fullscreen.toggle(); + player.fullscreen.toggle(); }, 'fullscreen', ); // Picture-in-Picture this.bind( - this.player.elements.buttons.pip, + player.elements.buttons.pip, 'click', () => { - this.player.pip = 'toggle'; + player.pip = 'toggle'; }, 'pip', ); // Airplay this.bind( - this.player.elements.buttons.airplay, + player.elements.buttons.airplay, 'click', - this.player.airplay, + player.airplay, 'airplay', ); // Settings menu - click toggle - this.bind(this.player.elements.buttons.settings, 'click', event => { - controls.toggleMenu.call(this.player, event); + this.bind(player.elements.buttons.settings, 'click', event => { + controls.toggleMenu.call(player, event); }); // Settings menu - keyboard toggle this.bind( - this.player.elements.buttons.settings, + player.elements.buttons.settings, 'keydown', event => { // We only care about space @@ -696,33 +713,28 @@ class Listeners { event.stopPropagation(); // Toggle menu - controls.toggleMenu.call(this.player, event); + controls.toggleMenu.call(player, event); }, null, false, ); // Set range input alternative "value", which matches the tooltip time (#954) - this.bind( - this.player.elements.inputs.seek, - 'mousedown mousemove', - event => { - const clientRect = this.player.elements.progress.getBoundingClientRect(); - const percent = - 100 / clientRect.width * (event.pageX - clientRect.left); - event.currentTarget.setAttribute('seek-value', percent); - }, - ); + this.bind(player.elements.inputs.seek, 'mousedown mousemove', event => { + const rect = player.elements.progress.getBoundingClientRect(); + const percent = 100 / rect.width * (event.pageX - rect.left); + event.currentTarget.setAttribute('seek-value', percent); + }); // Pause while seeking this.bind( - this.player.elements.inputs.seek, + player.elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => { const seek = event.currentTarget; - const code = event.keyCode ? event.keyCode : event.which; const eventType = event.type; + const attribute = 'play-on-seeked'; if ( (eventType === 'keydown' || eventType === 'keyup') && @@ -731,7 +743,7 @@ class Listeners { return; } // Was playing before? - const play = seek.hasAttribute('play-on-seeked'); + const play = seek.hasAttribute(attribute); // Done seeking const done = ['mouseup', 'touchend', 'keyup'].includes( @@ -740,18 +752,18 @@ class Listeners { // If we're done seeking and it was playing, resume playback if (play && done) { - seek.removeAttribute('play-on-seeked'); - this.player.play(); - } else if (!done && this.player.playing) { - seek.setAttribute('play-on-seeked', ''); - this.player.pause(); + seek.removeAttribute(attribute); + player.play(); + } else if (!done && player.playing) { + seek.setAttribute(attribute, ''); + player.pause(); } }, ); // Seek this.bind( - this.player.elements.inputs.seek, + player.elements.inputs.seek, inputEvent, event => { const seek = event.currentTarget; @@ -765,8 +777,13 @@ class Listeners { seek.removeAttribute('seek-value'); - this.player.currentTime = - seekTo / seek.max * this.player.duration; + // Super weird iOS bug where after you interact with an <input type="range">, + // it takes over further interactions on the page. This is a hack + if (browser.isIos) { + repaint(seek); + } + + player.currentTime = seekTo / seek.max * player.duration; }, 'seek', ); @@ -774,65 +791,61 @@ class Listeners { // Current time invert // Only if one time element is used for both currentTime and duration if ( - this.player.config.toggleInvert && - !is.element(this.player.elements.display.duration) + player.config.toggleInvert && + !is.element(player.elements.display.duration) ) { - this.bind(this.player.elements.display.currentTime, 'click', () => { + this.bind(player.elements.display.currentTime, 'click', () => { // Do nothing if we're at the start - if (this.player.currentTime === 0) { + if (player.currentTime === 0) { return; } - this.player.config.invertTime = !this.player.config.invertTime; + player.config.invertTime = !player.config.invertTime; - controls.timeUpdate.call(this.player); + controls.timeUpdate.call(player); }); } // Volume this.bind( - this.player.elements.inputs.volume, + player.elements.inputs.volume, inputEvent, event => { - this.player.volume = event.target.value; + player.volume = event.target.value; }, 'volume', ); // Polyfill for lower fill in <input type="range"> for webkit if (browser.isWebkit) { - Array.from( - getElements.call(this.player, 'input[type="range"]'), - ).forEach(element => { - this.bind(element, 'input', event => - controls.updateRangeFill.call(this.player, event.target), - ); - }); + Array.from(getElements.call(player, 'input[type="range"]')).forEach( + element => { + this.bind(element, 'input', event => + controls.updateRangeFill.call(player, event.target), + ); + }, + ); } // Seek tooltip this.bind( - this.player.elements.progress, + player.elements.progress, 'mouseenter mouseleave mousemove', - event => controls.updateSeekTooltip.call(this.player, event), + event => controls.updateSeekTooltip.call(player, event), ); // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting) - this.bind( - this.player.elements.controls, - 'mouseenter mouseleave', - event => { - this.player.elements.controls.hover = - !this.player.touch && event.type === 'mouseenter'; - }, - ); + this.bind(player.elements.controls, 'mouseenter mouseleave', event => { + player.elements.controls.hover = + !player.touch && event.type === 'mouseenter'; + }); // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting) this.bind( - this.player.elements.controls, + player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => { - this.player.elements.controls.pressed = [ + player.elements.controls.pressed = [ 'mousedown', 'touchstart', ].includes(event.type); @@ -840,21 +853,22 @@ class Listeners { ); // Focus in/out on controls - this.bind(this.player.elements.controls, 'focusin focusout', event => { - const { config, elements, timers } = this.player; + this.bind(player.elements.controls, 'focusin focusout', event => { + const { config, elements, timers } = player; + const isFocusIn = event.type === 'focusin'; // Skip transition to prevent focus from scrolling the parent element toggleClass( elements.controls, config.classNames.noTransition, - event.type === 'focusin', + isFocusIn, ); // Toggle - ui.toggleControls.call(this.player, event.type === 'focusin'); + ui.toggleControls.call(player, isFocusIn); // If focusin, hide again after delay - if (event.type === 'focusin') { + if (isFocusIn) { // Restore transition setTimeout(() => { toggleClass( @@ -872,7 +886,7 @@ class Listeners { // Hide timers.controls = setTimeout( - () => ui.toggleControls.call(this.player, false), + () => ui.toggleControls.call(player, false), delay, ); } @@ -880,7 +894,7 @@ class Listeners { // Mouse wheel for volume this.bind( - this.player.elements.inputs.volume, + player.elements.inputs.volume, 'wheel', event => { // Detect "natural" scroll - suppored on OS X Safari only @@ -896,10 +910,10 @@ class Listeners { const direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y); // Change the volume by 2% - this.player.increaseVolume(direction / 50); + player.increaseVolume(direction / 50); // Don't break page scrolling at max and min - const { volume } = this.player.media; + const { volume } = player.media; if ( (direction === 1 && volume < 1) || (direction === -1 && volume > 0) |