@ -361,7 +361,7 @@ player.fullscreen.enter(); // Enter fullscreen
|
||||
| `fullscreen.exit()` | - | Exit fullscreen. |
|
||||
| `fullscreen.toggle()` | - | Toggle fullscreen. |
|
||||
| `airplay()` | - | Trigger the airplay dialog on supported devices. |
|
||||
| `toggleControls(toggle)` | Boolean | Toggle the controls based on the specified boolean. |
|
||||
| `toggleControls(toggle)` | Boolean | Toggle the controls (video only). Takes optional truthy value to force it on/off. |
|
||||
| `on(event, function)` | String, Function | Add an event listener for the specified event. |
|
||||
| `off(event, function)` | String, Function | Remove an event listener for the specified event. |
|
||||
| `supports(type)` | String | Check support for a mime type. |
|
||||
|
@ -338,7 +338,6 @@ const defaults = {
|
||||
paused: 'plyr--paused',
|
||||
stopped: 'plyr--stopped',
|
||||
loading: 'plyr--loading',
|
||||
error: 'plyr--has-error',
|
||||
hover: 'plyr--hover',
|
||||
tooltip: 'plyr__tooltip',
|
||||
cues: 'plyr__cues',
|
||||
|
@ -238,13 +238,36 @@ class Listeners {
|
||||
}, 0);
|
||||
});
|
||||
|
||||
// Toggle controls visibility based on mouse movement
|
||||
if (this.player.config.hideControls) {
|
||||
// Toggle controls on mouse events and entering fullscreen
|
||||
utils.on(this.player.elements.container, 'mouseenter mouseleave mousemove touchstart touchend touchmove enterfullscreen exitfullscreen', event => {
|
||||
this.player.toggleControls(event);
|
||||
});
|
||||
utils.on(this.player.elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', event => {
|
||||
const { controls } = this.player.elements;
|
||||
|
||||
// Remove button states for fullscreen
|
||||
if (event.type === 'enterfullscreen') {
|
||||
controls.pressed = false;
|
||||
controls.hover = false;
|
||||
}
|
||||
|
||||
// Show, then hide after a timeout unless another control event occurs
|
||||
const show = [
|
||||
'touchstart',
|
||||
'touchmove',
|
||||
'mousemove',
|
||||
].includes(event.type);
|
||||
|
||||
let delay = 0;
|
||||
|
||||
if (show) {
|
||||
ui.toggleControls.call(this.player, true);
|
||||
// Use longer timeout for touch devices
|
||||
delay = this.player.touch ? 3000 : 2000;
|
||||
}
|
||||
|
||||
// Clear timer
|
||||
clearTimeout(this.player.timers.controls);
|
||||
// Timer to prevent flicker when seeking
|
||||
this.player.timers.controls = setTimeout(() => ui.toggleControls.call(this.player, false), delay);
|
||||
});
|
||||
}
|
||||
|
||||
// Listen for media events
|
||||
@ -283,9 +306,6 @@ class Listeners {
|
||||
// Loading state
|
||||
utils.on(this.player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(this.player, event));
|
||||
|
||||
// Check if media failed to load
|
||||
// utils.on(this.player.media, 'play', event => ui.checkFailed.call(this.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
|
||||
utils.on(this.player.media, 'playing', () => {
|
||||
@ -574,14 +594,12 @@ class Listeners {
|
||||
// Seek tooltip
|
||||
on(this.player.elements.progress, 'mouseenter mouseleave mousemove', event => controls.updateSeekTooltip.call(this.player, event));
|
||||
|
||||
// Toggle controls visibility based on mouse movement
|
||||
if (this.player.config.hideControls) {
|
||||
// Watch for cursor over controls so they don't hide when trying to interact
|
||||
// Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)
|
||||
on(this.player.elements.controls, 'mouseenter mouseleave', event => {
|
||||
this.player.elements.controls.hover = !this.player.touch && event.type === 'mouseenter';
|
||||
});
|
||||
|
||||
// Watch for cursor over controls so they don't hide when trying to interact
|
||||
// Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)
|
||||
on(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {
|
||||
this.player.elements.controls.pressed = [
|
||||
'mousedown',
|
||||
@ -591,9 +609,30 @@ class Listeners {
|
||||
|
||||
// Focus in/out on controls
|
||||
on(this.player.elements.controls, 'focusin focusout', event => {
|
||||
this.player.toggleControls(event);
|
||||
});
|
||||
const { config, elements, timers } = this.player;
|
||||
|
||||
// Skip transition to prevent focus from scrolling the parent element
|
||||
utils.toggleClass(elements.controls, config.classNames.noTransition, event.type === 'focusin');
|
||||
|
||||
// Toggle
|
||||
ui.toggleControls.call(this.player, event.type === 'focusin');
|
||||
|
||||
// If focusin, hide again after delay
|
||||
if (event.type === 'focusin') {
|
||||
// Restore transition
|
||||
setTimeout(() => {
|
||||
utils.toggleClass(elements.controls, config.classNames.noTransition, false);
|
||||
}, 0);
|
||||
|
||||
// Delay a little more for keyboard users
|
||||
const delay = this.touch ? 3000 : 4000;
|
||||
|
||||
// Clear timer
|
||||
clearTimeout(timers.controls);
|
||||
// Hide
|
||||
timers.controls = setTimeout(() => ui.toggleControls.call(this.player, false), delay);
|
||||
}
|
||||
});
|
||||
|
||||
// Mouse wheel for volume
|
||||
on(
|
||||
|
121
src/js/plyr.js
121
src/js/plyr.js
@ -971,119 +971,32 @@ class Plyr {
|
||||
|
||||
/**
|
||||
* Toggle the player controls
|
||||
* @param {boolean} toggle - Whether to show the controls
|
||||
* @param {boolean} [toggle] - Whether to show the controls
|
||||
*/
|
||||
toggleControls(toggle) {
|
||||
// We need controls of course...
|
||||
if (!utils.is.element(this.elements.controls)) {
|
||||
return;
|
||||
}
|
||||
// Don't toggle if missing UI support or if it's audio
|
||||
if (this.supported.ui && !this.isAudio) {
|
||||
// Get state before change
|
||||
const isHidden = utils.hasClass(this.elements.container, this.config.classNames.hideControls);
|
||||
|
||||
// Don't hide if no UI support or it's audio
|
||||
if (!this.supported.ui || this.isAudio) {
|
||||
return;
|
||||
}
|
||||
// Negate the argument if not undefined since adding the class to hides the controls
|
||||
const force = typeof toggle === 'undefined' ? undefined : !toggle;
|
||||
|
||||
let delay = 0;
|
||||
let show = toggle;
|
||||
let isEnterFullscreen = false;
|
||||
// Apply and get updated state
|
||||
const hiding = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, force);
|
||||
|
||||
// Get toggle state if not set
|
||||
if (!utils.is.boolean(toggle)) {
|
||||
if (utils.is.event(toggle)) {
|
||||
// Is the enter fullscreen event
|
||||
isEnterFullscreen = toggle.type === 'enterfullscreen';
|
||||
|
||||
// Events that show the controls
|
||||
const showEvents = [
|
||||
'touchstart',
|
||||
'touchmove',
|
||||
'mouseenter',
|
||||
'mousemove',
|
||||
'focusin',
|
||||
];
|
||||
|
||||
// Events that delay hiding
|
||||
const delayEvents = [
|
||||
'touchmove',
|
||||
'touchend',
|
||||
'mousemove',
|
||||
];
|
||||
|
||||
// Whether to show controls
|
||||
show = showEvents.includes(toggle.type);
|
||||
|
||||
// Delay hiding on move events
|
||||
if (delayEvents.includes(toggle.type)) {
|
||||
delay = 2000;
|
||||
}
|
||||
|
||||
// Delay a little more for keyboard users
|
||||
if (!this.touch && toggle.type === 'focusin') {
|
||||
delay = 3000;
|
||||
utils.toggleClass(this.elements.controls, this.config.classNames.noTransition, true);
|
||||
}
|
||||
} else {
|
||||
show = utils.hasClass(this.elements.container, this.config.classNames.hideControls);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear timer on every call
|
||||
clearTimeout(this.timers.controls);
|
||||
|
||||
// If the mouse is not over the controls, set a timeout to hide them
|
||||
if (show || this.paused || this.loading) {
|
||||
// Check if controls toggled
|
||||
const toggled = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, false);
|
||||
|
||||
// Trigger event
|
||||
if (toggled) {
|
||||
utils.dispatchEvent.call(this, this.media, 'controlsshown');
|
||||
}
|
||||
|
||||
// Always show controls when paused or if touch
|
||||
if (this.paused || this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delay for hiding on touch
|
||||
if (this.touch) {
|
||||
delay = 3000;
|
||||
}
|
||||
}
|
||||
|
||||
// If toggle is false or if we're playing (regardless of toggle),
|
||||
// then set the timer to hide the controls
|
||||
if (!show || this.playing) {
|
||||
this.timers.controls = setTimeout(() => {
|
||||
// We need controls of course...
|
||||
if (!utils.is.element(this.elements.controls)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the mouse is over the controls (and not entering fullscreen), bail
|
||||
if ((this.elements.controls.pressed || this.elements.controls.hover) && !isEnterFullscreen) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore transition behaviour
|
||||
if (!utils.hasClass(this.elements.container, this.config.classNames.hideControls)) {
|
||||
utils.toggleClass(this.elements.controls, this.config.classNames.noTransition, false);
|
||||
}
|
||||
|
||||
// Set hideControls class
|
||||
const toggled = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, this.config.hideControls);
|
||||
|
||||
// Trigger event and close menu
|
||||
if (toggled) {
|
||||
utils.dispatchEvent.call(this, this.media, 'controlshidden');
|
||||
|
||||
if (this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) {
|
||||
// Close menu
|
||||
if (hiding && this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) {
|
||||
controls.toggleMenu.call(this, false);
|
||||
}
|
||||
// Trigger event on change
|
||||
if (hiding !== isHidden) {
|
||||
const eventName = hiding ? 'controlshidden' : 'controlsshown';
|
||||
utils.dispatchEvent.call(this, this.media, eventName);
|
||||
}
|
||||
}, delay);
|
||||
return !hiding;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
33
src/js/ui.js
33
src/js/ui.js
@ -173,7 +173,7 @@ const ui = {
|
||||
}
|
||||
|
||||
// Toggle controls
|
||||
this.toggleControls(!this.playing);
|
||||
ui.toggleControls.call(this);
|
||||
},
|
||||
|
||||
// Check if media is loading
|
||||
@ -188,35 +188,22 @@ const ui = {
|
||||
|
||||
// Timer to prevent flicker when seeking
|
||||
this.timers.loading = setTimeout(() => {
|
||||
// Toggle container class hook
|
||||
// Update progress bar loading class state
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
|
||||
|
||||
// Show controls if loading, hide if done
|
||||
this.toggleControls(this.loading);
|
||||
// Update controls visibility
|
||||
ui.toggleControls.call(this);
|
||||
}, this.loading ? 250 : 0);
|
||||
},
|
||||
|
||||
// Check if media failed to load
|
||||
checkFailed() {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/networkState
|
||||
this.failed = this.media.networkState === 3;
|
||||
// Toggle controls based on state and `force` argument
|
||||
toggleControls(force) {
|
||||
const { controls } = this.elements;
|
||||
|
||||
if (this.failed) {
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.loading, false);
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.error, true);
|
||||
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));
|
||||
}
|
||||
|
||||
// Clear timer
|
||||
clearTimeout(this.timers.failed);
|
||||
|
||||
// Timer to prevent flicker when seeking
|
||||
this.timers.loading = setTimeout(() => {
|
||||
// Toggle container class hook
|
||||
utils.toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
|
||||
|
||||
// Show controls if loading, hide if done
|
||||
this.toggleControls(this.loading);
|
||||
}, this.loading ? 250 : 0);
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -393,14 +393,16 @@ const utils = {
|
||||
}
|
||||
},
|
||||
|
||||
// Toggle class on an element
|
||||
toggleClass(element, className, toggle) {
|
||||
// Mirror Element.classList.toggle, with IE compatibility for "force" argument
|
||||
toggleClass(element, className, force) {
|
||||
if (utils.is.element(element)) {
|
||||
const contains = element.classList.contains(className);
|
||||
let method = 'toggle';
|
||||
if (typeof force !== 'undefined') {
|
||||
method = force ? 'add' : 'remove';
|
||||
}
|
||||
|
||||
element.classList[toggle ? 'add' : 'remove'](className);
|
||||
|
||||
return (toggle && !contains) || (!toggle && contains);
|
||||
element.classList[method](className);
|
||||
return element.classList.contains(className);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -39,7 +39,6 @@
|
||||
@import 'components/video';
|
||||
@import 'components/volume';
|
||||
|
||||
@import 'states/error';
|
||||
@import 'states/fullscreen';
|
||||
|
||||
@import 'plugins/ads';
|
||||
|
@ -1,25 +0,0 @@
|
||||
// --------------------------------------------------------------
|
||||
// Error state
|
||||
// --------------------------------------------------------------
|
||||
|
||||
.plyr--has-error {
|
||||
pointer-events: none;
|
||||
|
||||
&::after {
|
||||
align-items: center;
|
||||
background: rgba(#000, 90%);
|
||||
color: #fff;
|
||||
content: attr(data-plyr-error);
|
||||
display: flex;
|
||||
font-size: $plyr-font-size-base;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
text-shadow: 0 1px 1px rgba(#000, 10%);
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user