Controls cleanup, work on captions bug, click to invert time

This commit is contained in:
Sam Potts 2017-11-18 19:30:26 +11:00
parent d7a1c44281
commit 6984d6fb16
32 changed files with 360 additions and 254 deletions

2
demo/dist/demo.css vendored

File diff suppressed because one or more lines are too long

2
demo/dist/demo.js vendored
View File

@ -1,3 +1,3 @@
document.addEventListener("DOMContentLoaded",function(){function e(e,t,o){e&&e.classList[o?"add":"remove"](t)}function t(t,a){if(t in n&&(a||t!==r)&&(r.length||t!==n.video)){switch(t){case n.video:o.source={type:"video",title:"View From A Blue Moon",sources:[{src:"https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4",type:"video/mp4"}],poster:"https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg",tracks:[{kind:"captions",label:"English",srclang:"en",src:"https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt",default:!0},{kind:"captions",label:"French",srclang:"fr",src:"https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt"}]};break;case n.audio:o.source={type:"audio",title:"Kishi Bashi – “It All Began With A Burst”",sources:[{src:"https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.mp3",type:"audio/mp3"},{src:"https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.ogg",type:"audio/ogg"}]};break;case n.youtube:o.source={type:"video",title:"View From A Blue Moon",sources:[{src:"https://youtube.com/watch?v=bTqVqk7FSmY",type:"youtube"}]};break;case n.vimeo:o.source={type:"video",sources:[{src:"https://vimeo.com/76979871",type:"vimeo"}]}}r=t,Array.from(i).forEach(function(t){return e(t.parentElement,"active",!1)}),e(document.querySelector('[data-source="'+t+'"]'),"active",!0),Array.from(document.querySelectorAll(".plyr__cite")).forEach(function(e){e.setAttribute("hidden","")}),document.querySelector(".plyr__cite--"+t).removeAttribute("hidden")}}window.shr&&window.shr.setup({count:{classname:"button__count"}});document.addEventListener("focusout",function(e){e.target.classList.remove("tab-focus")}),document.addEventListener("keydown",function(e){9===e.keyCode&&window.setTimeout(function(){document.activeElement.classList.add("tab-focus")},0)});var o=new window.Plyr("#player",{debug:!0,title:"View From A Blue Moon",iconUrl:"../dist/plyr.svg",keyboard:{global:!0},tooltips:{controls:!0},captions:{active:!0},controls:["play-large","play","progress","current-time","mute","volume","captions","settings","fullscreen","pip","airplay"]});window.player=o;var i=document.querySelectorAll("[data-source]"),n={video:"video",audio:"audio",youtube:"youtube",vimeo:"vimeo"},r=window.location.hash.replace("#",""),a=window.history&&window.history.pushState;if(Array.from(i).forEach(function(e){e.addEventListener("click",function(){var o=e.getAttribute("data-source");t(o),a&&window.history.pushState({type:o},"","#"+o)})}),window.addEventListener("popstate",function(e){e.state&&"type"in e.state&&t(e.state.type)}),a){var s=!r.length;s&&(r=n.video),r in n&&window.history.replaceState({type:r},"",s?"":"#"+r),r!==n.video&&t(r,!0)}}),"plyr.io"===window.location.host&&(!function(e,t,o,i,n,r,a){e.GoogleAnalyticsObject=n,e.ga=e.ga||function(){(e.ga.q=e.ga.q||[]).push(arguments)},e.ga.l=1*new Date,r=t.createElement(o),a=t.getElementsByTagName(o)[0],r.async=1,r.src="//www.google-analytics.com/analytics.js",a.parentNode.insertBefore(r,a)}(window,document,"script",0,"ga"),window.ga("create","UA-40881672-11","auto"),window.ga("send","pageview")); document.addEventListener("DOMContentLoaded",function(){function e(e,t,o){e&&e.classList[o?"add":"remove"](t)}function t(t,r){if(t in a&&(r||t!==n)&&(n.length||t!==a.video)){switch(t){case a.video:o.source={type:"video",title:"View From A Blue Moon",sources:[{src:"media/View_From_A_Blue_Moon_Trailer-HD.mp4",type:"video/mp4"}],poster:"hmedia/View_From_A_Blue_Moon_Trailer-HD.jpg",tracks:[{kind:"captions",label:"English",srclang:"en",src:"media/View_From_A_Blue_Moon_Trailer-HD.en.vtt",default:!0},{kind:"captions",label:"French",srclang:"fr",src:"media/View_From_A_Blue_Moon_Trailer-HD.fr.vtt"}]};break;case a.audio:o.source={type:"audio",title:"Kishi Bashi – “It All Began With A Burst”",sources:[{src:"https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.mp3",type:"audio/mp3"},{src:"https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.ogg",type:"audio/ogg"}]};break;case a.youtube:o.source={type:"video",title:"View From A Blue Moon",sources:[{src:"https://youtube.com/watch?v=bTqVqk7FSmY",type:"youtube"}]};break;case a.vimeo:o.source={type:"video",sources:[{src:"https://vimeo.com/76979871",type:"vimeo"}]}}n=t,Array.from(i).forEach(function(t){return e(t.parentElement,"active",!1)}),e(document.querySelector('[data-source="'+t+'"]'),"active",!0),Array.from(document.querySelectorAll(".plyr__cite")).forEach(function(e){e.setAttribute("hidden","")}),document.querySelector(".plyr__cite--"+t).removeAttribute("hidden")}}window.shr&&window.shr.setup({count:{classname:"button__count"}});document.addEventListener("focusout",function(e){e.target.classList.remove("tab-focus")}),document.addEventListener("keydown",function(e){9===e.keyCode&&window.setTimeout(function(){document.activeElement.classList.add("tab-focus")},0)});var o=new window.Plyr("#player",{debug:!0,title:"View From A Blue Moon",iconUrl:"../dist/plyr.svg",keyboard:{global:!0},tooltips:{controls:!0},captions:{active:!0},controls:["play-large","play","progress","current-time","mute","volume","captions","settings","fullscreen","pip","airplay"],keys:{google:"AIzaSyDrNwtN3nLH_8rjCmu5Wq3ZCm4MNAVdc0c"}});window.player=o;var i=document.querySelectorAll("[data-source]"),a={video:"video",audio:"audio",youtube:"youtube",vimeo:"vimeo"},n=window.location.hash.replace("#",""),r=window.history&&window.history.pushState;if(Array.from(i).forEach(function(e){e.addEventListener("click",function(){var o=e.getAttribute("data-source");t(o),r&&window.history.pushState({type:o},"","#"+o)})}),window.addEventListener("popstate",function(e){e.state&&"type"in e.state&&t(e.state.type)}),r){var s=!n.length;s&&(n=a.video),n in a&&window.history.replaceState({type:n},"",s?"":"#"+n),n!==a.video&&t(n,!0)}}),"plyr.io"===window.location.host&&(!function(e,t,o,i,a,n,r){e.GoogleAnalyticsObject=a,e.ga=e.ga||function(){(e.ga.q=e.ga.q||[]).push(arguments)},e.ga.l=1*new Date,n=t.createElement(o),r=t.getElementsByTagName(o)[0],n.async=1,n.src="//www.google-analytics.com/analytics.js",r.parentNode.insertBefore(n,r)}(window,document,"script",0,"ga"),window.ga("create","UA-40881672-11","auto"),window.ga("send","pageview"));
//# sourceMappingURL=demo.js.map //# sourceMappingURL=demo.js.map

File diff suppressed because one or more lines are too long

2
demo/dist/error.css vendored

File diff suppressed because one or more lines are too long

View File

@ -82,17 +82,16 @@
</header> </header>
<main> <main>
<video controls crossorigin playsinline loop poster="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg" <video controls crossorigin playsinline loop poster="media/View_From_A_Blue_Moon_Trailer-HD.jpg" id="player">
id="player">
<!-- Video files --> <!-- Video files -->
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4" type="video/mp4"> <source src="media/View_From_A_Blue_Moon_Trailer-HD.mp4" type="video/mp4">
<!-- Text track file --> <!-- Text track file -->
<track kind="captions" label="English" srclang="en" src="webvtt/View_From_A_Blue_Moon_Trailer-HD.en.vtt" default> <track kind="captions" label="English" srclang="en" src="media/View_From_A_Blue_Moon_Trailer-HD.en.vtt" default>
<track kind="captions" label="Français" srclang="fr" src="webvtt/View_From_A_Blue_Moon_Trailer-HD.fr.vtt"> <track kind="captions" label="Français" srclang="fr" src="media/View_From_A_Blue_Moon_Trailer-HD.fr.vtt">
<!-- Fallback for browsers that don't support the <video> element --> <!-- Fallback for browsers that don't support the <video> element -->
<a href="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4" download>Download</a> <a href="media/View_From_A_Blue_Moon_Trailer-HD.mp4" download>Download</a>
</video> </video>
<ul> <ul>

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

View File

@ -65,6 +65,9 @@ document.addEventListener('DOMContentLoaded', () => {
'pip', 'pip',
'airplay', 'airplay',
], ],
keys: {
google: 'AIzaSyDrNwtN3nLH_8rjCmu5Wq3ZCm4MNAVdc0c',
},
}); });
// Expose for testing // Expose for testing
@ -102,24 +105,24 @@ document.addEventListener('DOMContentLoaded', () => {
title: 'View From A Blue Moon', title: 'View From A Blue Moon',
sources: [ sources: [
{ {
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4', src: 'media/View_From_A_Blue_Moon_Trailer-HD.mp4',
type: 'video/mp4', type: 'video/mp4',
}, },
], ],
poster: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg', poster: 'hmedia/View_From_A_Blue_Moon_Trailer-HD.jpg',
tracks: [ tracks: [
{ {
kind: 'captions', kind: 'captions',
label: 'English', label: 'English',
srclang: 'en', srclang: 'en',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt', src: 'media/View_From_A_Blue_Moon_Trailer-HD.en.vtt',
default: true, default: true,
}, },
{ {
kind: 'captions', kind: 'captions',
label: 'French', label: 'French',
srclang: 'fr', srclang: 'fr',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt', src: 'media/View_From_A_Blue_Moon_Trailer-HD.fr.vtt',
}, },
], ],
}; };

View File

@ -10,7 +10,7 @@ video {
// Example players // Example players
.plyr { .plyr {
margin: 0 auto; margin: @spacing-base auto;
border-radius: @border-radius-base; border-radius: @border-radius-base;
box-shadow: 0 2px 5px fade(#000, 20%); box-shadow: 0 2px 5px fade(#000, 20%);
} }

View File

@ -27,15 +27,18 @@ body {
flex-direction: column; flex-direction: column;
} }
.grid {
flex: 1;
overflow: auto;
}
main { main {
margin: auto;
text-align: center; text-align: center;
} }
.grid {
flex: 1;
}
aside { aside {
position: relative;
display: flex; display: flex;
flex-shrink: 0; flex-shrink: 0;
align-items: center; align-items: center;

View File

@ -4,7 +4,7 @@
@font-face { @font-face {
font-family: 'Avenir'; font-family: 'Avenir';
src: url('https://cdn.plyr.io/static/fonts/avenir-medium.woff2') format('woff2'), src: local('Avenir-Medium'), url('https://cdn.plyr.io/static/fonts/avenir-medium.woff2') format('woff2'),
url('https://cdn.plyr.io/static/fonts/avenir-medium.woff') format('woff'); url('https://cdn.plyr.io/static/fonts/avenir-medium.woff') format('woff');
font-style: normal; font-style: normal;
font-weight: @font-weight-base; font-weight: @font-weight-base;
@ -13,7 +13,7 @@
@font-face { @font-face {
font-family: 'Avenir'; font-family: 'Avenir';
src: url('https://cdn.plyr.io/static/fonts/avenir-bold.woff2') format('woff2'), src: local('Avenir-Heavy'), url('https://cdn.plyr.io/static/fonts/avenir-bold.woff2') format('woff2'),
url('https://cdn.plyr.io/static/fonts/avenir-bold.woff') format('woff'); url('https://cdn.plyr.io/static/fonts/avenir-bold.woff') format('woff');
font-style: normal; font-style: normal;
font-weight: @font-weight-bold; font-weight: @font-weight-bold;
@ -22,7 +22,7 @@
@font-face { @font-face {
font-family: 'Avenir'; font-family: 'Avenir';
src: url('https://cdn.plyr.io/static/fonts/avenir-black.woff2?v=3') format('woff2'), src: local('Avenir-Black'), url('https://cdn.plyr.io/static/fonts/avenir-black.woff2?v=3') format('woff2'),
url('https://cdn.plyr.io/static/fonts/avenir-black.woff?v=3') format('woff'); url('https://cdn.plyr.io/static/fonts/avenir-black.woff?v=3') format('woff');
font-style: normal; font-style: normal;
font-weight: @font-weight-heavy; font-weight: @font-weight-heavy;

View File

@ -33,6 +33,7 @@
// --------------------------------------- // ---------------------------------------
.font-size(@font-size: 16) { .font-size(@font-size: 16) {
@rem: round((@font-size / 16), 3); @rem: round((@font-size / 16), 3);
font-size: (@font-size * 1px); font-size: (@font-size * 1px);
font-size: ~'@{rem}rem'; font-size: ~'@{rem}rem';
} }

2
dist/plyr.css vendored

File diff suppressed because one or more lines are too long

2
dist/plyr.js vendored

File diff suppressed because one or more lines are too long

2
dist/plyr.js.map vendored

File diff suppressed because one or more lines are too long

View File

@ -233,7 +233,7 @@ Option | Type | Default | Description
`seekTime` | Number | `10` | The time, in seconds, to seek when a user hits fast forward or rewind. `seekTime` | Number | `10` | The time, in seconds, to seek when a user hits fast forward or rewind.
`volume` | Number | `1` | A number, between 0 and 1, representing the initial volume of the player. `volume` | Number | `1` | A number, between 0 and 1, representing the initial volume of the player.
`muted` | Boolean | `false` | Whether to start playback muted. If the `muted` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true. `muted` | Boolean | `false` | Whether to start playback muted. If the `muted` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true.
`clickToPlay` | Boolean | `true` | Click (or tap) of the video container will toggle pause/play. `clickToPlay` | Boolean | `true` | Click (or tap) of the video container will toggle play/pause.
`disableContextMenu` | Boolean | `true` | Disable right click menu on video to <em>help</em> as very primitive obfuscation to prevent downloads of content. `disableContextMenu` | Boolean | `true` | Disable right click menu on video to <em>help</em> as very primitive obfuscation to prevent downloads of content.
`hideControls` | Boolean | `true` | Hide video controls automatically after 2s of no mouse or focus movement, on control element blur (tab out), on playback start or entering fullscreen. As soon as the mouse is moved, a control element is focused or playback is paused, the controls reappear instantly. `hideControls` | Boolean | `true` | Hide video controls automatically after 2s of no mouse or focus movement, on control element blur (tab out), on playback start or entering fullscreen. As soon as the mouse is moved, a control element is focused or playback is paused, the controls reappear instantly.
`showPosterOnEnd` | Boolean | false | This will restore and *reload* HTML5 video once playback is complete. Note: depending on the browser caching, this may result in the video downloading again (or parts of it). Use with caution. `showPosterOnEnd` | Boolean | false | This will restore and *reload* HTML5 video once playback is complete. Note: depending on the browser caching, this may result in the video downloading again (or parts of it). Use with caution.
@ -241,9 +241,12 @@ Option | Type | Default | Description
`tooltips` | Object | `{ controls: false, seek: true }` | `controls`: Display control labels as tooltips on `:hover` & `:focus` (by default, the labels are screen reader only). `seek`: Display a seek tooltip to indicate on click where the media would seek to. `tooltips` | Object | `{ controls: false, seek: true }` | `controls`: Display control labels as tooltips on `:hover` & `:focus` (by default, the labels are screen reader only). `seek`: Display a seek tooltip to indicate on click where the media would seek to.
`duration` | Number | `null` | Specify a custom duration for media. `duration` | Number | `null` | Specify a custom duration for media.
`displayDuration` | Boolean | `true` | Displays the duration of the media on the "metadataloaded" event (on startup) in the current time display. This will only work if the `preload` attribute is not set to `none` (or is not set at all) and you choose not to display the duration (see `controls` option). `displayDuration` | Boolean | `true` | Displays the duration of the media on the "metadataloaded" event (on startup) in the current time display. This will only work if the `preload` attribute is not set to `none` (or is not set at all) and you choose not to display the duration (see `controls` option).
`invertTime` | Boolean | `true` | Display the current time as a countdown rather than an incremental counter.
`toggleInvert` | Boolean | `true` | Allow users to click to toggle the above.
`listeners` | Object | `null` | Allows binding of event listeners to the controls before the default handlers. See the `defaults.js` for available listeners. IF your handler prevents default on the event, the default handler will not fire. `listeners` | Object | `null` | Allows binding of event listeners to the controls before the default handlers. See the `defaults.js` for available listeners. IF your handler prevents default on the event, the default handler will not fire.
`captions` | Object | `{ active: false, language: window.navigator.language.split('-')[0] }` | `active`: Toggles if captions should be active by default. `language`: Sets the default language to load (if available). `captions` | Object | `{ active: false, language: window.navigator.language.split('-')[0] }` | `active`: Toggles if captions should be active by default. `language`: Sets the default language to load (if available).
`fullscreen` | Object | `{ enabled: true, fallback: true }` | `enabled`: Toggles whether fullscreen should be enabled. `fallback`: Allow fallback to a full-window solution. `fullscreen` | Object | `{ enabled: true, fallback: true }` | `enabled`: Toggles whether fullscreen should be enabled. `fallback`: Allow fallback to a full-window solution.
`ratio` | String | `16:9` | The aspect ratio you want to use for embedded players.
`storage` | Object | `{ enabled: true, key: 'plyr' }` | `enabled`: Allow use of local storage to store user settings. `key`: The key name to use. `storage` | Object | `{ enabled: true, key: 'plyr' }` | `enabled`: Allow use of local storage to store user settings. `key`: The key name to use.
`speed` | Object | `{ selected: 1, options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2] }` | `selected`: The default speed for playback. `options`: Options to display in the menu. Most browsers will refuse to play slower than 0.5. `speed` | Object | `{ selected: 1, options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2] }` | `selected`: The default speed for playback. `options`: Options to display in the menu. Most browsers will refuse to play slower than 0.5.
`quality` | Object | `{ default: 'default', options: ['hd2160', 'hd1440', 'hd1080', 'hd720', 'large', 'medium', 'small', 'tiny', 'default'] }` | Currently only supported by YouTube. `default` is the default quality level, determined by YouTube. `options` are the options to display. `quality` | Object | `{ default: 'default', options: ['hd2160', 'hd1440', 'hd1080', 'hd720', 'large', 'medium', 'small', 'tiny', 'default'] }` | Currently only supported by YouTube. `default` is the default quality level, determined by YouTube. `options` are the options to display.

View File

@ -79,8 +79,9 @@ const captions = {
// Filter doesn't seem to work for a TextTrackList :-( // Filter doesn't seem to work for a TextTrackList :-(
Array.from(this.captions.tracks).forEach(track => { Array.from(this.captions.tracks).forEach(track => {
if (track.language === this.captions.language.toLowerCase()) { if (track.language.toLowerCase() === this.language.toLowerCase()) {
this.captions.currentTrack = track; this.captions.currentTrack = track;
console.warn(`Set current track to ${this.language}`);
} }
}); });
}; };

167
src/js/controls.js vendored
View File

@ -115,6 +115,10 @@ const controls = {
// Create a badge // Create a badge
createBadge(text) { createBadge(text) {
if (utils.is.empty(text)) {
return null;
}
const badge = utils.createElement('span', { const badge = utils.createElement('span', {
class: this.config.classNames.menu.value, class: this.config.classNames.menu.value,
}); });
@ -319,6 +323,39 @@ const controls = {
return container; return container;
}, },
// Create a settings menu item
createMenuItem(value, list, type, title, badge = null, checked = false) {
const item = utils.createElement('li');
const label = utils.createElement('label', {
class: this.config.classNames.control,
});
const radio = utils.createElement(
'input',
utils.extend(utils.getAttributesFromSelector(this.config.selectors.inputs[type]), {
type: 'radio',
name: `plyr-${type}`,
value,
checked,
class: 'plyr__sr-only',
})
);
const faux = utils.createElement('span', { 'aria-hidden': true });
label.appendChild(radio);
label.appendChild(faux);
label.insertAdjacentHTML('beforeend', title);
if (utils.is.htmlElement(badge)) {
label.appendChild(badge);
}
item.appendChild(label);
list.appendChild(item);
},
// Update hover tooltip for seeking // Update hover tooltip for seeking
updateSeekTooltip(event) { updateSeekTooltip(event) {
// Bail if setting not true // Bail if setting not true
@ -353,7 +390,7 @@ const controls = {
} }
// Display the time a click would seek to // Display the time a click would seek to
ui.updateTimeDisplay.call(this, this.duration / 100 * percent, this.elements.display.seekTooltip); ui.updateTimeDisplay.call(this, this.elements.display.seekTooltip, this.duration / 100 * percent);
// Set position // Set position
this.elements.display.seekTooltip.style.left = `${percent}%`; this.elements.display.seekTooltip.style.left = `${percent}%`;
@ -390,6 +427,7 @@ const controls = {
// Set the YouTube quality menu // Set the YouTube quality menu
// TODO: Support for HTML5 // TODO: Support for HTML5
setQualityMenu(options) { setQualityMenu(options) {
const type = 'quality';
const list = this.elements.settings.panes.quality.querySelector('ul'); const list = this.elements.settings.panes.quality.querySelector('ul');
// Set options if passed and filter based on config // Set options if passed and filter based on config
@ -401,7 +439,7 @@ const controls = {
// Toggle the pane and tab // Toggle the pane and tab
const toggle = !utils.is.empty(this.options.quality) && this.type === 'youtube'; const toggle = !utils.is.empty(this.options.quality) && this.type === 'youtube';
controls.toggleTab.call(this, 'quality', toggle); controls.toggleTab.call(this, type, toggle);
// If we're hiding, nothing more to do // If we're hiding, nothing more to do
if (!toggle) { if (!toggle) {
@ -443,35 +481,18 @@ const controls = {
return controls.createBadge.call(this, label); return controls.createBadge.call(this, label);
}; };
this.options.quality.forEach(quality => { this.options.quality.forEach(quality =>
const item = utils.createElement('li'); controls.createMenuItem.call(
this,
const label = utils.createElement('label', { quality,
class: this.config.classNames.control, list,
}); type,
controls.getLabel.call(this, 'quality', quality),
const radio = utils.createElement( getBadge(quality)
'input', )
utils.extend(utils.getAttributesFromSelector(this.config.selectors.inputs.quality), {
type: 'radio',
name: 'plyr-quality',
value: quality,
})
); );
label.appendChild(radio); controls.updateSetting.call(this, type, list);
label.appendChild(document.createTextNode(controls.getLabel.call(this, 'quality', quality)));
const badge = getBadge(quality);
if (utils.is.htmlElement(badge)) {
label.appendChild(badge);
}
item.appendChild(label);
list.appendChild(item);
});
controls.updateSetting.call(this, 'quality', list);
}, },
// Translate a value into a nice label // Translate a value into a nice label
@ -573,7 +594,7 @@ const controls = {
}, },
// Set the looping options // Set the looping options
setLoopMenu() { /* setLoopMenu() {
const options = ['start', 'end', 'all', 'reset']; const options = ['start', 'end', 'all', 'reset'];
const list = this.elements.settings.panes.loop.querySelector('ul'); const list = this.elements.settings.panes.loop.querySelector('ul');
@ -609,7 +630,7 @@ const controls = {
item.appendChild(button); item.appendChild(button);
list.appendChild(item); list.appendChild(item);
}); });
}, }, */
// Get current selected caption language // Get current selected caption language
// TODO: rework this to user the getter in the API? // TODO: rework this to user the getter in the API?
@ -631,11 +652,13 @@ const controls = {
// Set a list of available captions languages // Set a list of available captions languages
setCaptionsMenu() { setCaptionsMenu() {
// TODO: Captions or language? Currently it's mixed
const type = 'captions';
const list = this.elements.settings.panes.captions.querySelector('ul'); const list = this.elements.settings.panes.captions.querySelector('ul');
// Toggle the pane and tab // Toggle the pane and tab
const toggle = !utils.is.empty(this.captions.tracks); const toggle = !utils.is.empty(this.captions.tracks);
controls.toggleTab.call(this, 'captions', toggle); controls.toggleTab.call(this, type, toggle);
// Empty the menu // Empty the menu
utils.emptyElement(list); utils.emptyElement(list);
@ -648,7 +671,6 @@ const controls = {
// Re-map the tracks into just the data we need // Re-map the tracks into just the data we need
const tracks = Array.from(this.captions.tracks).map(track => ({ const tracks = Array.from(this.captions.tracks).map(track => ({
language: track.language, language: track.language,
badge: true,
label: !utils.is.empty(track.label) ? track.label : track.language.toUpperCase(), label: !utils.is.empty(track.label) ? track.label : track.language.toUpperCase(),
})); }));
@ -660,41 +682,24 @@ const controls = {
// Generate options // Generate options
tracks.forEach(track => { tracks.forEach(track => {
const item = utils.createElement('li'); controls.createMenuItem.call(
this,
const label = utils.createElement('label', { track.language,
class: this.config.classNames.control, list,
}); 'language',
track.label || track.language,
const radio = utils.createElement( controls.createBadge.call(this, track.language.toUpperCase()),
'input', track.language.toLowerCase() === this.captions.language.toLowerCase()
utils.extend(utils.getAttributesFromSelector(this.config.selectors.inputs.language), {
type: 'radio',
name: 'plyr-language',
value: track.language,
})
); );
if (track.language.toLowerCase() === this.captions.language.toLowerCase()) {
radio.checked = true;
}
label.appendChild(radio);
label.appendChild(document.createTextNode(track.label || track.language));
if (track.badge) {
label.appendChild(controls.createBadge.call(this, track.language.toUpperCase()));
}
item.appendChild(label);
list.appendChild(item);
}); });
controls.updateSetting.call(this, 'captions', list); controls.updateSetting.call(this, type, list);
}, },
// Set a list of available captions languages // Set a list of available captions languages
setSpeedMenu(options) { setSpeedMenu(options) {
const type = 'speed';
// Set options if passed and filter based on config // Set options if passed and filter based on config
if (utils.is.array(options)) { if (utils.is.array(options)) {
this.options.speed = options.filter(speed => this.config.speed.options.includes(speed)); this.options.speed = options.filter(speed => this.config.speed.options.includes(speed));
@ -704,7 +709,7 @@ const controls = {
// Toggle the pane and tab // Toggle the pane and tab
const toggle = !utils.is.empty(this.options.speed); const toggle = !utils.is.empty(this.options.speed);
controls.toggleTab.call(this, 'speed', toggle); controls.toggleTab.call(this, type, toggle);
// If we're hiding, nothing more to do // If we're hiding, nothing more to do
if (!toggle) { if (!toggle) {
@ -722,39 +727,23 @@ const controls = {
utils.emptyElement(list); utils.emptyElement(list);
// Create items // Create items
this.options.speed.forEach(speed => { this.options.speed.forEach(speed =>
const item = utils.createElement('li'); controls.createMenuItem.call(this, speed, list, type, controls.getLabel.call(this, 'speed', speed))
const label = utils.createElement('label', {
class: this.config.classNames.control,
});
const radio = utils.createElement(
'input',
utils.extend(utils.getAttributesFromSelector(this.config.selectors.inputs.speed), {
type: 'radio',
name: 'plyr-speed',
value: speed,
})
); );
label.appendChild(radio); controls.updateSetting.call(this, type, list);
label.insertAdjacentHTML('beforeend', controls.getLabel.call(this, 'speed', speed));
item.appendChild(label);
list.appendChild(item);
});
controls.updateSetting.call(this, 'speed', list);
}, },
// Show/hide menu // Show/hide menu
toggleMenu(event) { toggleMenu(event) {
const { form } = this.elements.settings; const { form } = this.elements.settings;
const button = this.elements.buttons.settings; const button = this.elements.buttons.settings;
const show = utils.is.boolean(event) ? event : form && form.getAttribute('aria-hidden') === 'true'; const show = utils.is.boolean(event)
? event
: utils.is.htmlElement(form) && form.getAttribute('aria-hidden') === 'true';
if (utils.is.event(event)) { if (utils.is.event(event)) {
const isMenuItem = form && form.contains(event.target); const isMenuItem = utils.is.htmlElement(form) && form.contains(event.target);
const isButton = event.target === this.elements.buttons.settings; const isButton = event.target === this.elements.buttons.settings;
// If the click was inside the form or if the click // If the click was inside the form or if the click
@ -771,10 +760,11 @@ const controls = {
} }
// Set form and button attributes // Set form and button attributes
if (button) { if (utils.is.htmlElement(button)) {
button.setAttribute('aria-expanded', show); button.setAttribute('aria-expanded', show);
} }
if (form) {
if (utils.is.htmlElement(form)) {
form.setAttribute('aria-hidden', !show); form.setAttribute('aria-hidden', !show);
if (show) { if (show) {
@ -882,6 +872,9 @@ const controls = {
pane.setAttribute('aria-hidden', !show); pane.setAttribute('aria-hidden', !show);
tab.setAttribute('aria-expanded', show); tab.setAttribute('aria-expanded', show);
pane.removeAttribute('tabindex'); pane.removeAttribute('tabindex');
// Focus the first item
pane.querySelectorAll('button:not(:disabled), input:not(:disabled), [tabindex]')[0].focus();
}, },
// Build the default HTML // Build the default HTML

View File

@ -22,13 +22,20 @@ const defaults = {
// Pass a custom duration // Pass a custom duration
duration: null, duration: null,
// Display the media duration // Display the media duration on load in the current time position
// If you have opted to display both duration and currentTime, this is ignored
displayDuration: true, displayDuration: true,
// Invert the current time to be a countdown
invertTime: true,
// Clicking the currentTime inverts it's value to show time left rather than elapsed
toggleInvert: true,
// Aspect ratio (for embeds) // Aspect ratio (for embeds)
ratio: '16:9', ratio: '16:9',
// Click video to play // Click video container to play/pause
clickToPlay: true, clickToPlay: true,
// Auto hide the controls // Auto hide the controls
@ -276,6 +283,7 @@ const defaults = {
isIos: 'plyr--is-ios', isIos: 'plyr--is-ios',
isTouch: 'plyr--is-touch', isTouch: 'plyr--is-touch',
uiSupported: 'plyr--full-ui', uiSupported: 'plyr--full-ui',
noTransition: 'plyr--no-transition',
menu: { menu: {
value: 'plyr__menu__value', value: 'plyr__menu__value',
badge: 'plyr__badge', badge: 'plyr__badge',
@ -298,6 +306,11 @@ const defaults = {
}, },
tabFocus: 'plyr__tab-focus', tabFocus: 'plyr__tab-focus',
}, },
// API keys
keys: {
google: null,
},
}; };
export default defaults; export default defaults;

View File

@ -209,7 +209,7 @@ const listeners = {
// Toggle controls on mouse events and entering fullscreen // Toggle controls on mouse events and entering fullscreen
utils.on( utils.on(
this.elements.container, this.elements.container,
'mouseenter mouseleave mousemove touchstart touchend touchcancel touchmove enterfullscreen', 'click mouseenter mouseleave mousemove touchmove enterfullscreen exitfullscreen',
event => { event => {
this.toggleControls(event); this.toggleControls(event);
} }
@ -217,11 +217,11 @@ const listeners = {
} }
// Handle user exiting fullscreen by escaping etc // Handle user exiting fullscreen by escaping etc
if (fullscreen.enabled) { /* if (fullscreen.enabled) {
utils.on(document, fullscreen.eventType, event => { utils.on(document, fullscreen.eventType, event => {
this.toggleFullscreen(event); this.toggleFullscreen(event);
}); });
} } */
}, },
// Listen for media events // Listen for media events
@ -230,7 +230,7 @@ const listeners = {
utils.on(this.media, 'timeupdate seeking', event => ui.timeUpdate.call(this, event)); utils.on(this.media, 'timeupdate seeking', event => ui.timeUpdate.call(this, event));
// Display duration // Display duration
utils.on(this.media, 'durationchange loadedmetadata', event => ui.displayDuration.call(this, event)); utils.on(this.media, 'durationchange loadedmetadata', event => ui.durationUpdate.call(this, event));
// Handle the media finishing // Handle the media finishing
utils.on(this.media, 'ended', () => { utils.on(this.media, 'ended', () => {
@ -463,11 +463,16 @@ const listeners = {
controls.showTab.call(this, event); controls.showTab.call(this, event);
// Settings menu items - use event delegation as items are added/removed // Settings menu items - use event delegation as items are added/removed
// Settings - Language
if (utils.matches(event.target, this.config.selectors.inputs.language)) { if (utils.matches(event.target, this.config.selectors.inputs.language)) {
// Settings - Language
proxy(event, 'language', () => { proxy(event, 'language', () => {
this.toggleCaptions(true); const language = event.target.value;
this.toggleCaptions(!utils.is.empty(language));
if (!utils.is.empty(language)) {
this.language = event.target.value.toLowerCase(); this.language = event.target.value.toLowerCase();
}
}); });
} else if (utils.matches(event.target, this.config.selectors.inputs.quality)) { } else if (utils.matches(event.target, this.config.selectors.inputs.quality)) {
// Settings - Quality // Settings - Quality
@ -479,7 +484,7 @@ const listeners = {
proxy(event, 'speed', () => { proxy(event, 'speed', () => {
this.speed = parseFloat(event.target.value); this.speed = parseFloat(event.target.value);
}); });
} else if (utils.matches(event.target, this.config.selectors.buttons.loop)) { } /* else if (utils.matches(event.target, this.config.selectors.buttons.loop)) {
// Settings - Looping // Settings - Looping
// TODO: use toggle buttons // TODO: use toggle buttons
proxy(event, 'loop', () => { proxy(event, 'loop', () => {
@ -488,7 +493,7 @@ const listeners = {
this.console.warn('Set loop'); this.console.warn('Set loop');
}); });
} } */
}); });
// Seek // Seek
@ -498,6 +503,20 @@ const listeners = {
}) })
); );
// Current time invert
// Only if one time element is used for both currentTime and duration
if (this.config.toggleInvert && !utils.is.htmlElement(this.elements.display.duration)) {
utils.on(this.elements.display.currentTime, 'click', () => {
// Do nothing if we're at the start
if (this.currentTime === 0) {
return;
}
this.config.invertTime = !this.config.invertTime;
ui.timeUpdate.call(this);
});
}
// Volume // Volume
utils.on(this.elements.inputs.volume, inputEvent, event => utils.on(this.elements.inputs.volume, inputEvent, event =>
proxy(event, 'volume', () => { proxy(event, 'volume', () => {
@ -533,7 +552,7 @@ const listeners = {
// TODO: Check we need capture here // TODO: Check we need capture here
utils.on( utils.on(
this.elements.controls, this.elements.controls,
'focus blur', 'focusin focusout',
event => { event => {
this.toggleControls(event); this.toggleControls(event);
}, },

View File

@ -23,19 +23,21 @@ const youtube = {
// Set ID // Set ID
this.media.setAttribute('id', utils.generateId(this.type)); this.media.setAttribute('id', utils.generateId(this.type));
// Get the title // Get the media title via Google API
const key = 'AIzaSyDrNwtN3nLH_8rjCmu5Wq3ZCm4MNAVdc0c'; const key = this.config.keys.google;
const url = `https://www.googleapis.com/youtube/v3/videos?id=${videoId}&fields=items(snippet(title))&part=snippet&key=${key}`; if (utils.is.string(key) && !utils.is.empty(key)) {
const url = `https://www.googleapis.com/youtube/v3/videos?id=${videoId}&key=${key}&fields=items(snippet(title))&part=snippet`;
fetch(url) fetch(url)
.then(response => response.json()) .then(response => (response.ok ? response.json() : null))
.then(obj => { .then(result => {
if (utils.is.object(obj)) { if (result !== null && utils.is.object(result)) {
this.config.title = obj.items[0].snippet.title; this.config.title = result.items[0].snippet.title;
ui.setTitle.call(this); ui.setTitle.call(this);
} }
}) })
.catch(() => {}); .catch(() => {});
}
// Setup API // Setup API
if (utils.is.object(window.YT)) { if (utils.is.object(window.YT)) {

View File

@ -669,7 +669,7 @@ class Plyr {
const language = input.toLowerCase(); const language = input.toLowerCase();
// If nothing to change, bail // If nothing to change, bail
if (this.captions.language === language) { if (this.language === language) {
return; return;
} }
@ -797,31 +797,28 @@ class Plyr {
// Show the player controls in fullscreen mode // Show the player controls in fullscreen mode
toggleControls(toggle) { toggleControls(toggle) {
const player = this;
// We need controls of course... // We need controls of course...
if (!utils.is.htmlElement(this.elements.controls)) { if (!utils.is.htmlElement(this.elements.controls)) {
return player; return this;
} }
// Don't hide if config says not to, it's audio, or not ready or loading // Don't hide if config says not to, it's audio, or not ready or loading
if (!this.supported.ui || !this.config.hideControls || this.type === 'audio') { if (!this.supported.ui || !this.config.hideControls || this.type === 'audio') {
return player; return this;
} }
let delay = 0; let delay = 0;
let show = toggle; let show = toggle;
let isEnterFullscreen = false; let isEnterFullscreen = false;
const loading = utils.hasClass(this.elements.container, this.config.classNames.loading);
// Default to false if no boolean // Get toggle state if not set
if (!utils.is.boolean(toggle)) { if (!utils.is.boolean(toggle)) {
if (utils.is.event(toggle)) { if (utils.is.event(toggle)) {
// Is the enter fullscreen event // Is the enter fullscreen event
isEnterFullscreen = toggle.type === 'enterfullscreen'; isEnterFullscreen = toggle.type === 'enterfullscreen';
// Whether to show controls // Whether to show controls
show = ['mousemove', 'touchstart', 'mouseenter', 'focus'].includes(toggle.type); show = ['click', 'mousemove', 'touchmove', 'mouseenter', 'focusin'].includes(toggle.type);
// Delay hiding on move events // Delay hiding on move events
if (['mousemove', 'touchmove'].includes(toggle.type)) { if (['mousemove', 'touchmove'].includes(toggle.type)) {
@ -829,8 +826,9 @@ class Plyr {
} }
// Delay a little more for keyboard users // Delay a little more for keyboard users
if (toggle.type === 'focus') { if (toggle.type === 'focusin') {
delay = 3000; delay = 3000;
utils.toggleClass(this.elements.controls, this.config.classNames.noTransition, true);
} }
} else { } else {
show = utils.hasClass(this.elements.container, this.config.classNames.hideControls); show = utils.hasClass(this.elements.container, this.config.classNames.hideControls);
@ -841,7 +839,7 @@ class Plyr {
window.clearTimeout(this.timers.hover); window.clearTimeout(this.timers.hover);
// If the mouse is not over the controls, set a timeout to hide them // If the mouse is not over the controls, set a timeout to hide them
if (show || this.media.paused || loading) { if (show || this.media.paused || this.loading) {
// Check if controls toggled // Check if controls toggled
const toggled = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, false); const toggled = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, false);
@ -851,8 +849,8 @@ class Plyr {
} }
// Always show controls when paused or if touch // Always show controls when paused or if touch
if (this.media.paused || loading) { if (this.media.paused || this.loading) {
return player; return this;
} }
// Delay for hiding on touch // Delay for hiding on touch
@ -870,6 +868,11 @@ class Plyr {
return; return;
} }
// Restore transition behaviour
if (!utils.hasClass(this.elements.container, this.config.classNames.hideControls)) {
utils.toggleClass(this.elements.controls, this.config.classNames.noTransition, false);
}
// Check if controls toggled // Check if controls toggled
const toggled = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, true); const toggled = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, true);

View File

@ -15,7 +15,7 @@ const ui = {
}, },
// Toggle native HTML5 media controls // Toggle native HTML5 media controls
toggleNativeControls(toggle) { toggleNativeControls(toggle = false) {
if (toggle && this.isHTML5) { if (toggle && this.isHTML5) {
this.media.setAttribute('controls', ''); this.media.setAttribute('controls', '');
} else { } else {
@ -100,26 +100,6 @@ const ui = {
ui.setTitle.call(this); ui.setTitle.call(this);
}, },
// Show the duration on metadataloaded
displayDuration() {
if (!this.supported.ui) {
return;
}
// If there's only one time display, display duration there
if (!this.elements.display.duration && this.config.displayDuration && this.paused) {
ui.updateTimeDisplay.call(this, this.duration, this.elements.display.currentTime);
}
// If there's a duration element, update content
if (this.elements.display.duration) {
ui.updateTimeDisplay.call(this, this.duration, this.elements.display.duration);
}
// Update the tooltip (if visible)
controls.updateSeekTooltip.call(this);
},
// Setup aria attribute for play and iframe title // Setup aria attribute for play and iframe title
setTitle() { setTitle() {
// Find the current text // Find the current text
@ -165,23 +145,6 @@ const ui = {
this.toggleControls(this.paused); this.toggleControls(this.paused);
}, },
// Update volume UI and storage
updateVolume() {
if (!this.supported.ui) {
return;
}
// Update range
if (utils.is.htmlElement(this.elements.inputs.volume)) {
ui.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);
}
// Update checkbox for mute state
if (utils.is.htmlElement(this.elements.buttons.mute)) {
utils.toggleState(this.elements.buttons.mute, this.muted || this.volume === 0);
}
},
// Check if media is loading // Check if media is loading
checkLoading(event) { checkLoading(event) {
this.loading = event.type === 'waiting'; this.loading = event.type === 'waiting';
@ -199,8 +162,25 @@ const ui = {
}, this.loading ? 250 : 0); }, this.loading ? 250 : 0);
}, },
// Update volume UI and storage
updateVolume() {
if (!this.supported.ui) {
return;
}
// Update range
if (utils.is.htmlElement(this.elements.inputs.volume)) {
ui.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);
}
// Update checkbox for mute state
if (utils.is.htmlElement(this.elements.buttons.mute)) {
utils.toggleState(this.elements.buttons.mute, this.muted || this.volume === 0);
}
},
// Update seek value and lower fill // Update seek value and lower fill
setRange(target, value) { setRange(target, value = 0) {
if (!utils.is.htmlElement(target)) { if (!utils.is.htmlElement(target)) {
return; return;
} }
@ -214,9 +194,8 @@ const ui = {
// Set <progress> value // Set <progress> value
setProgress(target, input) { setProgress(target, input) {
// Default to 0 const value = utils.is.number(input) ? input : 0;
const value = !utils.is.undefined(input) ? input : 0; const progress = utils.is.htmlElement(target) ? target : this.elements.display.buffer;
const progress = !utils.is.undefined(target) ? target : this.elements.display.buffer;
// Update value and label // Update value and label
if (utils.is.htmlElement(progress)) { if (utils.is.htmlElement(progress)) {
@ -232,7 +211,7 @@ const ui = {
// Update <progress> elements // Update <progress> elements
updateProgress(event) { updateProgress(event) {
if (!this.supported.ui) { if (!this.supported.ui || !utils.is.event(event)) {
return; return;
} }
@ -280,41 +259,49 @@ const ui = {
}, },
// Update the displayed time // Update the displayed time
updateTimeDisplay(value, element) { updateTimeDisplay(target = null, time = 0, inverted = false) {
// Bail if there's no duration display // Bail if there's no element to display or the value isn't a number
if (!utils.is.htmlElement(element)) { if (!utils.is.htmlElement(target) || !utils.is.number(time)) {
return null; return;
} }
// Fallback to 0 // Format time component to add leading zero
const time = !Number.isNaN(value) ? value : 0; const format = value => `0${value}`.slice(-2);
let secs = parseInt(time % 60, 10); // Helpers
let mins = parseInt((time / 60) % 60, 10); const getHours = value => parseInt((value / 60 / 60) % 60, 10);
const hours = parseInt((time / 60 / 60) % 60, 10); const getMinutes = value => parseInt((value / 60) % 60, 10);
const getSeconds = value => parseInt(value % 60, 10);
// Breakdown to hours, mins, secs
let hours = getHours(time);
const mins = getMinutes(time);
const secs = getSeconds(time);
// Do we need to display hours? // Do we need to display hours?
const displayHours = parseInt((this.duration / 60 / 60) % 60, 10) > 0; if (getHours(this.duration) > 0) {
hours = `${hours}:`;
// Ensure it's two digits. For example, 03 rather than 3. } else {
secs = `0${secs}`.slice(-2); hours = '';
mins = `0${mins}`.slice(-2); }
// Generate display
const display = `${(displayHours ? `${hours}:` : '') + mins}:${secs}`;
// Render // Render
// eslint-disable-next-line // eslint-disable-next-line no-param-reassign
element.textContent = display; target.textContent = `${inverted ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;
// Return for looping
return display;
}, },
// Handle time change event // Handle time change event
timeUpdate(event) { timeUpdate(event) {
// Only invert if only one time element is displayed and used for both duration and currentTime
const invert = !utils.is.htmlElement(this.elements.display.duration) && this.config.invertTime;
// Duration // Duration
ui.updateTimeDisplay.call(this, this.currentTime, this.elements.display.currentTime); ui.updateTimeDisplay.call(
this,
this.elements.display.currentTime,
invert ? this.duration - this.currentTime : this.currentTime,
invert
);
// Ignore updates while seeking // Ignore updates while seeking
if (event && event.type === 'timeupdate' && this.media.seeking) { if (event && event.type === 'timeupdate' && this.media.seeking) {
@ -324,6 +311,26 @@ const ui = {
// Playing progress // Playing progress
ui.updateProgress.call(this, event); ui.updateProgress.call(this, event);
}, },
// Show the duration on metadataloaded
durationUpdate() {
if (!this.supported.ui) {
return;
}
// If there's only one time display, display duration there
if (!utils.is.htmlElement(this.elements.display.duration) && this.config.displayDuration && this.paused) {
ui.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration);
}
// If there's a duration element, update content
if (utils.is.htmlElement(this.elements.display.duration)) {
ui.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);
}
// Update the tooltip (if visible)
controls.updateSeekTooltip.call(this);
},
}; };
export default ui; export default ui;

View File

@ -31,6 +31,9 @@ const utils = {
htmlElement(input) { htmlElement(input) {
return !this.undefined(input) && input instanceof HTMLElement; return !this.undefined(input) && input instanceof HTMLElement;
}, },
textNode(input) {
return this.getConstructor(input) === Text;
},
event(input) { event(input) {
return !this.undefined(input) && input instanceof Event; return !this.undefined(input) && input instanceof Event;
}, },
@ -49,8 +52,8 @@ const utils = {
return ( return (
input === null || input === null ||
typeof input === 'undefined' || typeof input === 'undefined' ||
((this.string(input) || this.array(input) || this.nodeList(input)) && input.length === 0) || ((this.string(input) || this.array(input) || this.nodeList(input)) && !input.length) ||
(this.object(input) && Object.keys(input).length === 0) (this.object(input) && !Object.keys(input).length)
); );
}, },
getConstructor(input) { getConstructor(input) {
@ -140,8 +143,12 @@ const utils = {
// Get the sprite // Get the sprite
fetch(url) fetch(url)
.then(response => response.text()) .then(response => (response.ok ? response.text() : null))
.then(text => { .then(text => {
if (text === null) {
return;
}
if (support.storage) { if (support.storage) {
window.localStorage.setItem( window.localStorage.setItem(
prefix + id, prefix + id,
@ -152,7 +159,8 @@ const utils = {
} }
updateSprite.call(container, text); updateSprite.call(container, text);
}); })
.catch(() => {});
} }
}, },
@ -201,22 +209,6 @@ const utils = {
}); });
}, },
// Remove an element
removeElement(element) {
if (!utils.is.htmlElement(element) || !utils.is.htmlElement(element.parentNode)) {
return null;
}
element.parentNode.removeChild(element);
return element;
},
// Inaert an element after another
insertAfter(element, target) {
target.parentNode.insertBefore(element, target.nextSibling);
},
// Create a DocumentFragment // Create a DocumentFragment
createElement(type, attributes, text) { createElement(type, attributes, text) {
// Create a new <element> // Create a new <element>
@ -236,12 +228,28 @@ const utils = {
return element; return element;
}, },
// Inaert an element after another
insertAfter(element, target) {
target.parentNode.insertBefore(element, target.nextSibling);
},
// Insert a DocumentFragment // Insert a DocumentFragment
insertElement(type, parent, attributes, text) { insertElement(type, parent, attributes, text) {
// Inject the new <element> // Inject the new <element>
parent.appendChild(utils.createElement(type, attributes, text)); parent.appendChild(utils.createElement(type, attributes, text));
}, },
// Remove an element
removeElement(element) {
if (!utils.is.htmlElement(element) || !utils.is.htmlElement(element.parentNode)) {
return null;
}
element.parentNode.removeChild(element);
return element;
},
// Remove all child elements // Remove all child elements
emptyElement(element) { emptyElement(element) {
let { length } = element.childNodes; let { length } = element.childNodes;
@ -433,9 +441,9 @@ const utils = {
// Trap focus inside container // Trap focus inside container
trapFocus() { trapFocus() {
const tabbables = utils.getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]'); const focusable = utils.getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]');
const first = tabbables[0]; const first = focusable[0];
const last = tabbables[tabbables.length - 1]; const last = focusable[focusable.length - 1];
utils.on( utils.on(
this.elements.container, this.elements.container,

View File

@ -12,6 +12,7 @@
line-height: @plyr-line-height; line-height: @plyr-line-height;
direction: ltr; direction: ltr;
text-shadow: none; text-shadow: none;
transition: box-shadow 0.3s ease;
.plyr-font-smoothing(off); .plyr-font-smoothing(off);
// Media elements // Media elements
@ -22,6 +23,11 @@
vertical-align: middle; vertical-align: middle;
border-radius: inherit; border-radius: inherit;
} }
&:focus {
outline: 0;
box-shadow: 0 0 0 3px fade(#000, 10%);
}
} }
// Full UI only // Full UI only

View File

@ -26,4 +26,5 @@
@import 'states/fullscreen'; @import 'states/fullscreen';
@import 'utils/animation';
@import 'utils/hidden'; @import 'utils/hidden';

View File

@ -3,11 +3,11 @@
// -------------------------------------------------------------- // --------------------------------------------------------------
.plyr__badge { .plyr__badge {
padding: 0 4px; padding: 3px 4px;
border-radius: 2px; border-radius: 2px;
background: @plyr-menu-color; background: @plyr-menu-color;
color: @plyr-menu-bg; color: @plyr-menu-bg;
font-size: @plyr-font-size-tiny; font-size: @plyr-font-size-tiny;
line-height: 1.5; line-height: 1;
.plyr-font-smoothing(on); .plyr-font-smoothing(on);
} }

View File

@ -57,9 +57,9 @@
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
padding: ceil(@plyr-control-spacing * 1.25); padding: ceil(@plyr-control-spacing * 1.5);
background: fade(@plyr-video-control-bg-hover, 80%); background: fade(@plyr-video-control-bg-hover, 80%);
border: 3px solid currentColor; border: 0;
border-radius: 100%; border-radius: 100%;
box-shadow: 0 1px 1px fade(#000, 15%); box-shadow: 0 1px 1px fade(#000, 15%);
color: @plyr-video-control-color; color: @plyr-video-control-color;
@ -81,7 +81,12 @@
} }
&:focus { &:focus {
outline: 1px dotted fade(@plyr-video-control-color, 50%); outline: 0;
}
&.plyr__tab-focus {
outline: 0;
box-shadow: 0 0 0 3px fade(@plyr-video-control-color, 50%);
} }
} }

View File

@ -129,11 +129,45 @@
} }
label.plyr__control { label.plyr__control {
padding-left: ceil(@plyr-control-padding * 2.5); padding-left: @plyr-control-padding;
input[type='radio'] { /*input[type='radio'] {
position: relative; position: relative;
left: -@plyr-control-padding; left: -@plyr-control-padding;
}*/
input[type='radio'] + span {
position: relative;
display: block;
height: 14px;
width: 14px;
border-radius: 100%;
background: fade(#000, 10%);
margin-right: @plyr-control-spacing;
box-shadow: inset 0 1px 1px fade(#000, 15%);
&::after {
content: '';
position: absolute;
height: 6px;
width: 6px;
top: 4px;
left: 4px;
transform: scale(0);
opacity: 0;
background: #fff;
border-radius: 100%;
transition: transform 0.3s ease, opacity 0.3s ease;
}
}
input[type='radio']:checked + span {
background: @plyr-color-main;
&::after {
transform: scale(1);
opacity: 1;
}
} }
} }

View File

@ -12,6 +12,8 @@
cursor: pointer; cursor: pointer;
border: none; border: none;
background: transparent; background: transparent;
transition: box-shadow 0.3s ease;
border-radius: (@plyr-range-thumb-height * 2);
// Used in JS to populate lower fill for WebKit // Used in JS to populate lower fill for WebKit
color: @plyr-range-selected-bg; color: @plyr-range-selected-bg;
@ -79,10 +81,6 @@
border: 0; border: 0;
} }
&.plyr__tab-focus {
outline-offset: 3px;
}
// Pressed styles // Pressed styles
&:active { &:active {
&::-webkit-slider-thumb { &::-webkit-slider-thumb {
@ -114,7 +112,7 @@
} }
&.plyr__tab-focus { &.plyr__tab-focus {
outline: 1px dotted fade(@plyr-video-control-color, 50%); box-shadow: 0 0 0 3px fade(@plyr-video-control-color, 50%);
} }
} }
@ -133,6 +131,6 @@
} }
&.plyr__tab-focus { &.plyr__tab-focus {
outline: 1px dotted fade(@plyr-audio-control-color, 50%); box-shadow: 0 0 0 3px fade(@plyr-audio-control-color, 50%);
} }
} }

View File

@ -0,0 +1,7 @@
// --------------------------------------------------------------
// Animation utils
// --------------------------------------------------------------
.plyr--no-transition {
transition: none !important;
}