diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/js/controls.js | 14 | ||||
-rw-r--r-- | src/js/listeners.js | 432 | ||||
-rw-r--r-- | src/js/plyr.js | 109 | ||||
-rw-r--r-- | src/js/support.js | 20 | ||||
-rw-r--r-- | src/js/ui.js | 82 | ||||
-rw-r--r-- | src/js/utils/animation.js | 14 | ||||
-rw-r--r-- | src/sass/components/poster.scss | 7 | ||||
-rw-r--r-- | src/sass/components/progress.scss | 1 | ||||
-rw-r--r-- | src/sass/lib/mixins.scss | 1 |
9 files changed, 412 insertions, 268 deletions
diff --git a/src/js/controls.js b/src/js/controls.js index da404441..27611b2f 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -378,7 +378,7 @@ const controls = { // Show the respective menu if (!isRadioButton && [32,39].includes(event.which)) { - controls.showMenuPanel.call(this, type); + controls.showMenuPanel.call(this, type, true); } else { let target; @@ -477,7 +477,7 @@ const controls = { break; } - controls.showMenuPanel.call(this, 'home'); + controls.showMenuPanel.call(this, 'home', event.type === 'keydown'); }, type, false, @@ -1118,7 +1118,7 @@ const controls = { }, // Show a panel in the menu - showMenuPanel(type = '') { + showMenuPanel(type = '', tabFocus = false) { const target = document.getElementById(`plyr-settings-${this.id}-${type}`); // Nothing to show, bail @@ -1170,7 +1170,7 @@ const controls = { // Focus the first item const firstItem = target.querySelector('[role^="menuitem"]'); - setFocus.call(this, firstItem, true); + setFocus.call(this, firstItem, tabFocus); }, // Build the default HTML @@ -1344,7 +1344,7 @@ const controls = { // Show menu on click on(menuItem, 'click', () => { - controls.showMenuPanel.call(this, type); + controls.showMenuPanel.call(this, type, false); }); const flex = createElement('span', null, i18n.get(type, this.config)); @@ -1406,12 +1406,12 @@ const controls = { event.stopPropagation(); // Show the respective menu - controls.showMenuPanel.call(this, 'home'); + controls.showMenuPanel.call(this, 'home', true); }, false); // Go back via button click on(backButton, 'click', () => { - controls.showMenuPanel.call(this, 'home'); + controls.showMenuPanel.call(this, 'home', false); }); // Add to pane 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) diff --git a/src/js/plyr.js b/src/js/plyr.js index ab1f7866..b4c54ca8 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -52,7 +52,11 @@ class Plyr { } // jQuery, NodeList or Array passed, use first element - if ((window.jQuery && this.media instanceof jQuery) || is.nodeList(this.media) || is.array(this.media)) { + if ( + (window.jQuery && this.media instanceof jQuery) || + is.nodeList(this.media) || + is.array(this.media) + ) { // eslint-disable-next-line this.media = this.media[0]; } @@ -65,7 +69,9 @@ class Plyr { options || {}, (() => { try { - return JSON.parse(this.media.getAttribute('data-plyr-config')); + return JSON.parse( + this.media.getAttribute('data-plyr-config'), + ); } catch (e) { return {}; } @@ -185,21 +191,30 @@ class Plyr { // TODO: replace fullscreen.iosNative with this playsinline config option // YouTube requires the playsinline in the URL if (this.isYouTube) { - this.config.playsinline = truthy.includes(url.searchParams.get('playsinline')); + this.config.playsinline = truthy.includes( + url.searchParams.get('playsinline'), + ); } else { this.config.playsinline = true; } } } else { // <div> with attributes - this.provider = this.media.getAttribute(this.config.attributes.embed.provider); + this.provider = this.media.getAttribute( + this.config.attributes.embed.provider, + ); // Remove attribute - this.media.removeAttribute(this.config.attributes.embed.provider); + this.media.removeAttribute( + this.config.attributes.embed.provider, + ); } // Unsupported or missing provider - if (is.empty(this.provider) || !Object.keys(providers).includes(this.provider)) { + if ( + is.empty(this.provider) || + !Object.keys(providers).includes(this.provider) + ) { this.debug.error('Setup failed: Invalid provider'); return; } @@ -221,7 +236,10 @@ class Plyr { if (this.media.hasAttribute('autoplay')) { this.config.autoplay = true; } - if (this.media.hasAttribute('playsinline')) { + if ( + this.media.hasAttribute('playsinline') || + this.media.hasAttribute('webkit-playsinline') + ) { this.config.playsinline = true; } if (this.media.hasAttribute('muted')) { @@ -239,7 +257,11 @@ class Plyr { } // Check for support again but with type - this.supported = support.check(this.type, this.provider, this.config.playsinline); + this.supported = support.check( + this.type, + this.provider, + this.config.playsinline, + ); // If no support for even API, bail if (!this.supported.api) { @@ -272,9 +294,14 @@ class Plyr { // Listen for events if debugging if (this.config.debug) { - on.call(this, this.elements.container, this.config.events.join(' '), event => { - this.debug.log(`event: ${event.type}`); - }); + on.call( + this, + this.elements.container, + this.config.events.join(' '), + event => { + this.debug.log(`event: ${event.type}`); + }, + ); } // Setup interface @@ -293,7 +320,9 @@ class Plyr { this.fullscreen = new Fullscreen(this); // Setup ads if provided - this.ads = new Ads(this); + if (this.config.ads.enabled) { + this.ads = new Ads(this); + } // Autoplay if required if (this.config.autoplay) { @@ -422,7 +451,9 @@ class Plyr { * @param {number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime */ rewind(seekTime) { - this.currentTime = this.currentTime - (is.number(seekTime) ? seekTime : this.config.seekTime); + this.currentTime = + this.currentTime - + (is.number(seekTime) ? seekTime : this.config.seekTime); } /** @@ -430,7 +461,9 @@ class Plyr { * @param {number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime */ forward(seekTime) { - this.currentTime = this.currentTime + (is.number(seekTime) ? seekTime : this.config.seekTime); + this.currentTime = + this.currentTime + + (is.number(seekTime) ? seekTime : this.config.seekTime); } /** @@ -447,7 +480,9 @@ class Plyr { const inputIsValid = is.number(input) && input > 0; // Set - this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0; + this.media.currentTime = inputIsValid + ? Math.min(input, this.duration) + : 0; // Logging this.debug.log(`Seeking to ${this.currentTime} seconds`); @@ -497,7 +532,10 @@ class Plyr { // Media duration can be NaN or Infinity before the media has loaded const realDuration = (this.media || {}).duration; - const duration = !is.number(realDuration) || realDuration === Infinity ? 0 : realDuration; + const duration = + !is.number(realDuration) || realDuration === Infinity + ? 0 + : realDuration; // If config duration is funky, use regular duration return fauxDuration || duration; @@ -691,12 +729,16 @@ class Plyr { if (!options.includes(quality)) { const value = closest(options, quality); - this.debug.warn(`Unsupported quality option: ${quality}, using ${value} instead`); + this.debug.warn( + `Unsupported quality option: ${quality}, using ${value} instead`, + ); quality = value; } // Trigger request event - triggerEvent.call(this, this.media, 'qualityrequested', false, { quality }); + triggerEvent.call(this, this.media, 'qualityrequested', false, { + quality, + }); // Update config config.selected = quality; @@ -888,7 +930,9 @@ class Plyr { const toggle = is.boolean(input) ? input : this.pip === states.inline; // Toggle based on current state - this.media.webkitSetPresentationMode(toggle ? states.pip : states.inline); + this.media.webkitSetPresentationMode( + toggle ? states.pip : states.inline, + ); } /** @@ -921,25 +965,39 @@ class Plyr { // Don't toggle if missing UI support or if it's audio if (this.supported.ui && !this.isAudio) { // Get state before change - const isHidden = hasClass(this.elements.container, this.config.classNames.hideControls); + const isHidden = hasClass( + this.elements.container, + this.config.classNames.hideControls, + ); // Negate the argument if not undefined since adding the class to hides the controls const force = typeof toggle === 'undefined' ? undefined : !toggle; // Apply and get updated state - const hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force); + const hiding = toggleClass( + this.elements.container, + this.config.classNames.hideControls, + force, + ); // Close menu - if (hiding && this.config.controls.includes('settings') && !is.empty(this.config.settings)) { + if ( + hiding && + this.config.controls.includes('settings') && + !is.empty(this.config.settings) + ) { controls.toggleMenu.call(this, false); } + // Trigger event on change if (hiding !== isHidden) { const eventName = hiding ? 'controlshidden' : 'controlsshown'; triggerEvent.call(this, this.media, eventName); } + return !hiding; } + return false; } @@ -1017,7 +1075,12 @@ class Plyr { replaceElement(this.elements.original, this.elements.container); // Event - triggerEvent.call(this, this.elements.original, 'destroyed', true); + triggerEvent.call( + this, + this.elements.original, + 'destroyed', + true, + ); // Callback if (is.function(callback)) { diff --git a/src/js/support.js b/src/js/support.js index 6395293f..4681f5c7 100644 --- a/src/js/support.js +++ b/src/js/support.js @@ -25,9 +25,13 @@ const support = { // Check for support // Basic functionality vs full UI check(type, provider, playsinline) { - const canPlayInline = browser.isIPhone && playsinline && support.playsinline; + const canPlayInline = + browser.isIPhone && playsinline && support.playsinline; const api = support[type] || provider !== 'html5'; - const ui = api && support.rangeInput && (type !== 'video' || !browser.isIPhone || canPlayInline); + const ui = + api && + support.rangeInput && + (type !== 'video' || !browser.isIPhone || canPlayInline); return { api, @@ -37,7 +41,9 @@ const support = { // Picture-in-picture support // Safari only currently - pip: (() => !browser.isIPhone && is.function(createElement('video').webkitSetPresentationMode))(), + pip: (() => + !browser.isIPhone && + is.function(createElement('video').webkitSetPresentationMode))(), // Airplay support // Safari only currently @@ -69,7 +75,9 @@ const support = { } try { - return Boolean(type && this.media.canPlayType(type).replace(/no/, '')); + return Boolean( + type && this.media.canPlayType(type).replace(/no/, ''), + ); } catch (err) { return false; } @@ -94,7 +102,9 @@ const support = { // Reduced motion iOS & MacOS setting // https://webkit.org/blog/7551/responsive-design-for-motion/ - reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches, + reducedMotion: + 'matchMedia' in window && + window.matchMedia('(prefers-reduced-motion)').matches, }; export default support; diff --git a/src/js/ui.js b/src/js/ui.js index 34fe7e82..8c61d805 100644 --- a/src/js/ui.js +++ b/src/js/ui.js @@ -14,8 +14,16 @@ import loadImage from './utils/loadImage'; const ui = { addStyleHook() { - toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true); - toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui); + toggleClass( + this.elements.container, + this.config.selectors.container.replace('.', ''), + true, + ); + toggleClass( + this.elements.container, + this.config.classNames.uiSupported, + this.supported.ui, + ); }, // Toggle native HTML5 media controls @@ -35,7 +43,9 @@ const ui = { // Don't setup interface if no support if (!this.supported.ui) { - this.debug.warn(`Basic support only for ${this.provider} ${this.type}`); + this.debug.warn( + `Basic support only for ${this.provider} ${this.type}`, + ); // Restore native controls ui.toggleNativeControls.call(this, true); @@ -93,13 +103,25 @@ const ui = { ); // Check for airplay support - toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5); + toggleClass( + this.elements.container, + this.config.classNames.airplay.supported, + support.airplay && this.isHTML5, + ); // Add iOS class - toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos); + toggleClass( + this.elements.container, + this.config.classNames.isIos, + browser.isIos, + ); // Add touch class - toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch); + toggleClass( + this.elements.container, + this.config.classNames.isTouch, + this.touch, + ); // Ready for API calls this.ready = true; @@ -149,7 +171,9 @@ const ui = { } // Default to media type - const title = !is.empty(this.config.title) ? this.config.title : 'video'; + const title = !is.empty(this.config.title) + ? this.config.title + : 'video'; const format = i18n.get('frameTitle', this.config); iframe.setAttribute('title', format.replace('{title}', title)); @@ -158,7 +182,11 @@ const ui = { // Toggle poster togglePoster(enable) { - toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable); + toggleClass( + this.elements.container, + this.config.classNames.posterEnabled, + enable, + ); }, // Set the poster image (async) @@ -189,7 +217,9 @@ const ui = { .then(() => { // Prevent race conditions if (poster !== this.poster) { - throw new Error('setPoster cancelled by later call to setPoster'); + throw new Error( + 'setPoster cancelled by later call to setPoster', + ); } }) .then(() => { @@ -207,9 +237,21 @@ const ui = { // Check playing state checkPlaying(event) { // Class hooks - toggleClass(this.elements.container, this.config.classNames.playing, this.playing); - toggleClass(this.elements.container, this.config.classNames.paused, this.paused); - toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped); + toggleClass( + this.elements.container, + this.config.classNames.playing, + this.playing, + ); + toggleClass( + this.elements.container, + this.config.classNames.paused, + this.paused, + ); + toggleClass( + this.elements.container, + this.config.classNames.stopped, + this.stopped, + ); // Set state Array.from(this.elements.buttons.play || []).forEach(target => { @@ -235,7 +277,11 @@ const ui = { // Timer to prevent flicker when seeking this.timers.loading = setTimeout(() => { // Update progress bar loading class state - toggleClass(this.elements.container, this.config.classNames.loading, this.loading); + toggleClass( + this.elements.container, + this.config.classNames.loading, + this.loading, + ); // Update controls visibility ui.toggleControls.call(this); @@ -248,7 +294,15 @@ const ui = { if (controls && this.config.hideControls) { // Show controls if force, loading, paused, or button interaction, otherwise hide - this.toggleControls(Boolean(force || this.loading || this.paused || controls.pressed || controls.hover)); + this.toggleControls( + Boolean( + force || + this.loading || + this.paused || + controls.pressed || + controls.hover, + ), + ); } }, }; diff --git a/src/js/utils/animation.js b/src/js/utils/animation.js index 95e39f03..49bc0b8c 100644 --- a/src/js/utils/animation.js +++ b/src/js/utils/animation.js @@ -15,7 +15,9 @@ export const transitionEndEvent = (() => { transition: 'transitionend', }; - const type = Object.keys(events).find(event => element.style[event] !== undefined); + const type = Object.keys(events).find( + event => element.style[event] !== undefined, + ); return is.string(type) ? events[type] : false; })(); @@ -23,8 +25,12 @@ export const transitionEndEvent = (() => { // Force repaint of element export function repaint(element) { setTimeout(() => { - toggleHidden(element, true); - element.offsetHeight; // eslint-disable-line - toggleHidden(element, false); + try { + toggleHidden(element, true); + element.offsetHeight; // eslint-disable-line + toggleHidden(element, false); + } catch (e) { + // Do nothing + } }, 0); } diff --git a/src/sass/components/poster.scss b/src/sass/components/poster.scss index 9bf7398d..15e87257 100644 --- a/src/sass/components/poster.scss +++ b/src/sass/components/poster.scss @@ -7,17 +7,16 @@ background-position: 50% 50%; background-repeat: no-repeat; background-size: contain; + display: none; height: 100%; left: 0; - opacity: 0; + pointer-events: none; position: absolute; top: 0; - transition: opacity 0.3s ease; width: 100%; z-index: 1; } .plyr--stopped.plyr__poster-enabled .plyr__poster { - opacity: 1; - pointer-events: none; + display: block; } diff --git a/src/sass/components/progress.scss b/src/sass/components/progress.scss index eddd32ab..16992808 100644 --- a/src/sass/components/progress.scss +++ b/src/sass/components/progress.scss @@ -3,7 +3,6 @@ // -------------------------------------------------------------- .plyr__progress { - display: flex; flex: 1; left: $plyr-range-thumb-height / 2; margin-right: $plyr-range-thumb-height; diff --git a/src/sass/lib/mixins.scss b/src/sass/lib/mixins.scss index 2aec702c..e6afe046 100644 --- a/src/sass/lib/mixins.scss +++ b/src/sass/lib/mixins.scss @@ -36,7 +36,6 @@ border: 0; border-radius: 100%; box-shadow: $plyr-range-thumb-shadow; - box-sizing: border-box; height: $plyr-range-thumb-height; position: relative; transition: all 0.2s ease; |