Controls cleanup, work on captions bug, click to invert time
This commit is contained in:
parent
d7a1c44281
commit
6984d6fb16
2
demo/dist/demo.css
vendored
2
demo/dist/demo.css
vendored
File diff suppressed because one or more lines are too long
2
demo/dist/demo.js
vendored
2
demo/dist/demo.js
vendored
@ -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
|
||||
|
2
demo/dist/demo.js.map
vendored
2
demo/dist/demo.js.map
vendored
File diff suppressed because one or more lines are too long
2
demo/dist/error.css
vendored
2
demo/dist/error.css
vendored
File diff suppressed because one or more lines are too long
@ -82,17 +82,16 @@
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<video controls crossorigin playsinline loop poster="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg"
|
||||
id="player">
|
||||
<video controls crossorigin playsinline loop poster="media/View_From_A_Blue_Moon_Trailer-HD.jpg" id="player">
|
||||
<!-- 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 -->
|
||||
<track kind="captions" label="English" srclang="en" src="webvtt/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="English" srclang="en" src="media/View_From_A_Blue_Moon_Trailer-HD.en.vtt" default>
|
||||
<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 -->
|
||||
<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>
|
||||
|
||||
<ul>
|
||||
|
BIN
demo/media/View_From_A_Blue_Moon_Trailer-HD.jpg
Normal file
BIN
demo/media/View_From_A_Blue_Moon_Trailer-HD.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 154 KiB |
@ -65,6 +65,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
'pip',
|
||||
'airplay',
|
||||
],
|
||||
keys: {
|
||||
google: 'AIzaSyDrNwtN3nLH_8rjCmu5Wq3ZCm4MNAVdc0c',
|
||||
},
|
||||
});
|
||||
|
||||
// Expose for testing
|
||||
@ -102,24 +105,24 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
title: 'View From A Blue Moon',
|
||||
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',
|
||||
},
|
||||
],
|
||||
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: [
|
||||
{
|
||||
kind: 'captions',
|
||||
label: 'English',
|
||||
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,
|
||||
},
|
||||
{
|
||||
kind: 'captions',
|
||||
label: 'French',
|
||||
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',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -10,7 +10,7 @@ video {
|
||||
|
||||
// Example players
|
||||
.plyr {
|
||||
margin: 0 auto;
|
||||
margin: @spacing-base auto;
|
||||
border-radius: @border-radius-base;
|
||||
box-shadow: 0 2px 5px fade(#000, 20%);
|
||||
}
|
||||
|
@ -27,15 +27,18 @@ body {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.grid {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
main {
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.grid {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
aside {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
@font-face {
|
||||
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');
|
||||
font-style: normal;
|
||||
font-weight: @font-weight-base;
|
||||
@ -13,7 +13,7 @@
|
||||
|
||||
@font-face {
|
||||
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');
|
||||
font-style: normal;
|
||||
font-weight: @font-weight-bold;
|
||||
@ -22,7 +22,7 @@
|
||||
|
||||
@font-face {
|
||||
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');
|
||||
font-style: normal;
|
||||
font-weight: @font-weight-heavy;
|
||||
|
@ -33,6 +33,7 @@
|
||||
// ---------------------------------------
|
||||
.font-size(@font-size: 16) {
|
||||
@rem: round((@font-size / 16), 3);
|
||||
|
||||
font-size: (@font-size * 1px);
|
||||
font-size: ~'@{rem}rem';
|
||||
}
|
||||
|
2
dist/plyr.css
vendored
2
dist/plyr.css
vendored
File diff suppressed because one or more lines are too long
2
dist/plyr.js
vendored
2
dist/plyr.js
vendored
File diff suppressed because one or more lines are too long
2
dist/plyr.js.map
vendored
2
dist/plyr.js.map
vendored
File diff suppressed because one or more lines are too long
@ -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.
|
||||
`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.
|
||||
`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.
|
||||
`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.
|
||||
@ -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.
|
||||
`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).
|
||||
`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.
|
||||
`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.
|
||||
`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.
|
||||
`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.
|
||||
|
@ -79,8 +79,9 @@ const captions = {
|
||||
|
||||
// Filter doesn't seem to work for a TextTrackList :-(
|
||||
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;
|
||||
console.warn(`Set current track to ${this.language}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
171
src/js/controls.js
vendored
171
src/js/controls.js
vendored
@ -115,6 +115,10 @@ const controls = {
|
||||
|
||||
// Create a badge
|
||||
createBadge(text) {
|
||||
if (utils.is.empty(text)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const badge = utils.createElement('span', {
|
||||
class: this.config.classNames.menu.value,
|
||||
});
|
||||
@ -319,6 +323,39 @@ const controls = {
|
||||
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
|
||||
updateSeekTooltip(event) {
|
||||
// Bail if setting not true
|
||||
@ -353,7 +390,7 @@ const controls = {
|
||||
}
|
||||
|
||||
// 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
|
||||
this.elements.display.seekTooltip.style.left = `${percent}%`;
|
||||
@ -390,6 +427,7 @@ const controls = {
|
||||
// Set the YouTube quality menu
|
||||
// TODO: Support for HTML5
|
||||
setQualityMenu(options) {
|
||||
const type = 'quality';
|
||||
const list = this.elements.settings.panes.quality.querySelector('ul');
|
||||
|
||||
// Set options if passed and filter based on config
|
||||
@ -401,7 +439,7 @@ const controls = {
|
||||
|
||||
// Toggle the pane and tab
|
||||
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 (!toggle) {
|
||||
@ -443,35 +481,18 @@ const controls = {
|
||||
return controls.createBadge.call(this, label);
|
||||
};
|
||||
|
||||
this.options.quality.forEach(quality => {
|
||||
const item = utils.createElement('li');
|
||||
this.options.quality.forEach(quality =>
|
||||
controls.createMenuItem.call(
|
||||
this,
|
||||
quality,
|
||||
list,
|
||||
type,
|
||||
controls.getLabel.call(this, 'quality', quality),
|
||||
getBadge(quality)
|
||||
)
|
||||
);
|
||||
|
||||
const label = utils.createElement('label', {
|
||||
class: this.config.classNames.control,
|
||||
});
|
||||
|
||||
const radio = utils.createElement(
|
||||
'input',
|
||||
utils.extend(utils.getAttributesFromSelector(this.config.selectors.inputs.quality), {
|
||||
type: 'radio',
|
||||
name: 'plyr-quality',
|
||||
value: quality,
|
||||
})
|
||||
);
|
||||
|
||||
label.appendChild(radio);
|
||||
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);
|
||||
controls.updateSetting.call(this, type, list);
|
||||
},
|
||||
|
||||
// Translate a value into a nice label
|
||||
@ -573,7 +594,7 @@ const controls = {
|
||||
},
|
||||
|
||||
// Set the looping options
|
||||
setLoopMenu() {
|
||||
/* setLoopMenu() {
|
||||
const options = ['start', 'end', 'all', 'reset'];
|
||||
const list = this.elements.settings.panes.loop.querySelector('ul');
|
||||
|
||||
@ -609,7 +630,7 @@ const controls = {
|
||||
item.appendChild(button);
|
||||
list.appendChild(item);
|
||||
});
|
||||
},
|
||||
}, */
|
||||
|
||||
// Get current selected caption language
|
||||
// TODO: rework this to user the getter in the API?
|
||||
@ -631,11 +652,13 @@ const controls = {
|
||||
|
||||
// Set a list of available captions languages
|
||||
setCaptionsMenu() {
|
||||
// TODO: Captions or language? Currently it's mixed
|
||||
const type = 'captions';
|
||||
const list = this.elements.settings.panes.captions.querySelector('ul');
|
||||
|
||||
// Toggle the pane and tab
|
||||
const toggle = !utils.is.empty(this.captions.tracks);
|
||||
controls.toggleTab.call(this, 'captions', toggle);
|
||||
controls.toggleTab.call(this, type, toggle);
|
||||
|
||||
// Empty the menu
|
||||
utils.emptyElement(list);
|
||||
@ -648,7 +671,6 @@ const controls = {
|
||||
// Re-map the tracks into just the data we need
|
||||
const tracks = Array.from(this.captions.tracks).map(track => ({
|
||||
language: track.language,
|
||||
badge: true,
|
||||
label: !utils.is.empty(track.label) ? track.label : track.language.toUpperCase(),
|
||||
}));
|
||||
|
||||
@ -660,41 +682,24 @@ const controls = {
|
||||
|
||||
// Generate options
|
||||
tracks.forEach(track => {
|
||||
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.language), {
|
||||
type: 'radio',
|
||||
name: 'plyr-language',
|
||||
value: track.language,
|
||||
})
|
||||
controls.createMenuItem.call(
|
||||
this,
|
||||
track.language,
|
||||
list,
|
||||
'language',
|
||||
track.label || track.language,
|
||||
controls.createBadge.call(this, track.language.toUpperCase()),
|
||||
track.language.toLowerCase() === this.captions.language.toLowerCase()
|
||||
);
|
||||
|
||||
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
|
||||
setSpeedMenu(options) {
|
||||
const type = 'speed';
|
||||
|
||||
// Set options if passed and filter based on config
|
||||
if (utils.is.array(options)) {
|
||||
this.options.speed = options.filter(speed => this.config.speed.options.includes(speed));
|
||||
@ -704,7 +709,7 @@ const controls = {
|
||||
|
||||
// Toggle the pane and tab
|
||||
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 (!toggle) {
|
||||
@ -722,39 +727,23 @@ const controls = {
|
||||
utils.emptyElement(list);
|
||||
|
||||
// Create items
|
||||
this.options.speed.forEach(speed => {
|
||||
const item = utils.createElement('li');
|
||||
this.options.speed.forEach(speed =>
|
||||
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);
|
||||
label.insertAdjacentHTML('beforeend', controls.getLabel.call(this, 'speed', speed));
|
||||
item.appendChild(label);
|
||||
list.appendChild(item);
|
||||
});
|
||||
|
||||
controls.updateSetting.call(this, 'speed', list);
|
||||
controls.updateSetting.call(this, type, list);
|
||||
},
|
||||
|
||||
// Show/hide menu
|
||||
toggleMenu(event) {
|
||||
const { form } = this.elements.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)) {
|
||||
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;
|
||||
|
||||
// If the click was inside the form or if the click
|
||||
@ -771,10 +760,11 @@ const controls = {
|
||||
}
|
||||
|
||||
// Set form and button attributes
|
||||
if (button) {
|
||||
if (utils.is.htmlElement(button)) {
|
||||
button.setAttribute('aria-expanded', show);
|
||||
}
|
||||
if (form) {
|
||||
|
||||
if (utils.is.htmlElement(form)) {
|
||||
form.setAttribute('aria-hidden', !show);
|
||||
|
||||
if (show) {
|
||||
@ -882,6 +872,9 @@ const controls = {
|
||||
pane.setAttribute('aria-hidden', !show);
|
||||
tab.setAttribute('aria-expanded', show);
|
||||
pane.removeAttribute('tabindex');
|
||||
|
||||
// Focus the first item
|
||||
pane.querySelectorAll('button:not(:disabled), input:not(:disabled), [tabindex]')[0].focus();
|
||||
},
|
||||
|
||||
// Build the default HTML
|
||||
|
@ -22,13 +22,20 @@ const defaults = {
|
||||
// Pass a custom duration
|
||||
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,
|
||||
|
||||
// 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)
|
||||
ratio: '16:9',
|
||||
|
||||
// Click video to play
|
||||
// Click video container to play/pause
|
||||
clickToPlay: true,
|
||||
|
||||
// Auto hide the controls
|
||||
@ -276,6 +283,7 @@ const defaults = {
|
||||
isIos: 'plyr--is-ios',
|
||||
isTouch: 'plyr--is-touch',
|
||||
uiSupported: 'plyr--full-ui',
|
||||
noTransition: 'plyr--no-transition',
|
||||
menu: {
|
||||
value: 'plyr__menu__value',
|
||||
badge: 'plyr__badge',
|
||||
@ -298,6 +306,11 @@ const defaults = {
|
||||
},
|
||||
tabFocus: 'plyr__tab-focus',
|
||||
},
|
||||
|
||||
// API keys
|
||||
keys: {
|
||||
google: null,
|
||||
},
|
||||
};
|
||||
|
||||
export default defaults;
|
||||
|
@ -209,7 +209,7 @@ const listeners = {
|
||||
// Toggle controls on mouse events and entering fullscreen
|
||||
utils.on(
|
||||
this.elements.container,
|
||||
'mouseenter mouseleave mousemove touchstart touchend touchcancel touchmove enterfullscreen',
|
||||
'click mouseenter mouseleave mousemove touchmove enterfullscreen exitfullscreen',
|
||||
event => {
|
||||
this.toggleControls(event);
|
||||
}
|
||||
@ -217,11 +217,11 @@ const listeners = {
|
||||
}
|
||||
|
||||
// Handle user exiting fullscreen by escaping etc
|
||||
if (fullscreen.enabled) {
|
||||
/* if (fullscreen.enabled) {
|
||||
utils.on(document, fullscreen.eventType, event => {
|
||||
this.toggleFullscreen(event);
|
||||
});
|
||||
}
|
||||
} */
|
||||
},
|
||||
|
||||
// Listen for media events
|
||||
@ -230,7 +230,7 @@ const listeners = {
|
||||
utils.on(this.media, 'timeupdate seeking', event => ui.timeUpdate.call(this, event));
|
||||
|
||||
// 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
|
||||
utils.on(this.media, 'ended', () => {
|
||||
@ -463,11 +463,16 @@ const listeners = {
|
||||
controls.showTab.call(this, event);
|
||||
|
||||
// Settings menu items - use event delegation as items are added/removed
|
||||
// Settings - Language
|
||||
if (utils.matches(event.target, this.config.selectors.inputs.language)) {
|
||||
// Settings - Language
|
||||
proxy(event, 'language', () => {
|
||||
this.toggleCaptions(true);
|
||||
this.language = event.target.value.toLowerCase();
|
||||
const language = event.target.value;
|
||||
|
||||
this.toggleCaptions(!utils.is.empty(language));
|
||||
|
||||
if (!utils.is.empty(language)) {
|
||||
this.language = event.target.value.toLowerCase();
|
||||
}
|
||||
});
|
||||
} else if (utils.matches(event.target, this.config.selectors.inputs.quality)) {
|
||||
// Settings - Quality
|
||||
@ -479,7 +484,7 @@ const listeners = {
|
||||
proxy(event, 'speed', () => {
|
||||
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
|
||||
// TODO: use toggle buttons
|
||||
proxy(event, 'loop', () => {
|
||||
@ -488,7 +493,7 @@ const listeners = {
|
||||
|
||||
this.console.warn('Set loop');
|
||||
});
|
||||
}
|
||||
} */
|
||||
});
|
||||
|
||||
// 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
|
||||
utils.on(this.elements.inputs.volume, inputEvent, event =>
|
||||
proxy(event, 'volume', () => {
|
||||
@ -533,7 +552,7 @@ const listeners = {
|
||||
// TODO: Check we need capture here
|
||||
utils.on(
|
||||
this.elements.controls,
|
||||
'focus blur',
|
||||
'focusin focusout',
|
||||
event => {
|
||||
this.toggleControls(event);
|
||||
},
|
||||
|
@ -23,19 +23,21 @@ const youtube = {
|
||||
// Set ID
|
||||
this.media.setAttribute('id', utils.generateId(this.type));
|
||||
|
||||
// Get the title
|
||||
const key = 'AIzaSyDrNwtN3nLH_8rjCmu5Wq3ZCm4MNAVdc0c';
|
||||
const url = `https://www.googleapis.com/youtube/v3/videos?id=${videoId}&fields=items(snippet(title))&part=snippet&key=${key}`;
|
||||
// Get the media title via Google API
|
||||
const key = this.config.keys.google;
|
||||
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)
|
||||
.then(response => response.json())
|
||||
.then(obj => {
|
||||
if (utils.is.object(obj)) {
|
||||
this.config.title = obj.items[0].snippet.title;
|
||||
ui.setTitle.call(this);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
fetch(url)
|
||||
.then(response => (response.ok ? response.json() : null))
|
||||
.then(result => {
|
||||
if (result !== null && utils.is.object(result)) {
|
||||
this.config.title = result.items[0].snippet.title;
|
||||
ui.setTitle.call(this);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
// Setup API
|
||||
if (utils.is.object(window.YT)) {
|
||||
|
@ -669,7 +669,7 @@ class Plyr {
|
||||
const language = input.toLowerCase();
|
||||
|
||||
// If nothing to change, bail
|
||||
if (this.captions.language === language) {
|
||||
if (this.language === language) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -797,31 +797,28 @@ class Plyr {
|
||||
|
||||
// Show the player controls in fullscreen mode
|
||||
toggleControls(toggle) {
|
||||
const player = this;
|
||||
|
||||
// We need controls of course...
|
||||
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
|
||||
if (!this.supported.ui || !this.config.hideControls || this.type === 'audio') {
|
||||
return player;
|
||||
return this;
|
||||
}
|
||||
|
||||
let delay = 0;
|
||||
let show = toggle;
|
||||
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.event(toggle)) {
|
||||
// Is the enter fullscreen event
|
||||
isEnterFullscreen = toggle.type === 'enterfullscreen';
|
||||
|
||||
// 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
|
||||
if (['mousemove', 'touchmove'].includes(toggle.type)) {
|
||||
@ -829,8 +826,9 @@ class Plyr {
|
||||
}
|
||||
|
||||
// Delay a little more for keyboard users
|
||||
if (toggle.type === 'focus') {
|
||||
if (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);
|
||||
@ -841,7 +839,7 @@ class Plyr {
|
||||
window.clearTimeout(this.timers.hover);
|
||||
|
||||
// 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
|
||||
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
|
||||
if (this.media.paused || loading) {
|
||||
return player;
|
||||
if (this.media.paused || this.loading) {
|
||||
return this;
|
||||
}
|
||||
|
||||
// Delay for hiding on touch
|
||||
@ -870,6 +868,11 @@ class Plyr {
|
||||
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
|
||||
const toggled = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, true);
|
||||
|
||||
|
139
src/js/ui.js
139
src/js/ui.js
@ -15,7 +15,7 @@ const ui = {
|
||||
},
|
||||
|
||||
// Toggle native HTML5 media controls
|
||||
toggleNativeControls(toggle) {
|
||||
toggleNativeControls(toggle = false) {
|
||||
if (toggle && this.isHTML5) {
|
||||
this.media.setAttribute('controls', '');
|
||||
} else {
|
||||
@ -100,26 +100,6 @@ const ui = {
|
||||
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
|
||||
setTitle() {
|
||||
// Find the current text
|
||||
@ -165,23 +145,6 @@ const ui = {
|
||||
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
|
||||
checkLoading(event) {
|
||||
this.loading = event.type === 'waiting';
|
||||
@ -199,8 +162,25 @@ const ui = {
|
||||
}, 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
|
||||
setRange(target, value) {
|
||||
setRange(target, value = 0) {
|
||||
if (!utils.is.htmlElement(target)) {
|
||||
return;
|
||||
}
|
||||
@ -214,9 +194,8 @@ const ui = {
|
||||
|
||||
// Set <progress> value
|
||||
setProgress(target, input) {
|
||||
// Default to 0
|
||||
const value = !utils.is.undefined(input) ? input : 0;
|
||||
const progress = !utils.is.undefined(target) ? target : this.elements.display.buffer;
|
||||
const value = utils.is.number(input) ? input : 0;
|
||||
const progress = utils.is.htmlElement(target) ? target : this.elements.display.buffer;
|
||||
|
||||
// Update value and label
|
||||
if (utils.is.htmlElement(progress)) {
|
||||
@ -232,7 +211,7 @@ const ui = {
|
||||
|
||||
// Update <progress> elements
|
||||
updateProgress(event) {
|
||||
if (!this.supported.ui) {
|
||||
if (!this.supported.ui || !utils.is.event(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -280,41 +259,49 @@ const ui = {
|
||||
},
|
||||
|
||||
// Update the displayed time
|
||||
updateTimeDisplay(value, element) {
|
||||
// Bail if there's no duration display
|
||||
if (!utils.is.htmlElement(element)) {
|
||||
return null;
|
||||
updateTimeDisplay(target = null, time = 0, inverted = false) {
|
||||
// Bail if there's no element to display or the value isn't a number
|
||||
if (!utils.is.htmlElement(target) || !utils.is.number(time)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback to 0
|
||||
const time = !Number.isNaN(value) ? value : 0;
|
||||
// Format time component to add leading zero
|
||||
const format = value => `0${value}`.slice(-2);
|
||||
|
||||
let secs = parseInt(time % 60, 10);
|
||||
let mins = parseInt((time / 60) % 60, 10);
|
||||
const hours = parseInt((time / 60 / 60) % 60, 10);
|
||||
// Helpers
|
||||
const getHours = value => parseInt((value / 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?
|
||||
const displayHours = parseInt((this.duration / 60 / 60) % 60, 10) > 0;
|
||||
|
||||
// Ensure it's two digits. For example, 03 rather than 3.
|
||||
secs = `0${secs}`.slice(-2);
|
||||
mins = `0${mins}`.slice(-2);
|
||||
|
||||
// Generate display
|
||||
const display = `${(displayHours ? `${hours}:` : '') + mins}:${secs}`;
|
||||
if (getHours(this.duration) > 0) {
|
||||
hours = `${hours}:`;
|
||||
} else {
|
||||
hours = '';
|
||||
}
|
||||
|
||||
// Render
|
||||
// eslint-disable-next-line
|
||||
element.textContent = display;
|
||||
|
||||
// Return for looping
|
||||
return display;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
target.textContent = `${inverted ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;
|
||||
},
|
||||
|
||||
// Handle time change 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
|
||||
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
|
||||
if (event && event.type === 'timeupdate' && this.media.seeking) {
|
||||
@ -324,6 +311,26 @@ const ui = {
|
||||
// Playing progress
|
||||
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;
|
||||
|
@ -31,6 +31,9 @@ const utils = {
|
||||
htmlElement(input) {
|
||||
return !this.undefined(input) && input instanceof HTMLElement;
|
||||
},
|
||||
textNode(input) {
|
||||
return this.getConstructor(input) === Text;
|
||||
},
|
||||
event(input) {
|
||||
return !this.undefined(input) && input instanceof Event;
|
||||
},
|
||||
@ -49,8 +52,8 @@ const utils = {
|
||||
return (
|
||||
input === null ||
|
||||
typeof input === 'undefined' ||
|
||||
((this.string(input) || this.array(input) || this.nodeList(input)) && input.length === 0) ||
|
||||
(this.object(input) && Object.keys(input).length === 0)
|
||||
((this.string(input) || this.array(input) || this.nodeList(input)) && !input.length) ||
|
||||
(this.object(input) && !Object.keys(input).length)
|
||||
);
|
||||
},
|
||||
getConstructor(input) {
|
||||
@ -140,8 +143,12 @@ const utils = {
|
||||
|
||||
// Get the sprite
|
||||
fetch(url)
|
||||
.then(response => response.text())
|
||||
.then(response => (response.ok ? response.text() : null))
|
||||
.then(text => {
|
||||
if (text === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (support.storage) {
|
||||
window.localStorage.setItem(
|
||||
prefix + id,
|
||||
@ -152,7 +159,8 @@ const utils = {
|
||||
}
|
||||
|
||||
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
|
||||
createElement(type, attributes, text) {
|
||||
// Create a new <element>
|
||||
@ -236,12 +228,28 @@ const utils = {
|
||||
return element;
|
||||
},
|
||||
|
||||
// Inaert an element after another
|
||||
insertAfter(element, target) {
|
||||
target.parentNode.insertBefore(element, target.nextSibling);
|
||||
},
|
||||
|
||||
// Insert a DocumentFragment
|
||||
insertElement(type, parent, attributes, text) {
|
||||
// Inject the new <element>
|
||||
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
|
||||
emptyElement(element) {
|
||||
let { length } = element.childNodes;
|
||||
@ -433,9 +441,9 @@ const utils = {
|
||||
|
||||
// Trap focus inside container
|
||||
trapFocus() {
|
||||
const tabbables = utils.getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]');
|
||||
const first = tabbables[0];
|
||||
const last = tabbables[tabbables.length - 1];
|
||||
const focusable = utils.getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]');
|
||||
const first = focusable[0];
|
||||
const last = focusable[focusable.length - 1];
|
||||
|
||||
utils.on(
|
||||
this.elements.container,
|
||||
|
@ -12,6 +12,7 @@
|
||||
line-height: @plyr-line-height;
|
||||
direction: ltr;
|
||||
text-shadow: none;
|
||||
transition: box-shadow 0.3s ease;
|
||||
.plyr-font-smoothing(off);
|
||||
|
||||
// Media elements
|
||||
@ -22,6 +23,11 @@
|
||||
vertical-align: middle;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 3px fade(#000, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
// Full UI only
|
||||
|
@ -26,4 +26,5 @@
|
||||
|
||||
@import 'states/fullscreen';
|
||||
|
||||
@import 'utils/animation';
|
||||
@import 'utils/hidden';
|
||||
|
@ -3,11 +3,11 @@
|
||||
// --------------------------------------------------------------
|
||||
|
||||
.plyr__badge {
|
||||
padding: 0 4px;
|
||||
padding: 3px 4px;
|
||||
border-radius: 2px;
|
||||
background: @plyr-menu-color;
|
||||
color: @plyr-menu-bg;
|
||||
font-size: @plyr-font-size-tiny;
|
||||
line-height: 1.5;
|
||||
line-height: 1;
|
||||
.plyr-font-smoothing(on);
|
||||
}
|
||||
|
@ -57,9 +57,9 @@
|
||||
top: 50%;
|
||||
left: 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%);
|
||||
border: 3px solid currentColor;
|
||||
border: 0;
|
||||
border-radius: 100%;
|
||||
box-shadow: 0 1px 1px fade(#000, 15%);
|
||||
color: @plyr-video-control-color;
|
||||
@ -81,7 +81,12 @@
|
||||
}
|
||||
|
||||
&: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%);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -129,11 +129,45 @@
|
||||
}
|
||||
|
||||
label.plyr__control {
|
||||
padding-left: ceil(@plyr-control-padding * 2.5);
|
||||
padding-left: @plyr-control-padding;
|
||||
|
||||
input[type='radio'] {
|
||||
/*input[type='radio'] {
|
||||
position: relative;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,8 @@
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: transparent;
|
||||
transition: box-shadow 0.3s ease;
|
||||
border-radius: (@plyr-range-thumb-height * 2);
|
||||
|
||||
// Used in JS to populate lower fill for WebKit
|
||||
color: @plyr-range-selected-bg;
|
||||
@ -79,10 +81,6 @@
|
||||
border: 0;
|
||||
}
|
||||
|
||||
&.plyr__tab-focus {
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
// Pressed styles
|
||||
&:active {
|
||||
&::-webkit-slider-thumb {
|
||||
@ -114,7 +112,7 @@
|
||||
}
|
||||
|
||||
&.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 {
|
||||
outline: 1px dotted fade(@plyr-audio-control-color, 50%);
|
||||
box-shadow: 0 0 0 3px fade(@plyr-audio-control-color, 50%);
|
||||
}
|
||||
}
|
||||
|
7
src/less/utils/animation.less
Normal file
7
src/less/utils/animation.less
Normal file
@ -0,0 +1,7 @@
|
||||
// --------------------------------------------------------------
|
||||
// Animation utils
|
||||
// --------------------------------------------------------------
|
||||
|
||||
.plyr--no-transition {
|
||||
transition: none !important;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user