Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c442c9357 | |||
| e17e0a81dd | |||
| 2488299d7b | |||
| dfc09b8e04 | |||
| 6438baaddc | |||
| 8fc6c2ba52 | |||
| 95092edc93 | |||
| c4b3e0672e | |||
| e8e2b8ba39 | |||
| 2e40b91ec1 | |||
| 97d9228bed | |||
| 0f14865d56 | |||
| c94ab2a39f | |||
| ab89e055de | |||
| ac6e3dba5a | |||
| d9d2c4a219 | |||
| 1890a9378d | |||
| ac88e6e190 | |||
| 15cbae8a19 | |||
| aaf096d96d | |||
| 93f5acbffd | |||
| 9c717275d2 | |||
| 0249772f01 | |||
| 5d699d5f47 | |||
| 34d79a5443 | |||
| 1e761e237a | |||
| 36ad132c82 | |||
| c9055f391b | |||
| 99c95363b4 | |||
| c90526bf07 | |||
| 97a6e72e10 | |||
| f100caba81 | |||
| 80aa6ffe43 | |||
| 0694e58650 | |||
| e644eeb5b6 | |||
| 5ddd9e02de | |||
| a064f0b4fd | |||
| b9dbcc30fa | |||
| 7ab34c7d2e | |||
| 17caa3c57b | |||
| 2e6361898b | |||
| a1b2c0419f | |||
| 9b81e776fb | |||
| 7a4a7dece8 | |||
| f0d3e8c3b9 | |||
| ad68d9484f | |||
| c3fd822857 | |||
| e147f3a754 | |||
| b694e7d3ab | |||
| b2fff4c33f | |||
| 9c1060d9b0 | |||
| a91652287b | |||
| 2cf44c236d | |||
| 243db9eda3 | |||
| b675ba1f35 | |||
| e9367ee85e | |||
| d9b7928ce6 | |||
| cdacae6697 |
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"parser": "babel-eslint",
|
||||
"extends": ["airbnb-base", "prettier"],
|
||||
"plugins": ["simple-import-sort", "import"],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true
|
||||
},
|
||||
"globals": {
|
||||
"Plyr": false,
|
||||
"jQuery": false
|
||||
},
|
||||
"rules": {
|
||||
"import/no-cycle": "warn",
|
||||
"padding-line-between-statements": [
|
||||
"error",
|
||||
{
|
||||
"blankLine": "never",
|
||||
"prev": ["singleline-const", "singleline-let", "singleline-var"],
|
||||
"next": ["singleline-const", "singleline-let", "singleline-var"]
|
||||
}
|
||||
],
|
||||
"sort-imports": "off",
|
||||
"import/order": "off",
|
||||
"simple-import-sort/sort": "error"
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"parser": "babel-eslint",
|
||||
"extends": ["airbnb-base", "prettier"],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true
|
||||
},
|
||||
"globals": {
|
||||
"Plyr": false,
|
||||
"jQuery": false
|
||||
},
|
||||
"rules": {
|
||||
"import/no-cycle": 1,
|
||||
"no-const-assign": 1,
|
||||
"no-shadow": 0,
|
||||
"no-this-before-super": 1,
|
||||
"no-undef": 1,
|
||||
"no-unreachable": 1,
|
||||
"no-unused-vars": 1,
|
||||
"constructor-super": 1,
|
||||
"valid-typeof": 1,
|
||||
"indent": [2, 4, { "SwitchCase": 1 }],
|
||||
"quotes": [2, "single", "avoid-escape"],
|
||||
"semi": [2, "always"],
|
||||
"eqeqeq": [2, "always"],
|
||||
"one-var": [2, "never"],
|
||||
"comma-dangle": [2, "always-multiline"],
|
||||
"spaced-comment": [2, "always"],
|
||||
"no-restricted-globals": 2,
|
||||
"no-param-reassign": [2, { "props": false }]
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: sampotts
|
||||
patreon: plyr
|
||||
@@ -3,9 +3,7 @@ node_modules
|
||||
credentials.json
|
||||
*.mp4
|
||||
!dist/blank.mp4
|
||||
index-*.html
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
package-lock.json
|
||||
*.webm
|
||||
.idea/
|
||||
|
||||
+4
-2
@@ -10,13 +10,15 @@
|
||||
"src": "./src/js/plyr.polyfilled.js",
|
||||
"dist": "./dist/",
|
||||
"formats": ["es", "umd"],
|
||||
"namespace": "Plyr"
|
||||
"namespace": "Plyr",
|
||||
"polyfill": true
|
||||
},
|
||||
"demo.js": {
|
||||
"src": "./demo/src/js/demo.js",
|
||||
"dist": "./demo/dist/",
|
||||
"formats": ["iife"],
|
||||
"namespace": "Demo"
|
||||
"namespace": "Demo",
|
||||
"polyfill": true
|
||||
}
|
||||
},
|
||||
"css": {
|
||||
|
||||
@@ -1,3 +1,27 @@
|
||||
## v3.5.6
|
||||
|
||||
- Another Edge fix (thanks Nick Hawk via Slack)
|
||||
|
||||
## v3.5.5
|
||||
|
||||
- YouTube fix for when there are other embeds on the page (thanks @aFarkas)
|
||||
- Separated demo dependencies into their own package.json
|
||||
- Fix for Edge controls flexbox issue when resizing the player (thanks Nick Hawk via Slack)
|
||||
- More aspect ratio fixes
|
||||
|
||||
## v3.5.4
|
||||
|
||||
- Added: Set download URL via new setter
|
||||
- Improvement: The order of the `controls` option now effects the order in the DOM - i.e. you can re-order the controls - Note: this may break any custom CSS you have setup. Please see the changes in the PR to the default SASS
|
||||
- Fixed issue with empty controls and preview thumbs
|
||||
- Fixed issue with setGutter call (from Sentry)
|
||||
- Fixed issue with initial selected speed not working
|
||||
- Added notes on `autoplay` config option and browser compatibility
|
||||
- Fixed issue with ads volume not matching current content volume
|
||||
- Fixed race condition where ads were loading during source change
|
||||
- Improvement: Automatic aspect ratio for YouTube is now supported, meaning all aspect ratios are set based on media content - Note: we're now using a different API to get YouTube video metadata so you may need to adjust any CSPs you have setup
|
||||
- Fix for menu in the Shadow DOM (thanks @emielbeinema)
|
||||
|
||||
## v3.5.3
|
||||
|
||||
- Improved the usage of the `ratio` config option; it now works as expected and for all video types. The default has not changed, it is to dynamically, where possible (except YouTube where 16:9 is used) determine the ratio from the media source so this is not a breaking change.
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+13308
-1137
File diff suppressed because it is too large
Load Diff
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+28
-29
@@ -17,7 +17,7 @@
|
||||
<link rel="icon" type="image/png" href="https://cdn.plyr.io/static/icons/16x16.png" sizes="16x16" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="https://cdn.plyr.io/static/icons/180x180.png" />
|
||||
|
||||
<!-- Opengraph -->
|
||||
<!-- Open Graph -->
|
||||
<meta
|
||||
property="og:title"
|
||||
content="Plyr - A simple, customizable HTML5 Video, Audio, YouTube and Vimeo player"
|
||||
@@ -50,12 +50,23 @@
|
||||
type="font/woff2"
|
||||
href="https://cdn.plyr.io/static/fonts/gordita-bold.woff2"
|
||||
/>
|
||||
|
||||
<!-- Google Analytics-->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-132699580-1"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag() {
|
||||
dataLayer.push(arguments);
|
||||
}
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'UA-132699580-1');
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="grid">
|
||||
<header>
|
||||
<h1>Plyr</h1>
|
||||
<h1>Pl<span>a</span>y<span>e</span>r</h1>
|
||||
<p>
|
||||
A simple, accessible and customisable media player for
|
||||
<button type="button" class="faux-link" data-source="video">
|
||||
@@ -106,22 +117,20 @@
|
||||
</p>
|
||||
|
||||
<div class="call-to-action">
|
||||
<span class="button--with-count">
|
||||
<a href="https://github.com/sampotts/plyr" target="_blank" class="button js-shr-button">
|
||||
<svg class="icon" role="presentation">
|
||||
<title>GitHub</title>
|
||||
<path
|
||||
d="M8,0.2c-4.4,0-8,3.6-8,8c0,3.5,2.3,6.5,5.5,7.6
|
||||
C5.9,15.9,6,15.6,6,15.4c0-0.2,0-0.7,0-1.4C3.8,14.5,3.3,13,3.3,13c-0.4-0.9-0.9-1.2-0.9-1.2c-0.7-0.5,0.1-0.5,0.1-0.5
|
||||
c0.8,0.1,1.2,0.8,1.2,0.8C4.4,13.4,5.6,13,6,12.8c0.1-0.5,0.3-0.9,0.5-1.1c-1.8-0.2-3.6-0.9-3.6-4c0-0.9,0.3-1.6,0.8-2.1
|
||||
c-0.1-0.2-0.4-1,0.1-2.1c0,0,0.7-0.2,2.2,0.8c0.6-0.2,1.3-0.3,2-0.3c0.7,0,1.4,0.1,2,0.3c1.5-1,2.2-0.8,2.2-0.8
|
||||
c0.4,1.1,0.2,1.9,0.1,2.1c0.5,0.6,0.8,1.3,0.8,2.1c0,3.1-1.9,3.7-3.7,3.9C9.7,12,10,12.5,10,13.2c0,1.1,0,1.9,0,2.2
|
||||
c0,0.2,0.1,0.5,0.6,0.4c3.2-1.1,5.5-4.1,5.5-7.6C16,3.8,12.4,0.2,8,0.2z"
|
||||
></path>
|
||||
</svg>
|
||||
Download on GitHub
|
||||
</a>
|
||||
</span>
|
||||
<a href="https://github.com/sampotts/plyr" target="_blank" class="button js-shr">
|
||||
<svg class="icon" role="presentation">
|
||||
<title>GitHub</title>
|
||||
<path
|
||||
d="M8,0.2c-4.4,0-8,3.6-8,8c0,3.5,2.3,6.5,5.5,7.6
|
||||
C5.9,15.9,6,15.6,6,15.4c0-0.2,0-0.7,0-1.4C3.8,14.5,3.3,13,3.3,13c-0.4-0.9-0.9-1.2-0.9-1.2c-0.7-0.5,0.1-0.5,0.1-0.5
|
||||
c0.8,0.1,1.2,0.8,1.2,0.8C4.4,13.4,5.6,13,6,12.8c0.1-0.5,0.3-0.9,0.5-1.1c-1.8-0.2-3.6-0.9-3.6-4c0-0.9,0.3-1.6,0.8-2.1
|
||||
c-0.1-0.2-0.4-1,0.1-2.1c0,0,0.7-0.2,2.2,0.8c0.6-0.2,1.3-0.3,2-0.3c0.7,0,1.4,0.1,2,0.3c1.5-1,2.2-0.8,2.2-0.8
|
||||
c0.4,1.1,0.2,1.9,0.1,2.1c0.5,0.6,0.8,1.3,0.8,2.1c0,3.1-1.9,3.7-3.7,3.9C9.7,12,10,12.5,10,13.2c0,1.1,0,1.9,0,2.2
|
||||
c0,0.2,0.1,0.5,0.6,0.4c3.2-1.1,5.5-4.1,5.5-7.6C16,3.8,12.4,0.2,8,0.2z"
|
||||
></path>
|
||||
</svg>
|
||||
Download on GitHub
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -256,23 +265,13 @@
|
||||
<a
|
||||
href="https://twitter.com/intent/tweet?text=A+simple+HTML5+media+player+with+custom+controls+and+WebVTT+captions.&url=http%3A%2F%2Fplyr.io&via=Sam_Potts"
|
||||
target="_blank"
|
||||
class="js-shr-button"
|
||||
class="js-shr"
|
||||
>tweet it</a
|
||||
>
|
||||
👍
|
||||
</p>
|
||||
</aside>
|
||||
|
||||
<!-- Polyfills -->
|
||||
<script
|
||||
src="https://cdn.polyfill.io/v2/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent,Object.entries,Object.values,URL,Math.trunc"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
|
||||
<!-- Sharing libary (https://shr.one) -->
|
||||
<script src="https://cdn.shr.one/2.0.0-beta.2/shr.js" crossorigin="anonymous"></script>
|
||||
|
||||
<!-- Docs script -->
|
||||
<script src="dist/demo.js" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "plyr-demo",
|
||||
"version": "1.0.0",
|
||||
"description": "Demo for Plyr",
|
||||
"homepage": "https://plyr.io",
|
||||
"author": "Sam Potts <sam@potts.es>",
|
||||
"dependencies": {
|
||||
"core-js": "^3.1.4",
|
||||
"custom-event-polyfill": "^1.0.7",
|
||||
"raven-js": "^3.27.2",
|
||||
"shr-buttons": "2.0.3",
|
||||
"url-polyfill": "^1.1.5"
|
||||
}
|
||||
}
|
||||
+63
-191
@@ -4,8 +4,16 @@
|
||||
// Please see readme.md in the root or github.com/sampotts/plyr
|
||||
// ==========================================================================
|
||||
|
||||
import './tab-focus';
|
||||
import 'custom-event-polyfill';
|
||||
import 'url-polyfill';
|
||||
|
||||
import Raven from 'raven-js';
|
||||
import Shr from 'shr-buttons';
|
||||
|
||||
import Plyr from '../../../src/js/plyr';
|
||||
import sources from './sources';
|
||||
import toggleClass from './toggle-class';
|
||||
|
||||
(() => {
|
||||
const { host } = window.location;
|
||||
@@ -17,45 +25,15 @@ import Plyr from '../../../src/js/plyr';
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
Raven.context(() => {
|
||||
const selector = '#player';
|
||||
const container = document.getElementById('container');
|
||||
|
||||
if (window.Shr) {
|
||||
window.Shr.setup('.js-shr-button', {
|
||||
count: {
|
||||
classname: 'button__count',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Setup tab focus
|
||||
const tabClassName = 'tab-focus';
|
||||
|
||||
// Remove class on blur
|
||||
document.addEventListener('focusout', event => {
|
||||
if (!event.target.classList || container.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.target.classList.remove(tabClassName);
|
||||
});
|
||||
|
||||
// Add classname to tabbed elements
|
||||
document.addEventListener('keydown', event => {
|
||||
if (event.keyCode !== 9) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delay the adding of classname until the focus has changed
|
||||
// This event fires before the focusin event
|
||||
setTimeout(() => {
|
||||
const focused = document.activeElement;
|
||||
|
||||
if (!focused || !focused.classList || container.contains(focused)) {
|
||||
return;
|
||||
}
|
||||
|
||||
focused.classList.add(tabClassName);
|
||||
}, 10);
|
||||
// Setup share buttons
|
||||
Shr.setup('.js-shr', {
|
||||
count: {
|
||||
className: 'button__count',
|
||||
},
|
||||
wrapper: {
|
||||
className: 'button--with-count',
|
||||
},
|
||||
});
|
||||
|
||||
// Setup the player
|
||||
@@ -93,131 +71,12 @@ import Plyr from '../../../src/js/plyr';
|
||||
|
||||
// Setup type toggle
|
||||
const buttons = document.querySelectorAll('[data-source]');
|
||||
const types = {
|
||||
video: 'video',
|
||||
audio: 'audio',
|
||||
youtube: 'youtube',
|
||||
vimeo: 'vimeo',
|
||||
};
|
||||
let currentType = window.location.hash.replace('#', '');
|
||||
const historySupport = window.history && window.history.pushState;
|
||||
|
||||
// Toggle class on an element
|
||||
function toggleClass(element, className, state) {
|
||||
if (element) {
|
||||
element.classList[state ? 'add' : 'remove'](className);
|
||||
}
|
||||
}
|
||||
|
||||
// Set a new source
|
||||
function newSource(type, init) {
|
||||
// Bail if new type isn't known, it's the current type, or current type is empty (video is default) and new type is video
|
||||
if (
|
||||
!(type in types) ||
|
||||
(!init && type === currentType) ||
|
||||
(!currentType.length && type === types.video)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case types.video:
|
||||
player.source = {
|
||||
type: 'video',
|
||||
title: 'View From A Blue Moon',
|
||||
sources: [
|
||||
{
|
||||
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4',
|
||||
type: 'video/mp4',
|
||||
size: 576,
|
||||
},
|
||||
{
|
||||
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4',
|
||||
type: 'video/mp4',
|
||||
size: 720,
|
||||
},
|
||||
{
|
||||
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4',
|
||||
type: 'video/mp4',
|
||||
size: 1080,
|
||||
},
|
||||
{
|
||||
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1440p.mp4',
|
||||
type: 'video/mp4',
|
||||
size: 1440,
|
||||
},
|
||||
],
|
||||
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: true,
|
||||
},
|
||||
{
|
||||
kind: 'captions',
|
||||
label: 'French',
|
||||
srclang: 'fr',
|
||||
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
break;
|
||||
|
||||
case types.audio:
|
||||
player.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 types.youtube:
|
||||
player.source = {
|
||||
type: 'video',
|
||||
sources: [
|
||||
{
|
||||
src: 'https://youtube.com/watch?v=bTqVqk7FSmY',
|
||||
provider: 'youtube',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
break;
|
||||
|
||||
case types.vimeo:
|
||||
player.source = {
|
||||
type: 'video',
|
||||
sources: [
|
||||
{
|
||||
src: 'https://vimeo.com/76979871',
|
||||
provider: 'vimeo',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Set the current type for next time
|
||||
currentType = type;
|
||||
const types = Object.keys(sources);
|
||||
const historySupport = Boolean(window.history && window.history.pushState);
|
||||
let currentType = window.location.hash.substring(1);
|
||||
const hasCurrentType = !currentType.length;
|
||||
|
||||
function render(type) {
|
||||
// Remove active classes
|
||||
Array.from(buttons).forEach(button => toggleClass(button.parentElement, 'active', false));
|
||||
|
||||
@@ -226,9 +85,31 @@ import Plyr from '../../../src/js/plyr';
|
||||
|
||||
// Show cite
|
||||
Array.from(document.querySelectorAll('.plyr__cite')).forEach(cite => {
|
||||
cite.setAttribute('hidden', '');
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
cite.hidden = true;
|
||||
});
|
||||
document.querySelector(`.plyr__cite--${type}`).removeAttribute('hidden');
|
||||
|
||||
document.querySelector(`.plyr__cite--${type}`).hidden = false;
|
||||
}
|
||||
|
||||
// Set a new source
|
||||
function setSource(type, init) {
|
||||
// Bail if new type isn't known, it's the current type, or current type is empty (video is default) and new type is video
|
||||
if (
|
||||
!types.includes(type) ||
|
||||
(!init && type === currentType) ||
|
||||
(!currentType.length && type === 'video')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the new source
|
||||
player.source = sources[type];
|
||||
|
||||
// Set the current type for next time
|
||||
currentType = type;
|
||||
|
||||
render(type);
|
||||
}
|
||||
|
||||
// Bind to each button
|
||||
@@ -236,7 +117,7 @@ import Plyr from '../../../src/js/plyr';
|
||||
button.addEventListener('click', () => {
|
||||
const type = button.getAttribute('data-source');
|
||||
|
||||
newSource(type);
|
||||
setSource(type);
|
||||
|
||||
if (historySupport) {
|
||||
window.history.pushState({ type }, '', `#${type}`);
|
||||
@@ -246,36 +127,27 @@ import Plyr from '../../../src/js/plyr';
|
||||
|
||||
// List for backwards/forwards
|
||||
window.addEventListener('popstate', event => {
|
||||
if (event.state && 'type' in event.state) {
|
||||
newSource(event.state.type);
|
||||
if (event.state && Object.keys(event.state).includes('type')) {
|
||||
setSource(event.state.type);
|
||||
}
|
||||
});
|
||||
|
||||
// On load
|
||||
if (historySupport) {
|
||||
const video = !currentType.length;
|
||||
|
||||
// If there's no current type set, assume video
|
||||
if (video) {
|
||||
currentType = types.video;
|
||||
}
|
||||
|
||||
// Replace current history state
|
||||
if (currentType in types) {
|
||||
window.history.replaceState(
|
||||
{
|
||||
type: currentType,
|
||||
},
|
||||
'',
|
||||
video ? '' : `#${currentType}`,
|
||||
);
|
||||
}
|
||||
|
||||
// If it's not video, load the source
|
||||
if (currentType !== types.video) {
|
||||
newSource(currentType, true);
|
||||
}
|
||||
// If there's no current type set, assume video
|
||||
if (hasCurrentType) {
|
||||
currentType = 'video';
|
||||
}
|
||||
|
||||
// Replace current history state
|
||||
if (historySupport && types.includes(currentType)) {
|
||||
window.history.replaceState({ type: currentType }, '', hasCurrentType ? '' : `#${currentType}`);
|
||||
}
|
||||
|
||||
// If it's not video, load the source
|
||||
if (currentType !== 'video') {
|
||||
setSource(currentType, true);
|
||||
}
|
||||
|
||||
render(currentType);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
const sources = {
|
||||
video: {
|
||||
type: 'video',
|
||||
title: 'View From A Blue Moon',
|
||||
sources: [
|
||||
{
|
||||
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4',
|
||||
type: 'video/mp4',
|
||||
size: 576,
|
||||
},
|
||||
{
|
||||
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4',
|
||||
type: 'video/mp4',
|
||||
size: 720,
|
||||
},
|
||||
{
|
||||
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4',
|
||||
type: 'video/mp4',
|
||||
size: 1080,
|
||||
},
|
||||
{
|
||||
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1440p.mp4',
|
||||
type: 'video/mp4',
|
||||
size: 1440,
|
||||
},
|
||||
],
|
||||
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: true,
|
||||
},
|
||||
{
|
||||
kind: 'captions',
|
||||
label: 'French',
|
||||
srclang: 'fr',
|
||||
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt',
|
||||
},
|
||||
],
|
||||
},
|
||||
audio: {
|
||||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
youtube: {
|
||||
type: 'video',
|
||||
sources: [
|
||||
{
|
||||
src: 'https://youtube.com/watch?v=bTqVqk7FSmY',
|
||||
provider: 'youtube',
|
||||
},
|
||||
],
|
||||
},
|
||||
vimeo: {
|
||||
type: 'video',
|
||||
sources: [
|
||||
{
|
||||
src: 'https://vimeo.com/76979871',
|
||||
provider: 'vimeo',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default sources;
|
||||
@@ -0,0 +1,31 @@
|
||||
// Setup tab focus
|
||||
const container = document.getElementById('container');
|
||||
const tabClassName = 'tab-focus';
|
||||
|
||||
// Remove class on blur
|
||||
document.addEventListener('focusout', event => {
|
||||
if (!event.target.classList || container.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.target.classList.remove(tabClassName);
|
||||
});
|
||||
|
||||
// Add classname to tabbed elements
|
||||
document.addEventListener('keydown', event => {
|
||||
if (event.keyCode !== 9) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delay the adding of classname until the focus has changed
|
||||
// This event fires before the focusin event
|
||||
setTimeout(() => {
|
||||
const focused = document.activeElement;
|
||||
|
||||
if (!focused || !focused.classList || container.contains(focused)) {
|
||||
return;
|
||||
}
|
||||
|
||||
focused.classList.add(tabClassName);
|
||||
}, 10);
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
// Toggle class on an element
|
||||
const toggleClass = (element, className = '', toggle = false) =>
|
||||
element && element.classList[toggle ? 'add' : 'remove'](className);
|
||||
|
||||
export default toggleClass;
|
||||
@@ -6,11 +6,9 @@
|
||||
.button,
|
||||
.button__count {
|
||||
align-items: center;
|
||||
background: $color-button-background;
|
||||
border: 0;
|
||||
border-radius: $border-radius-base;
|
||||
box-shadow: 0 1px 1px rgba(#000, 0.1);
|
||||
color: $color-button-text;
|
||||
display: inline-flex;
|
||||
padding: ($spacing-base * 0.75);
|
||||
position: relative;
|
||||
@@ -21,14 +19,16 @@
|
||||
|
||||
// Buttons
|
||||
.button {
|
||||
background: $color-button-background;
|
||||
color: $color-button-text;
|
||||
font-weight: $font-weight-bold;
|
||||
padding-left: $spacing-base;
|
||||
padding-right: $spacing-base;
|
||||
padding-left: ($spacing-base * 1.25);
|
||||
padding-right: ($spacing-base * 1.25);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: $gray-dark;
|
||||
background: $color-button-background-hover;
|
||||
|
||||
// Remove the underline/border
|
||||
&::after {
|
||||
@@ -38,7 +38,6 @@
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 2px 2px rgba(#000, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
@@ -50,7 +49,7 @@
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
top: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,12 +65,14 @@
|
||||
// Count bubble
|
||||
.button__count {
|
||||
animation: fadein 0.2s ease;
|
||||
margin-left: ($spacing-base / 2);
|
||||
background: $color-button-count-background;
|
||||
color: $color-button-count-text;
|
||||
margin-left: ($spacing-base * 0.75);
|
||||
|
||||
&::before {
|
||||
border: $arrow-size solid transparent;
|
||||
border-left-width: 0;
|
||||
border-right-color: $color-button-background;
|
||||
border-right-color: $color-button-count-background;
|
||||
content: '';
|
||||
height: 0;
|
||||
position: absolute;
|
||||
|
||||
@@ -6,6 +6,13 @@ header {
|
||||
padding-bottom: $spacing-base;
|
||||
text-align: center;
|
||||
|
||||
h1 span {
|
||||
animation: shrinkHide 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) 2s forwards;
|
||||
display: inline-block;
|
||||
font-weight: $font-weight-light;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.call-to-action {
|
||||
margin-top: ($spacing-base * 1.5);
|
||||
}
|
||||
@@ -15,5 +22,9 @@ header {
|
||||
max-width: 360px;
|
||||
padding-bottom: ($spacing-base * 2);
|
||||
text-align: left;
|
||||
|
||||
p:first-of-type {
|
||||
@include font-size($font-size-base + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,5 +19,5 @@ label svg {
|
||||
|
||||
a .icon,
|
||||
.btn .icon {
|
||||
margin-right: floor($spacing-base / 3);
|
||||
margin-right: ($spacing-base / 2);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ button.faux-link {
|
||||
a {
|
||||
border-bottom: 1px dotted currentColor;
|
||||
color: $color-link;
|
||||
font-weight: $font-weight-bold;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
@@ -2,16 +2,10 @@
|
||||
// Examples
|
||||
// ==========================================================================
|
||||
|
||||
// For non supported browsers
|
||||
video {
|
||||
max-width: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
// Example players
|
||||
.plyr {
|
||||
border-radius: $border-radius-base;
|
||||
box-shadow: 0 2px 5px rgba(#000, 0.2);
|
||||
box-shadow: 0 2px 15px rgba(#000, 0.1);
|
||||
margin: $spacing-base auto;
|
||||
|
||||
&.plyr--audio {
|
||||
@@ -34,17 +28,9 @@ video {
|
||||
|
||||
// Style full supported player
|
||||
.plyr__cite {
|
||||
display: none;
|
||||
margin-top: $spacing-base;
|
||||
color: $color-gray-5;
|
||||
|
||||
.icon {
|
||||
margin-right: ceil($spacing-base / 6);
|
||||
}
|
||||
}
|
||||
|
||||
.plyr--video:not(.plyr--youtube):not(.plyr--vimeo) ~ ul .plyr__cite--video,
|
||||
.plyr--audio ~ ul .plyr__cite--audio,
|
||||
.plyr--youtube ~ ul .plyr__cite--youtube,
|
||||
.plyr--vimeo ~ ul .plyr__cite--vimeo {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -35,11 +35,10 @@ main {
|
||||
aside {
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
color: $gray;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
justify-content: center;
|
||||
padding: ($spacing-base * 0.75);
|
||||
padding: $spacing-base;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
text-shadow: none;
|
||||
|
||||
@@ -11,3 +11,17 @@
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shrinkHide {
|
||||
0% {
|
||||
opacity: 0.5;
|
||||
width: 38px;
|
||||
}
|
||||
20% {
|
||||
width: 45px;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
@return #{$rem}rem;
|
||||
}
|
||||
|
||||
@mixin font-size($size: 16) {
|
||||
@mixin font-size($size: $font-size-base) {
|
||||
font-size: $size * 1px; // Fallback in px
|
||||
font-size: calculate-rem($size);
|
||||
}
|
||||
|
||||
@@ -2,31 +2,41 @@
|
||||
// Colors
|
||||
// ==========================================================================
|
||||
|
||||
// Greyscale
|
||||
$gray-dark: #343f4a;
|
||||
$gray: #55646b;
|
||||
$gray-light: #cbd0d3;
|
||||
$gray-lighter: #dbe3e8;
|
||||
$off-white: #f2f5f7;
|
||||
// Grayscale
|
||||
$color-gray-9: hsl(210, 15%, 16%);
|
||||
$color-gray-8: lighten($color-gray-9, 9%);
|
||||
$color-gray-7: lighten($color-gray-8, 9%);
|
||||
$color-gray-6: lighten($color-gray-7, 9%);
|
||||
$color-gray-5: lighten($color-gray-6, 9%);
|
||||
$color-gray-4: lighten($color-gray-5, 9%);
|
||||
$color-gray-3: lighten($color-gray-4, 9%);
|
||||
$color-gray-2: lighten($color-gray-3, 9%);
|
||||
$color-gray-1: lighten($color-gray-2, 9%);
|
||||
$color-gray-0: lighten($color-gray-1, 9%);
|
||||
|
||||
// Branding
|
||||
$color-brand-primary: hsl(198, 100%, 50%);
|
||||
|
||||
// Text
|
||||
$color-text: #fff;
|
||||
|
||||
// Plyr
|
||||
$color-brand-primary: #1aafff;
|
||||
$color-text: $color-gray-7;
|
||||
$color-headings: $color-brand-primary;
|
||||
|
||||
// Brands
|
||||
$color-twitter: #4baaf4;
|
||||
$color-youtube: #cc181e;
|
||||
$color-vimeo: #19b7ed;
|
||||
|
||||
// Elements
|
||||
$color-link: #fff;
|
||||
$color-background: $color-brand-primary;
|
||||
$color-link: $color-brand-primary;
|
||||
|
||||
// Background
|
||||
$color-background-from: hsl(198, 100%, 94%);
|
||||
$color-background-to: hsl(198, 100%, 98%);
|
||||
|
||||
// Buttons
|
||||
$color-button-background: #fff;
|
||||
$color-button-text: $gray;
|
||||
$color-button-background: $color-brand-primary;
|
||||
$color-button-text: #fff;
|
||||
$color-button-background-hover: hsl(198, 100%, 55%);
|
||||
$color-button-count-background: #fff;
|
||||
$color-button-count-text: $color-gray-6;
|
||||
|
||||
// Focus
|
||||
$tab-focus-default-color: #fff;
|
||||
|
||||
@@ -9,4 +9,4 @@ $arrow-size: 5px;
|
||||
$border-radius-base: 4px;
|
||||
|
||||
// Background
|
||||
$page-background: linear-gradient(to left top, lighten($color-background, 10%), darken($color-background, 20%));
|
||||
$page-background: linear-gradient(to left top, $color-background-from, $color-background-to);
|
||||
|
||||
@@ -14,6 +14,9 @@ $plyr-font-size-badges: 9px;
|
||||
// Other
|
||||
$plyr-font-smoothing: true;
|
||||
|
||||
// Colors
|
||||
$plyr-color-main: $color-brand-primary;
|
||||
|
||||
// Captions
|
||||
$plyr-font-size-captions-base: $plyr-font-size-base;
|
||||
$plyr-font-size-captions-small: $plyr-font-size-small;
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
// Colors
|
||||
// ==========================================================================
|
||||
|
||||
$spacing-base: 20px;
|
||||
$spacing-base: 16px;
|
||||
|
||||
@@ -14,7 +14,6 @@ body {
|
||||
font-family: $font-sans-serif;
|
||||
font-weight: $font-weight-medium;
|
||||
line-height: $line-height-base;
|
||||
text-shadow: 0 1px 1px rgba(#000, 0.15);
|
||||
}
|
||||
|
||||
button,
|
||||
@@ -26,7 +25,7 @@ textarea {
|
||||
|
||||
p,
|
||||
small {
|
||||
margin: 0 0 $spacing-base;
|
||||
margin: 0 0 ($spacing-base * 1.5);
|
||||
}
|
||||
|
||||
small {
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
|
||||
h1 {
|
||||
@include font-size($font-size-h1);
|
||||
color: $color-headings;
|
||||
font-weight: $font-weight-bold;
|
||||
letter-spacing: $letter-spacing-headings;
|
||||
line-height: 1.2;
|
||||
margin: 0 0 $spacing-base;
|
||||
margin: 0 0 ($spacing-base * 1.5);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
core-js@^3.1.4:
|
||||
version "3.1.4"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.1.4.tgz#3a2837fc48e582e1ae25907afcd6cf03b0cc7a07"
|
||||
integrity sha512-YNZN8lt82XIMLnLirj9MhKDFZHalwzzrL9YLt6eb0T5D0EDl4IQ90IGkua8mHbnxNrkj1d8hbdizMc0Qmg1WnQ==
|
||||
|
||||
custom-event-polyfill@^1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz#9bc993ddda937c1a30ccd335614c6c58c4f87aee"
|
||||
integrity sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==
|
||||
|
||||
raven-js@^3.27.2:
|
||||
version "3.27.2"
|
||||
resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.27.2.tgz#6c33df952026cd73820aa999122b7b7737a66775"
|
||||
integrity sha512-mFWQcXnhRFEQe5HeFroPaEghlnqy7F5E2J3Fsab189ondqUzcjwSVi7el7F36cr6PvQYXoZ1P2F5CSF2/azeMQ==
|
||||
|
||||
shr-buttons@2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/shr-buttons/-/shr-buttons-2.0.3.tgz#2ffd021fc3d789e1510ce2736b938bd09ea1da5a"
|
||||
integrity sha512-sPAgHiw4uaIt9TnxTfyZEedDChcldSVtnBHE44cpe/mSC7rqm4IEKZRLYqnVlTcGM+FSDNBPUNpSf50Q2ntd+w==
|
||||
|
||||
url-polyfill@^1.1.5:
|
||||
version "1.1.5"
|
||||
resolved "https://registry.yarnpkg.com/url-polyfill/-/url-polyfill-1.1.5.tgz#bec79b72b5407dba6d8cced2e32e4ab273aa9fb1"
|
||||
integrity sha512-9XjIJ6nwrU+nGd8t90Ze0Zs7t8A+SU0gqsqPttj6j3zAVe5q0HFcuv37nDBdVSPpi4aTHTfbUF/i+ZVD+o2EbA==
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+705
-592
File diff suppressed because it is too large
Load Diff
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+705
-592
File diff suppressed because it is too large
Load Diff
Vendored
+1963
-1759
File diff suppressed because it is too large
Load Diff
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1963
-1759
File diff suppressed because it is too large
Load Diff
+30
-19
@@ -6,24 +6,28 @@
|
||||
|
||||
const path = require('path');
|
||||
const gulp = require('gulp');
|
||||
|
||||
// ------------------------------------
|
||||
// JavaScript
|
||||
// ------------------------------------
|
||||
const terser = require('gulp-terser');
|
||||
const rollup = require('gulp-better-rollup');
|
||||
const babel = require('rollup-plugin-babel');
|
||||
const commonjs = require('rollup-plugin-commonjs');
|
||||
const resolve = require('rollup-plugin-node-resolve');
|
||||
|
||||
// ------------------------------------
|
||||
// CSS
|
||||
// ------------------------------------
|
||||
const sass = require('gulp-sass');
|
||||
const clean = require('gulp-clean-css');
|
||||
const prefix = require('gulp-autoprefixer');
|
||||
|
||||
// ------------------------------------
|
||||
// Images
|
||||
// ------------------------------------
|
||||
const svgstore = require('gulp-svgstore');
|
||||
const imagemin = require('gulp-imagemin');
|
||||
|
||||
// ------------------------------------
|
||||
// Utils
|
||||
// ------------------------------------
|
||||
const del = require('del');
|
||||
const filter = require('gulp-filter');
|
||||
const header = require('gulp-header');
|
||||
@@ -37,18 +41,22 @@ const plumber = require('gulp-plumber');
|
||||
const size = require('gulp-size');
|
||||
const sourcemaps = require('gulp-sourcemaps');
|
||||
const through = require('through2');
|
||||
|
||||
// ------------------------------------
|
||||
// Deployment
|
||||
// ------------------------------------
|
||||
const aws = require('aws-sdk');
|
||||
const publish = require('gulp-awspublish');
|
||||
const FastlyPurge = require('fastly-purge');
|
||||
|
||||
// ------------------------------------
|
||||
// Configs
|
||||
// ------------------------------------
|
||||
const pkg = require('./package.json');
|
||||
const build = require('./build.json');
|
||||
const deploy = require('./deploy.json');
|
||||
|
||||
// ------------------------------------
|
||||
// Info from package
|
||||
// ------------------------------------
|
||||
const { browserslist: browsers, version } = pkg;
|
||||
|
||||
const minSuffix = '.min';
|
||||
|
||||
// Get AWS config
|
||||
@@ -125,15 +133,16 @@ gulp.task(tasks.clean, done => {
|
||||
|
||||
// JavaScript
|
||||
Object.entries(build.js).forEach(([filename, entry]) => {
|
||||
entry.formats.forEach(format => {
|
||||
const { dist, formats, namespace, polyfill, src } = entry;
|
||||
|
||||
formats.forEach(format => {
|
||||
const name = `js:${filename}:${format}`;
|
||||
tasks.js.push(name);
|
||||
const polyfill = filename.includes('polyfilled');
|
||||
const extension = format === 'es' ? 'mjs' : 'js';
|
||||
tasks.js.push(name);
|
||||
|
||||
gulp.task(name, () =>
|
||||
gulp
|
||||
.src(entry.src)
|
||||
.src(src)
|
||||
.pipe(plumber())
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(
|
||||
@@ -159,7 +168,7 @@ Object.entries(build.js).forEach(([filename, entry]) => {
|
||||
],
|
||||
},
|
||||
{
|
||||
name: entry.namespace,
|
||||
name: namespace,
|
||||
format,
|
||||
},
|
||||
),
|
||||
@@ -170,25 +179,26 @@ Object.entries(build.js).forEach(([filename, entry]) => {
|
||||
extname: `.${extension}`,
|
||||
}),
|
||||
)
|
||||
.pipe(gulp.dest(entry.dist))
|
||||
.pipe(gulp.dest(dist))
|
||||
.pipe(filter(`**/*.${extension}`))
|
||||
.pipe(terser())
|
||||
.pipe(rename({ suffix: minSuffix }))
|
||||
.pipe(size(sizeOptions))
|
||||
.pipe(sourcemaps.write(''))
|
||||
.pipe(gulp.dest(entry.dist)),
|
||||
.pipe(gulp.dest(dist)),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// CSS
|
||||
Object.entries(build.css).forEach(([filename, entry]) => {
|
||||
const { dist, src } = entry;
|
||||
const name = `css:${filename}`;
|
||||
tasks.css.push(name);
|
||||
|
||||
gulp.task(name, () =>
|
||||
gulp
|
||||
.src(entry.src)
|
||||
.src(src)
|
||||
.pipe(plumber())
|
||||
.pipe(sass())
|
||||
.pipe(
|
||||
@@ -198,24 +208,25 @@ Object.entries(build.css).forEach(([filename, entry]) => {
|
||||
)
|
||||
.pipe(clean())
|
||||
.pipe(size(sizeOptions))
|
||||
.pipe(gulp.dest(entry.dist)),
|
||||
.pipe(gulp.dest(dist)),
|
||||
);
|
||||
});
|
||||
|
||||
// SVG Sprites
|
||||
Object.entries(build.sprite).forEach(([filename, entry]) => {
|
||||
const { dist, src } = entry;
|
||||
const name = `sprite:${filename}`;
|
||||
tasks.sprite.push(name);
|
||||
|
||||
gulp.task(name, () =>
|
||||
gulp
|
||||
.src(entry.src)
|
||||
.src(src)
|
||||
.pipe(plumber())
|
||||
.pipe(imagemin())
|
||||
.pipe(svgstore())
|
||||
.pipe(rename({ basename: path.parse(filename).name }))
|
||||
.pipe(size(sizeOptions))
|
||||
.pipe(gulp.dest(entry.dist)),
|
||||
.pipe(gulp.dest(dist)),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
+30
-29
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "plyr",
|
||||
"version": "3.5.3",
|
||||
"version": "3.5.6",
|
||||
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
|
||||
"homepage": "https://plyr.io",
|
||||
"author": "Sam Potts <sam@potts.es>",
|
||||
@@ -31,31 +31,33 @@
|
||||
"scripts": {
|
||||
"build": "gulp build",
|
||||
"lint": "eslint src/js && npm run-script remark",
|
||||
"lint:fix": "eslint --fix src/js",
|
||||
"remark": "remark -f --use 'validate-links=repository:\"sampotts/plyr\"' '{,!(node_modules),.?**/}*.md'",
|
||||
"deploy": "yarn lint && gulp deploy"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ansi-colors": "^3.2.4",
|
||||
"aws-sdk": "^2.437.0",
|
||||
"@babel/core": "^7.4.3",
|
||||
"@babel/preset-env": "^7.4.3",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"del": "^4.1.0",
|
||||
"ansi-colors": "^4.0.1",
|
||||
"aws-sdk": "^2.478.0",
|
||||
"@babel/core": "^7.4.5",
|
||||
"@babel/preset-env": "^7.4.5",
|
||||
"babel-eslint": "^10.0.2",
|
||||
"del": "^4.1.1",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-config-airbnb-base": "^13.1.0",
|
||||
"eslint-config-prettier": "^4.1.0",
|
||||
"eslint-plugin-import": "^2.16.0",
|
||||
"eslint-config-prettier": "^5.0.0",
|
||||
"eslint-plugin-import": "^2.17.3",
|
||||
"eslint-plugin-simple-import-sort": "^4.0.0",
|
||||
"fancy-log": "^1.3.3",
|
||||
"fastly-purge": "^1.0.1",
|
||||
"git-branch": "^2.0.1",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-autoprefixer": "^6.0.0",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-autoprefixer": "^6.1.0",
|
||||
"gulp-awspublish": "^4.0.0",
|
||||
"gulp-better-rollup": "^4.0.1",
|
||||
"gulp-clean-css": "^4.0.0",
|
||||
"gulp-filter": "^5.1.0",
|
||||
"gulp-clean-css": "^4.2.0",
|
||||
"gulp-filter": "^6.0.0",
|
||||
"gulp-header": "^2.0.7",
|
||||
"gulp-imagemin": "^5.0.3",
|
||||
"gulp-imagemin": "^6.0.0",
|
||||
"gulp-open": "^3.0.1",
|
||||
"gulp-plumber": "^1.2.1",
|
||||
"gulp-postcss": "^8.0.0",
|
||||
@@ -65,31 +67,30 @@
|
||||
"gulp-size": "^3.0.0",
|
||||
"gulp-sourcemaps": "^2.6.5",
|
||||
"gulp-svgstore": "^7.0.1",
|
||||
"gulp-terser": "^1.1.7",
|
||||
"postcss-custom-properties": "^8.0.10",
|
||||
"prettier-eslint": "^8.8.2",
|
||||
"gulp-terser": "^1.2.0",
|
||||
"postcss-custom-properties": "^9.0.1",
|
||||
"prettier-eslint": "^9.0.0",
|
||||
"prettier-stylelint": "^0.4.2",
|
||||
"remark-cli": "^6.0.1",
|
||||
"remark-validate-links": "^8.0.2",
|
||||
"rollup": "^1.10.0",
|
||||
"remark-validate-links": "^8.0.3",
|
||||
"rollup": "^1.15.6",
|
||||
"rollup-plugin-babel": "^4.3.2",
|
||||
"rollup-plugin-commonjs": "^9.3.4",
|
||||
"rollup-plugin-node-resolve": "^4.2.3",
|
||||
"stylelint": "^9.10.1",
|
||||
"stylelint-config-prettier": "^5.0.0",
|
||||
"stylelint-config-recommended": "^2.1.0",
|
||||
"stylelint-config-sass-guidelines": "^5.3.0",
|
||||
"stylelint-order": "^2.2.1",
|
||||
"stylelint-scss": "^3.5.4",
|
||||
"rollup-plugin-commonjs": "^10.0.0",
|
||||
"rollup-plugin-node-resolve": "^5.0.3",
|
||||
"stylelint": "^10.1.0",
|
||||
"stylelint-config-prettier": "^5.2.0",
|
||||
"stylelint-config-recommended": "^2.2.0",
|
||||
"stylelint-config-sass-guidelines": "^6.0.0",
|
||||
"stylelint-order": "^3.0.0",
|
||||
"stylelint-scss": "^3.8.0",
|
||||
"stylelint-selector-bem-pattern": "^2.1.0",
|
||||
"through2": "^3.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "^3.0.1",
|
||||
"core-js": "^3.1.4",
|
||||
"custom-event-polyfill": "^1.0.7",
|
||||
"loadjs": "^3.6.1",
|
||||
"rangetouch": "^2.0.0",
|
||||
"raven-js": "^3.27.0",
|
||||
"url-polyfill": "^1.1.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,9 +23,6 @@
|
||||
"editor.tabSize": 4,
|
||||
"editor.insertSpaces": true,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true
|
||||
},
|
||||
|
||||
// Trim on save
|
||||
"files.trimTrailingWhitespace": true
|
||||
|
||||
@@ -6,13 +6,12 @@ Plyr is a simple, lightweight, accessible and customizable HTML5, YouTube and Vi
|
||||
|
||||
# Features
|
||||
|
||||
- 📼 **HTML Video & Audio, YouTube & Vimeo** - support for the major formats
|
||||
- 💪 **Accessible** - full support for VTT captions and screen readers
|
||||
- 🔧 **[Customisable](#html)** - make the player look how you want with the markup you want
|
||||
- 😎 **Good HTML** - uses the _right_ elements. `<input type="range">` for volume and `<progress>` for progress and well, `<button>`s for buttons. There's no
|
||||
- 😎 **Clean HTML** - uses the _right_ elements. `<input type="range">` for volume and `<progress>` for progress and well, `<button>`s for buttons. There's no
|
||||
`<span>` or `<a href="#">` button hacks
|
||||
- 📱 **Responsive** - works with any screen size
|
||||
- 📼 **HTML Video & Audio** - support for both formats
|
||||
- 📺 **[Embedded Video](#embeds)** - support for YouTube and Vimeo video playback
|
||||
- 💵 **[Monetization](#ads)** - make money from your videos
|
||||
- 📹 **[Streaming](#demos)** - support for hls.js, Shaka and dash.js streaming playback
|
||||
- 🎛 **[API](#api)** - toggle playback, volume, seeking, and more through a standardized API
|
||||
@@ -25,7 +24,7 @@ Plyr is a simple, lightweight, accessible and customizable HTML5, YouTube and Vi
|
||||
- 📖 **Multiple captions** - support for multiple caption tracks
|
||||
- 🌎 **i18n support** - support for internationalization of controls
|
||||
- 👌 **[Preview thumbnails](#preview-thumbnails)** - support for displaying preview thumbnails
|
||||
- 🤟 **No dependencies** - written in "vanilla" ES6 JavaScript, no jQuery required
|
||||
- 🤟 **No frameworks** - written in "vanilla" ES6 JavaScript, no jQuery required
|
||||
- 💁♀️ **SASS** - to include in your build processes
|
||||
|
||||
### Demos
|
||||
@@ -109,7 +108,15 @@ Or the `<div>` non progressively enhanced method:
|
||||
|
||||
## JavaScript
|
||||
|
||||
Include the `plyr.js` script before the closing `</body>` tag and then in your JS create a new instance of Plyr as below.
|
||||
You can use Plyr as an ES6 module as follows:
|
||||
|
||||
```javascript
|
||||
import Plyr from 'plyr';
|
||||
|
||||
const player = new Plyr('#player');
|
||||
```
|
||||
|
||||
Alertnatively you can include the `plyr.js` script before the closing `</body>` tag and then in your JS create a new instance of Plyr as below.
|
||||
|
||||
```html
|
||||
<script src="path/to/plyr.js"></script>
|
||||
@@ -123,18 +130,18 @@ See [initialising](#initialising) for more information on advanced setups.
|
||||
You can use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript. There's 2 versions; one with and one without [polyfills](#polyfills). My recommendation would be to manage polyfills seperately as part of your application but to make life easier you can use the polyfilled build.
|
||||
|
||||
```html
|
||||
<script src="https://cdn.plyr.io/3.5.3/plyr.js"></script>
|
||||
<script src="https://cdn.plyr.io/3.5.6/plyr.js"></script>
|
||||
```
|
||||
|
||||
...or...
|
||||
|
||||
```html
|
||||
<script src="https://cdn.plyr.io/3.5.3/plyr.polyfilled.js"></script>
|
||||
<script src="https://cdn.plyr.io/3.5.6/plyr.polyfilled.js"></script>
|
||||
```
|
||||
|
||||
## CSS
|
||||
|
||||
Include the `plyr.css` stylsheet into your `<head>`
|
||||
Include the `plyr.css` stylsheet into your `<head>`.
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="path/to/plyr.css" />
|
||||
@@ -143,13 +150,13 @@ Include the `plyr.css` stylsheet into your `<head>`
|
||||
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the default CSS, you can use the following:
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="https://cdn.plyr.io/3.5.3/plyr.css" />
|
||||
<link rel="stylesheet" href="https://cdn.plyr.io/3.5.6/plyr.css" />
|
||||
```
|
||||
|
||||
## SVG Sprite
|
||||
|
||||
The SVG sprite is loaded automatically from our CDN (provided by [Fastly](https://www.fastly.com/)). To change this, see the [options](#options) below. For
|
||||
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.5.3/plyr.svg`.
|
||||
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.5.6/plyr.svg`.
|
||||
|
||||
# Ads
|
||||
|
||||
@@ -204,7 +211,7 @@ WebVTT captions are supported. To add a caption track, check the HTML example ab
|
||||
|
||||
You can specify a range of arguments for the constructor to use:
|
||||
|
||||
- A CSS string selector that's compatible with [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)
|
||||
- A [CSS string selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors)
|
||||
- A [`HTMLElement`](https://developer.mozilla.org/en/docs/Web/API/HTMLElement)
|
||||
- A [jQuery](https://jquery.com) object
|
||||
|
||||
@@ -212,7 +219,7 @@ _Note_: If a `NodeList`, `Array`, or jQuery object are passed, the first element
|
||||
|
||||
#### Single player
|
||||
|
||||
Passing a [string selector](https://developer.mozilla.org/en-US/docs/Web/API/NodeList):
|
||||
Passing a CSS string selector that's compatible with [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector):
|
||||
|
||||
```javascript
|
||||
const player = new Plyr('#player');
|
||||
@@ -238,7 +245,7 @@ You have two choices here. You can either use a simple array loop to map the con
|
||||
const players = Array.from(document.querySelectorAll('.js-player')).map(p => new Plyr(p));
|
||||
```
|
||||
|
||||
...or use a static method where you can pass a [string selector](https://developer.mozilla.org/en-US/docs/Web/API/NodeList), a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList), an [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) of elements, or a [JQuery](https://jquery.com) object:
|
||||
...or use a static method where you can pass a [CSS string selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors), a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList), an [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) of [HTMLElement](https://developer.mozilla.org/en/docs/Web/API/HTMLElement), or a [JQuery](https://jquery.com) object:
|
||||
|
||||
```javascript
|
||||
const players = Plyr.setup('.js-player');
|
||||
@@ -275,7 +282,7 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
|
||||
| `iconUrl` | String | `null` | Specify a URL or path to the SVG sprite. See the [SVG section](#svg) for more info. |
|
||||
| `iconPrefix` | String | `plyr` | Specify the id prefix for the icons used in the default controls (e.g. "plyr-play" would be "plyr"). This is to prevent clashes if you're using your own SVG sprite but with the default controls. Most people can ignore this option. |
|
||||
| `blankVideo` | String | `https://cdn.plyr.io/static/blank.mp4` | Specify a URL or path to a blank video file used to properly cancel network requests. |
|
||||
| `autoplay` | Boolean | `false` | Autoplay the media on load. This is generally advised against on UX grounds. It is also disabled by default in some browsers. If the `autoplay` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true. |
|
||||
| `autoplay`² | Boolean | `false` | Autoplay the media on load. If the `autoplay` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true. |
|
||||
| `autopause`¹ | Boolean | `true` | Only allow one player playing at once. |
|
||||
| `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. |
|
||||
@@ -305,6 +312,11 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
|
||||
| `previewThumbnails` | Object | `{ enabled: false, src: '' }` | `enabled`: Whether to enable the preview thumbnails (they must be generated by you). `src` must be either a string or an array of strings representing URLs for the VTT files containing the image URL(s). Learn more about [preview thumbnails](#preview-thumbnails) below. |
|
||||
|
||||
1. Vimeo only
|
||||
2. Autoplay is generally not recommended as it is seen as a negative user experience. It is also disabled in many browsers. Before raising issues, do your homework. More info can be found here:
|
||||
|
||||
- https://webkit.org/blog/6784/new-video-policies-for-ios/
|
||||
- https://developers.google.com/web/updates/2017/09/autoplay-policy-changes
|
||||
- https://hacks.mozilla.org/2019/02/firefox-66-to-block-automatically-playing-audible-video-and-audio/
|
||||
|
||||
# API
|
||||
|
||||
@@ -406,6 +418,7 @@ player.fullscreen.active; // false;
|
||||
| `fullscreen.enabled` | ✓ | - | Returns a boolean indicating if the current player has fullscreen enabled. |
|
||||
| `pip`¹ | ✓ | ✓ | Gets or sets the picture-in-picture state of the player. The setter accepts a boolean. This currently only supported on Safari 10+ (on MacOS Sierra+ and iOS 10+) and Chrome 70+. |
|
||||
| `ratio` | ✓ | ✓ | Gets or sets the video aspect ratio. The setter accepts a string in the same format as the `ratio` option. |
|
||||
| `download` | ✓ | ✓ | Gets or sets the URL for the download button. The setter accepts a string containing a valid absolute URL. |
|
||||
|
||||
1. HTML5 only
|
||||
|
||||
@@ -649,7 +662,7 @@ The arguments are:
|
||||
- Provider (`html5`, `youtube` or `vimeo`)
|
||||
- Whether the player has the `playsinline` attribute (only applicable to iOS 10+)
|
||||
|
||||
## Disable support programatically
|
||||
## Disable support programmatically
|
||||
|
||||
The `enabled` option can be used to disable certain User Agents. For example, if you don't want to use Plyr for smartphones, you could use:
|
||||
|
||||
@@ -697,6 +710,7 @@ Plyr costs money to run, not only my time. I donate my time for free as I enjoy
|
||||
- [HTML5 Weekly #177](http://html5weekly.com/issues/177)
|
||||
- [Responsive Design #149](http://us4.campaign-archive2.com/?u=559bc631fe5294fc66f5f7f89&id=451a61490f)
|
||||
- [Web Design Weekly #174](https://web-design-weekly.com/2015/02/24/web-design-weekly-174/)
|
||||
- [Front End Focus #177](https://frontendfoc.us/issues/177)
|
||||
- [Hacker News](https://news.ycombinator.com/item?id=9136774)
|
||||
- [Web Platform Daily](http://webplatformdaily.org/releases/2015-03-04)
|
||||
- [LayerVault Designer News](https://news.layervault.com/stories/45394-plyr--a-simple-html5-media-player)
|
||||
@@ -716,7 +730,7 @@ Plyr costs money to run, not only my time. I donate my time for free as I enjoy
|
||||
- [Sparkk TV](https://www.sparkktv.com/)
|
||||
- [@halfhalftravel](https://www.halfhalftravel.com/)
|
||||
|
||||
Let me know on [Twitter](https://twitter.com/sam_potts) I can add you to the above list. It'd be awesome to see how you're using Plyr :-)
|
||||
If you want to be added to the list, open a pull request. It'd be awesome to see how you're using Plyr 😎
|
||||
|
||||
# Useful links and credits
|
||||
|
||||
|
||||
+19
-15
@@ -85,7 +85,6 @@ const captions = {
|
||||
|
||||
const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];
|
||||
const languages = dedupe(browserLanguages.map(language => language.split('-')[0]));
|
||||
|
||||
let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase();
|
||||
|
||||
// Use first browser language when language is 'auto'
|
||||
@@ -124,19 +123,22 @@ const captions = {
|
||||
|
||||
// Handle tracks (add event listener and "pseudo"-default)
|
||||
if (this.isHTML5 && this.isVideo) {
|
||||
tracks.filter(track => !meta.get(track)).forEach(track => {
|
||||
this.debug.log('Track added', track);
|
||||
// Attempt to store if the original dom element was "default"
|
||||
meta.set(track, {
|
||||
default: track.mode === 'showing',
|
||||
tracks
|
||||
.filter(track => !meta.get(track))
|
||||
.forEach(track => {
|
||||
this.debug.log('Track added', track);
|
||||
// Attempt to store if the original dom element was "default"
|
||||
meta.set(track, {
|
||||
default: track.mode === 'showing',
|
||||
});
|
||||
|
||||
// Turn off native caption rendering to avoid double captions
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
track.mode = 'hidden';
|
||||
|
||||
// Add event listener for cue changes
|
||||
on.call(this, track, 'cuechange', () => captions.updateCues.call(this));
|
||||
});
|
||||
|
||||
// Turn off native caption rendering to avoid double captions
|
||||
track.mode = 'hidden';
|
||||
|
||||
// Add event listener for cue changes
|
||||
on.call(this, track, 'cuechange', () => captions.updateCues.call(this));
|
||||
});
|
||||
}
|
||||
|
||||
// Update language first time it matches, or if the previous matching track was removed
|
||||
@@ -164,7 +166,6 @@ const captions = {
|
||||
|
||||
const { toggled } = this.captions; // Current state
|
||||
const activeClass = this.config.classNames.captions.active;
|
||||
|
||||
// Get the next state
|
||||
// If the method is called without parameter, toggle based on current value
|
||||
const active = is.nullOrUndefined(input) ? !toggled : input;
|
||||
@@ -300,10 +301,12 @@ const captions = {
|
||||
const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default);
|
||||
const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));
|
||||
let track;
|
||||
|
||||
languages.every(language => {
|
||||
track = sorted.find(track => track.language === language);
|
||||
track = sorted.find(t => t.language === language);
|
||||
return !track; // Break iteration if there is a match
|
||||
});
|
||||
|
||||
// If no match is found but is required, get first
|
||||
return track || (force ? sorted[0] : undefined);
|
||||
},
|
||||
@@ -360,6 +363,7 @@ const captions = {
|
||||
// Get cues from track
|
||||
if (!cues) {
|
||||
const track = captions.getCurrentTrack.call(this);
|
||||
|
||||
cues = Array.from((track || {}).activeCues || [])
|
||||
.map(cue => cue.getCueAsHTML())
|
||||
.map(getHTML);
|
||||
|
||||
@@ -61,7 +61,7 @@ const defaults = {
|
||||
// Sprite (for icons)
|
||||
loadSprite: true,
|
||||
iconPrefix: 'plyr',
|
||||
iconUrl: 'https://cdn.plyr.io/3.5.3/plyr.svg',
|
||||
iconUrl: 'https://cdn.plyr.io/3.5.6/plyr.svg',
|
||||
|
||||
// Blank video (used to prevent errors on source change)
|
||||
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
|
||||
@@ -128,6 +128,7 @@ const defaults = {
|
||||
// 'fast-forward',
|
||||
'progress',
|
||||
'current-time',
|
||||
// 'duration',
|
||||
'mute',
|
||||
'volume',
|
||||
'captions',
|
||||
@@ -195,8 +196,7 @@ const defaults = {
|
||||
},
|
||||
youtube: {
|
||||
sdk: 'https://www.youtube.com/iframe_api',
|
||||
api:
|
||||
'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title))&part=snippet',
|
||||
api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}',
|
||||
},
|
||||
googleIMA: {
|
||||
sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
|
||||
@@ -320,9 +320,6 @@ const defaults = {
|
||||
progress: '.plyr__progress',
|
||||
captions: '.plyr__captions',
|
||||
caption: '.plyr__caption',
|
||||
menu: {
|
||||
quality: '.js-plyr__menu__list--quality',
|
||||
},
|
||||
},
|
||||
|
||||
// Class hooks added to the player in different states
|
||||
@@ -396,11 +393,6 @@ const defaults = {
|
||||
},
|
||||
},
|
||||
|
||||
// API keys
|
||||
keys: {
|
||||
google: null,
|
||||
},
|
||||
|
||||
// Advertisements plugin
|
||||
// Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio
|
||||
ads: {
|
||||
|
||||
Vendored
+333
-299
@@ -4,6 +4,7 @@
|
||||
// ==========================================================================
|
||||
|
||||
import RangeTouch from 'rangetouch';
|
||||
|
||||
import captions from './captions';
|
||||
import html5 from './html5';
|
||||
import support from './support';
|
||||
@@ -27,7 +28,7 @@ import {
|
||||
import { off, on } from './utils/events';
|
||||
import i18n from './utils/i18n';
|
||||
import is from './utils/is';
|
||||
import loadSprite from './utils/loadSprite';
|
||||
import loadSprite from './utils/load-sprite';
|
||||
import { extend } from './utils/objects';
|
||||
import { getPercentage, replaceAll, toCamelCase, toTitleCase } from './utils/strings';
|
||||
import { formatTime, getHours } from './utils/time';
|
||||
@@ -105,7 +106,6 @@ const controls = {
|
||||
const namespace = 'http://www.w3.org/2000/svg';
|
||||
const iconUrl = controls.getIconUrl.call(this);
|
||||
const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`;
|
||||
|
||||
// Create <svg>
|
||||
const icon = document.createElementNS(namespace, 'svg');
|
||||
setAttributes(
|
||||
@@ -172,7 +172,7 @@ const controls = {
|
||||
|
||||
// Create a <button>
|
||||
createButton(buttonType, attr) {
|
||||
const attributes = Object.assign({}, attr);
|
||||
const attributes = extend({}, attr);
|
||||
let type = toCamelCase(buttonType);
|
||||
|
||||
const props = {
|
||||
@@ -198,8 +198,10 @@ const controls = {
|
||||
|
||||
// Set class name
|
||||
if (Object.keys(attributes).includes('class')) {
|
||||
if (!attributes.class.includes(this.config.classNames.control)) {
|
||||
attributes.class += ` ${this.config.classNames.control}`;
|
||||
if (!attributes.class.split(' ').some(c => c === this.config.classNames.control)) {
|
||||
extend(attributes, {
|
||||
class: `${attributes.class} ${this.config.classNames.control}`,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
attributes.class = this.config.classNames.control;
|
||||
@@ -377,13 +379,13 @@ const controls = {
|
||||
},
|
||||
|
||||
// Create time display
|
||||
createTime(type) {
|
||||
const attributes = getAttributesFromSelector(this.config.selectors.display[type]);
|
||||
createTime(type, attrs) {
|
||||
const attributes = getAttributesFromSelector(this.config.selectors.display[type], attrs);
|
||||
|
||||
const container = createElement(
|
||||
'div',
|
||||
extend(attributes, {
|
||||
class: `${this.config.classNames.display.time} ${attributes.class ? attributes.class : ''}`.trim(),
|
||||
class: `${attributes.class ? attributes.class : ''} ${this.config.classNames.display.time} `.trim(),
|
||||
'aria-label': i18n.get(type, this.config),
|
||||
}),
|
||||
'00:00',
|
||||
@@ -491,15 +493,15 @@ const controls = {
|
||||
get() {
|
||||
return menuItem.getAttribute('aria-checked') === 'true';
|
||||
},
|
||||
set(checked) {
|
||||
set(check) {
|
||||
// Ensure exclusivity
|
||||
if (checked) {
|
||||
if (check) {
|
||||
Array.from(menuItem.parentNode.children)
|
||||
.filter(node => matches(node, '[role="menuitemradio"]'))
|
||||
.forEach(node => node.setAttribute('aria-checked', 'false'));
|
||||
}
|
||||
|
||||
menuItem.setAttribute('aria-checked', checked ? 'true' : 'false');
|
||||
menuItem.setAttribute('aria-checked', check ? 'true' : 'false');
|
||||
},
|
||||
});
|
||||
|
||||
@@ -607,17 +609,17 @@ const controls = {
|
||||
let value = 0;
|
||||
|
||||
const setProgress = (target, input) => {
|
||||
const value = is.number(input) ? input : 0;
|
||||
const val = is.number(input) ? input : 0;
|
||||
const progress = is.element(target) ? target : this.elements.display.buffer;
|
||||
|
||||
// Update value and label
|
||||
if (is.element(progress)) {
|
||||
progress.value = value;
|
||||
progress.value = val;
|
||||
|
||||
// Update text label inside
|
||||
const label = progress.getElementsByTagName('span')[0];
|
||||
if (is.element(label)) {
|
||||
label.childNodes[0].nodeValue = value;
|
||||
label.childNodes[0].nodeValue = val;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -699,14 +701,8 @@ const controls = {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate percentage
|
||||
let percent = 0;
|
||||
const clientRect = this.elements.progress.getBoundingClientRect();
|
||||
const visible = `${this.config.classNames.tooltip}--visible`;
|
||||
|
||||
const toggle = toggle => {
|
||||
toggleClass(this.elements.display.seekTooltip, visible, toggle);
|
||||
};
|
||||
const toggle = show => toggleClass(this.elements.display.seekTooltip, visible, show);
|
||||
|
||||
// Hide on touch
|
||||
if (this.touch) {
|
||||
@@ -715,6 +711,9 @@ const controls = {
|
||||
}
|
||||
|
||||
// Determine percentage, if already visible
|
||||
let percent = 0;
|
||||
const clientRect = this.elements.progress.getBoundingClientRect();
|
||||
|
||||
if (is.event(event)) {
|
||||
percent = (100 / clientRect.width) * (event.pageX - clientRect.left);
|
||||
} else if (hasClass(this.elements.display.seekTooltip, visible)) {
|
||||
@@ -1111,7 +1110,7 @@ const controls = {
|
||||
let target = pane;
|
||||
|
||||
if (!is.element(target)) {
|
||||
target = Object.values(this.elements.settings.panels).find(pane => !pane.hidden);
|
||||
target = Object.values(this.elements.settings.panels).find(p => !p.hidden);
|
||||
}
|
||||
|
||||
const firstItem = target.querySelector('[role^="menuitem"]');
|
||||
@@ -1138,7 +1137,10 @@ const controls = {
|
||||
} else if (is.keyboardEvent(input) && input.which === 27) {
|
||||
show = false;
|
||||
} else if (is.event(input)) {
|
||||
const isMenuItem = popup.contains(input.target);
|
||||
// If Plyr is in a shadowDOM, the event target is set to the component, instead of the
|
||||
// Element in the shadowDOM. The path, if available, is complete.
|
||||
const target = is.function(input.composedPath) ? input.composedPath()[0] : input.target;
|
||||
const isMenuItem = popup.contains(target);
|
||||
|
||||
// If the click was inside the menu or if the click
|
||||
// wasn't the button or menu item and we're trying to
|
||||
@@ -1191,7 +1193,7 @@ const controls = {
|
||||
|
||||
// Show a panel in the menu
|
||||
showMenuPanel(type = '', tabFocus = false) {
|
||||
const target = document.getElementById(`plyr-settings-${this.id}-${type}`);
|
||||
const target = this.elements.container.querySelector(`#plyr-settings-${this.id}-${type}`);
|
||||
|
||||
// Nothing to show, bail
|
||||
if (!is.element(target)) {
|
||||
@@ -1244,8 +1246,8 @@ const controls = {
|
||||
controls.focusFirstMenuItem.call(this, target, tabFocus);
|
||||
},
|
||||
|
||||
// Set the download link
|
||||
setDownloadLink() {
|
||||
// Set the download URL
|
||||
setDownloadUrl() {
|
||||
const button = this.elements.buttons.download;
|
||||
|
||||
// Bail if no button
|
||||
@@ -1253,324 +1255,356 @@ const controls = {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set download link
|
||||
// Set attribute
|
||||
button.setAttribute('href', this.download);
|
||||
},
|
||||
|
||||
// Build the default HTML
|
||||
// TODO: Set order based on order in the config.controls array?
|
||||
create(data) {
|
||||
const {
|
||||
bindMenuItemShortcuts,
|
||||
createButton,
|
||||
createProgress,
|
||||
createRange,
|
||||
createTime,
|
||||
setQualityMenu,
|
||||
setSpeedMenu,
|
||||
showMenuPanel,
|
||||
} = controls;
|
||||
this.elements.controls = null;
|
||||
|
||||
// Larger overlaid play button
|
||||
if (this.config.controls.includes('play-large')) {
|
||||
this.elements.container.appendChild(createButton.call(this, 'play-large'));
|
||||
}
|
||||
|
||||
// Create the container
|
||||
const container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper));
|
||||
this.elements.controls = container;
|
||||
|
||||
// Restart button
|
||||
if (this.config.controls.includes('restart')) {
|
||||
container.appendChild(controls.createButton.call(this, 'restart'));
|
||||
}
|
||||
// Default item attributes
|
||||
const defaultAttributes = { class: 'plyr__controls__item' };
|
||||
|
||||
// Rewind button
|
||||
if (this.config.controls.includes('rewind')) {
|
||||
container.appendChild(controls.createButton.call(this, 'rewind'));
|
||||
}
|
||||
// Loop through controls in order
|
||||
dedupe(this.config.controls).forEach(control => {
|
||||
// Restart button
|
||||
if (control === 'restart') {
|
||||
container.appendChild(createButton.call(this, 'restart', defaultAttributes));
|
||||
}
|
||||
|
||||
// Play/Pause button
|
||||
if (this.config.controls.includes('play')) {
|
||||
container.appendChild(controls.createButton.call(this, 'play'));
|
||||
}
|
||||
// Rewind button
|
||||
if (control === 'rewind') {
|
||||
container.appendChild(createButton.call(this, 'rewind', defaultAttributes));
|
||||
}
|
||||
|
||||
// Fast forward button
|
||||
if (this.config.controls.includes('fast-forward')) {
|
||||
container.appendChild(controls.createButton.call(this, 'fast-forward'));
|
||||
}
|
||||
// Play/Pause button
|
||||
if (control === 'play') {
|
||||
container.appendChild(createButton.call(this, 'play', defaultAttributes));
|
||||
}
|
||||
|
||||
// Progress
|
||||
if (this.config.controls.includes('progress')) {
|
||||
const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress));
|
||||
// Fast forward button
|
||||
if (control === 'fast-forward') {
|
||||
container.appendChild(createButton.call(this, 'fast-forward', defaultAttributes));
|
||||
}
|
||||
|
||||
// Seek range slider
|
||||
progress.appendChild(
|
||||
controls.createRange.call(this, 'seek', {
|
||||
id: `plyr-seek-${data.id}`,
|
||||
}),
|
||||
);
|
||||
// Progress
|
||||
if (control === 'progress') {
|
||||
const progressContainer = createElement('div', {
|
||||
class: `${defaultAttributes.class} plyr__progress__container`,
|
||||
});
|
||||
|
||||
// Buffer progress
|
||||
progress.appendChild(controls.createProgress.call(this, 'buffer'));
|
||||
const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress));
|
||||
|
||||
// TODO: Add loop display indicator
|
||||
|
||||
// Seek tooltip
|
||||
if (this.config.tooltips.seek) {
|
||||
const tooltip = createElement(
|
||||
'span',
|
||||
{
|
||||
class: this.config.classNames.tooltip,
|
||||
},
|
||||
'00:00',
|
||||
// Seek range slider
|
||||
progress.appendChild(
|
||||
createRange.call(this, 'seek', {
|
||||
id: `plyr-seek-${data.id}`,
|
||||
}),
|
||||
);
|
||||
|
||||
progress.appendChild(tooltip);
|
||||
this.elements.display.seekTooltip = tooltip;
|
||||
// Buffer progress
|
||||
progress.appendChild(createProgress.call(this, 'buffer'));
|
||||
|
||||
// TODO: Add loop display indicator
|
||||
|
||||
// Seek tooltip
|
||||
if (this.config.tooltips.seek) {
|
||||
const tooltip = createElement(
|
||||
'span',
|
||||
{
|
||||
class: this.config.classNames.tooltip,
|
||||
},
|
||||
'00:00',
|
||||
);
|
||||
|
||||
progress.appendChild(tooltip);
|
||||
this.elements.display.seekTooltip = tooltip;
|
||||
}
|
||||
|
||||
this.elements.progress = progress;
|
||||
progressContainer.appendChild(this.elements.progress);
|
||||
container.appendChild(progressContainer);
|
||||
}
|
||||
|
||||
this.elements.progress = progress;
|
||||
container.appendChild(this.elements.progress);
|
||||
}
|
||||
|
||||
// Media current time display
|
||||
if (this.config.controls.includes('current-time')) {
|
||||
container.appendChild(controls.createTime.call(this, 'currentTime'));
|
||||
}
|
||||
|
||||
// Media duration display
|
||||
if (this.config.controls.includes('duration')) {
|
||||
container.appendChild(controls.createTime.call(this, 'duration'));
|
||||
}
|
||||
|
||||
// Volume controls
|
||||
if (this.config.controls.includes('mute') || this.config.controls.includes('volume')) {
|
||||
const volume = createElement('div', {
|
||||
class: 'plyr__volume',
|
||||
});
|
||||
|
||||
// Toggle mute button
|
||||
if (this.config.controls.includes('mute')) {
|
||||
volume.appendChild(controls.createButton.call(this, 'mute'));
|
||||
// Media current time display
|
||||
if (control === 'current-time') {
|
||||
container.appendChild(createTime.call(this, 'currentTime', defaultAttributes));
|
||||
}
|
||||
|
||||
// Volume range control
|
||||
if (this.config.controls.includes('volume')) {
|
||||
// Set the attributes
|
||||
const attributes = {
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
value: this.config.volume,
|
||||
};
|
||||
// Media duration display
|
||||
if (control === 'duration') {
|
||||
container.appendChild(createTime.call(this, 'duration', defaultAttributes));
|
||||
}
|
||||
|
||||
// Create the volume range slider
|
||||
volume.appendChild(
|
||||
controls.createRange.call(
|
||||
this,
|
||||
'volume',
|
||||
extend(attributes, {
|
||||
id: `plyr-volume-${data.id}`,
|
||||
// Volume controls
|
||||
if (control === 'mute' || control === 'volume') {
|
||||
let { volume } = this.elements;
|
||||
|
||||
// Create the volume container if needed
|
||||
if (!is.element(volume) || !container.contains(volume)) {
|
||||
volume = createElement(
|
||||
'div',
|
||||
extend({}, defaultAttributes, {
|
||||
class: `${defaultAttributes.class} plyr__volume`.trim(),
|
||||
}),
|
||||
),
|
||||
);
|
||||
);
|
||||
|
||||
this.elements.volume = volume;
|
||||
this.elements.volume = volume;
|
||||
|
||||
container.appendChild(volume);
|
||||
}
|
||||
|
||||
// Toggle mute button
|
||||
if (control === 'mute') {
|
||||
volume.appendChild(createButton.call(this, 'mute'));
|
||||
}
|
||||
|
||||
// Volume range control
|
||||
if (control === 'volume') {
|
||||
// Set the attributes
|
||||
const attributes = {
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
value: this.config.volume,
|
||||
};
|
||||
|
||||
// Create the volume range slider
|
||||
volume.appendChild(
|
||||
createRange.call(
|
||||
this,
|
||||
'volume',
|
||||
extend(attributes, {
|
||||
id: `plyr-volume-${data.id}`,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
container.appendChild(volume);
|
||||
}
|
||||
// Toggle captions button
|
||||
if (control === 'captions') {
|
||||
container.appendChild(createButton.call(this, 'captions', defaultAttributes));
|
||||
}
|
||||
|
||||
// Toggle captions button
|
||||
if (this.config.controls.includes('captions')) {
|
||||
container.appendChild(controls.createButton.call(this, 'captions'));
|
||||
}
|
||||
|
||||
// Settings button / menu
|
||||
if (this.config.controls.includes('settings') && !is.empty(this.config.settings)) {
|
||||
const control = createElement('div', {
|
||||
class: 'plyr__menu',
|
||||
hidden: '',
|
||||
});
|
||||
|
||||
control.appendChild(
|
||||
controls.createButton.call(this, 'settings', {
|
||||
'aria-haspopup': true,
|
||||
'aria-controls': `plyr-settings-${data.id}`,
|
||||
'aria-expanded': false,
|
||||
}),
|
||||
);
|
||||
|
||||
const popup = createElement('div', {
|
||||
class: 'plyr__menu__container',
|
||||
id: `plyr-settings-${data.id}`,
|
||||
hidden: '',
|
||||
});
|
||||
|
||||
const inner = createElement('div');
|
||||
|
||||
const home = createElement('div', {
|
||||
id: `plyr-settings-${data.id}-home`,
|
||||
});
|
||||
|
||||
// Create the menu
|
||||
const menu = createElement('div', {
|
||||
role: 'menu',
|
||||
});
|
||||
|
||||
home.appendChild(menu);
|
||||
inner.appendChild(home);
|
||||
this.elements.settings.panels.home = home;
|
||||
|
||||
// Build the menu items
|
||||
this.config.settings.forEach(type => {
|
||||
// TODO: bundle this with the createMenuItem helper and bindings
|
||||
const menuItem = createElement(
|
||||
'button',
|
||||
extend(getAttributesFromSelector(this.config.selectors.buttons.settings), {
|
||||
type: 'button',
|
||||
class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`,
|
||||
role: 'menuitem',
|
||||
'aria-haspopup': true,
|
||||
// Settings button / menu
|
||||
if (control === 'settings' && !is.empty(this.config.settings)) {
|
||||
const wrapper = createElement(
|
||||
'div',
|
||||
extend({}, defaultAttributes, {
|
||||
class: `${defaultAttributes.class} plyr__menu`.trim(),
|
||||
hidden: '',
|
||||
}),
|
||||
);
|
||||
|
||||
// Bind menu shortcuts for keyboard users
|
||||
controls.bindMenuItemShortcuts.call(this, menuItem, type);
|
||||
|
||||
// Show menu on click
|
||||
on(menuItem, 'click', () => {
|
||||
controls.showMenuPanel.call(this, type, false);
|
||||
});
|
||||
|
||||
const flex = createElement('span', null, i18n.get(type, this.config));
|
||||
|
||||
const value = createElement('span', {
|
||||
class: this.config.classNames.menu.value,
|
||||
});
|
||||
|
||||
// Speed contains HTML entities
|
||||
value.innerHTML = data[type];
|
||||
|
||||
flex.appendChild(value);
|
||||
menuItem.appendChild(flex);
|
||||
menu.appendChild(menuItem);
|
||||
|
||||
// Build the panes
|
||||
const pane = createElement('div', {
|
||||
id: `plyr-settings-${data.id}-${type}`,
|
||||
hidden: '',
|
||||
});
|
||||
|
||||
// Back button
|
||||
const backButton = createElement('button', {
|
||||
type: 'button',
|
||||
class: `${this.config.classNames.control} ${this.config.classNames.control}--back`,
|
||||
});
|
||||
|
||||
// Visible label
|
||||
backButton.appendChild(
|
||||
createElement(
|
||||
'span',
|
||||
{
|
||||
'aria-hidden': true,
|
||||
},
|
||||
i18n.get(type, this.config),
|
||||
),
|
||||
);
|
||||
|
||||
// Screen reader label
|
||||
backButton.appendChild(
|
||||
createElement(
|
||||
'span',
|
||||
{
|
||||
class: this.config.classNames.hidden,
|
||||
},
|
||||
i18n.get('menuBack', this.config),
|
||||
),
|
||||
);
|
||||
|
||||
// Go back via keyboard
|
||||
on(
|
||||
pane,
|
||||
'keydown',
|
||||
event => {
|
||||
// We only care about <-
|
||||
if (event.which !== 37) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent seek
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// Show the respective menu
|
||||
controls.showMenuPanel.call(this, 'home', true);
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
// Go back via button click
|
||||
on(backButton, 'click', () => {
|
||||
controls.showMenuPanel.call(this, 'home', false);
|
||||
});
|
||||
|
||||
// Add to pane
|
||||
pane.appendChild(backButton);
|
||||
|
||||
// Menu
|
||||
pane.appendChild(
|
||||
createElement('div', {
|
||||
role: 'menu',
|
||||
wrapper.appendChild(
|
||||
createButton.call(this, 'settings', {
|
||||
'aria-haspopup': true,
|
||||
'aria-controls': `plyr-settings-${data.id}`,
|
||||
'aria-expanded': false,
|
||||
}),
|
||||
);
|
||||
|
||||
inner.appendChild(pane);
|
||||
|
||||
this.elements.settings.buttons[type] = menuItem;
|
||||
this.elements.settings.panels[type] = pane;
|
||||
});
|
||||
|
||||
popup.appendChild(inner);
|
||||
control.appendChild(popup);
|
||||
container.appendChild(control);
|
||||
|
||||
this.elements.settings.popup = popup;
|
||||
this.elements.settings.menu = control;
|
||||
}
|
||||
|
||||
// Picture in picture button
|
||||
if (this.config.controls.includes('pip') && support.pip) {
|
||||
container.appendChild(controls.createButton.call(this, 'pip'));
|
||||
}
|
||||
|
||||
// Airplay button
|
||||
if (this.config.controls.includes('airplay') && support.airplay) {
|
||||
container.appendChild(controls.createButton.call(this, 'airplay'));
|
||||
}
|
||||
|
||||
// Download button
|
||||
if (this.config.controls.includes('download')) {
|
||||
const attributes = {
|
||||
element: 'a',
|
||||
href: this.download,
|
||||
target: '_blank',
|
||||
};
|
||||
|
||||
const { download } = this.config.urls;
|
||||
|
||||
if (!is.url(download) && this.isEmbed) {
|
||||
extend(attributes, {
|
||||
icon: `logo-${this.provider}`,
|
||||
label: this.provider,
|
||||
const popup = createElement('div', {
|
||||
class: 'plyr__menu__container',
|
||||
id: `plyr-settings-${data.id}`,
|
||||
hidden: '',
|
||||
});
|
||||
|
||||
const inner = createElement('div');
|
||||
|
||||
const home = createElement('div', {
|
||||
id: `plyr-settings-${data.id}-home`,
|
||||
});
|
||||
|
||||
// Create the menu
|
||||
const menu = createElement('div', {
|
||||
role: 'menu',
|
||||
});
|
||||
|
||||
home.appendChild(menu);
|
||||
inner.appendChild(home);
|
||||
this.elements.settings.panels.home = home;
|
||||
|
||||
// Build the menu items
|
||||
this.config.settings.forEach(type => {
|
||||
// TODO: bundle this with the createMenuItem helper and bindings
|
||||
const menuItem = createElement(
|
||||
'button',
|
||||
extend(getAttributesFromSelector(this.config.selectors.buttons.settings), {
|
||||
type: 'button',
|
||||
class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`,
|
||||
role: 'menuitem',
|
||||
'aria-haspopup': true,
|
||||
hidden: '',
|
||||
}),
|
||||
);
|
||||
|
||||
// Bind menu shortcuts for keyboard users
|
||||
bindMenuItemShortcuts.call(this, menuItem, type);
|
||||
|
||||
// Show menu on click
|
||||
on(menuItem, 'click', () => {
|
||||
showMenuPanel.call(this, type, false);
|
||||
});
|
||||
|
||||
const flex = createElement('span', null, i18n.get(type, this.config));
|
||||
|
||||
const value = createElement('span', {
|
||||
class: this.config.classNames.menu.value,
|
||||
});
|
||||
|
||||
// Speed contains HTML entities
|
||||
value.innerHTML = data[type];
|
||||
|
||||
flex.appendChild(value);
|
||||
menuItem.appendChild(flex);
|
||||
menu.appendChild(menuItem);
|
||||
|
||||
// Build the panes
|
||||
const pane = createElement('div', {
|
||||
id: `plyr-settings-${data.id}-${type}`,
|
||||
hidden: '',
|
||||
});
|
||||
|
||||
// Back button
|
||||
const backButton = createElement('button', {
|
||||
type: 'button',
|
||||
class: `${this.config.classNames.control} ${this.config.classNames.control}--back`,
|
||||
});
|
||||
|
||||
// Visible label
|
||||
backButton.appendChild(
|
||||
createElement(
|
||||
'span',
|
||||
{
|
||||
'aria-hidden': true,
|
||||
},
|
||||
i18n.get(type, this.config),
|
||||
),
|
||||
);
|
||||
|
||||
// Screen reader label
|
||||
backButton.appendChild(
|
||||
createElement(
|
||||
'span',
|
||||
{
|
||||
class: this.config.classNames.hidden,
|
||||
},
|
||||
i18n.get('menuBack', this.config),
|
||||
),
|
||||
);
|
||||
|
||||
// Go back via keyboard
|
||||
on(
|
||||
pane,
|
||||
'keydown',
|
||||
event => {
|
||||
// We only care about <-
|
||||
if (event.which !== 37) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent seek
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// Show the respective menu
|
||||
showMenuPanel.call(this, 'home', true);
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
// Go back via button click
|
||||
on(backButton, 'click', () => {
|
||||
showMenuPanel.call(this, 'home', false);
|
||||
});
|
||||
|
||||
// Add to pane
|
||||
pane.appendChild(backButton);
|
||||
|
||||
// Menu
|
||||
pane.appendChild(
|
||||
createElement('div', {
|
||||
role: 'menu',
|
||||
}),
|
||||
);
|
||||
|
||||
inner.appendChild(pane);
|
||||
|
||||
this.elements.settings.buttons[type] = menuItem;
|
||||
this.elements.settings.panels[type] = pane;
|
||||
});
|
||||
|
||||
popup.appendChild(inner);
|
||||
wrapper.appendChild(popup);
|
||||
container.appendChild(wrapper);
|
||||
|
||||
this.elements.settings.popup = popup;
|
||||
this.elements.settings.menu = wrapper;
|
||||
}
|
||||
|
||||
container.appendChild(controls.createButton.call(this, 'download', attributes));
|
||||
}
|
||||
// Picture in picture button
|
||||
if (control === 'pip' && support.pip) {
|
||||
container.appendChild(createButton.call(this, 'pip', defaultAttributes));
|
||||
}
|
||||
|
||||
// Toggle fullscreen button
|
||||
if (this.config.controls.includes('fullscreen')) {
|
||||
container.appendChild(controls.createButton.call(this, 'fullscreen'));
|
||||
}
|
||||
// Airplay button
|
||||
if (control === 'airplay' && support.airplay) {
|
||||
container.appendChild(createButton.call(this, 'airplay', defaultAttributes));
|
||||
}
|
||||
|
||||
// Larger overlaid play button
|
||||
if (this.config.controls.includes('play-large')) {
|
||||
this.elements.container.appendChild(controls.createButton.call(this, 'play-large'));
|
||||
}
|
||||
// Download button
|
||||
if (control === 'download') {
|
||||
const attributes = extend({}, defaultAttributes, {
|
||||
element: 'a',
|
||||
href: this.download,
|
||||
target: '_blank',
|
||||
});
|
||||
|
||||
this.elements.controls = container;
|
||||
const { download } = this.config.urls;
|
||||
|
||||
if (!is.url(download) && this.isEmbed) {
|
||||
extend(attributes, {
|
||||
icon: `logo-${this.provider}`,
|
||||
label: this.provider,
|
||||
});
|
||||
}
|
||||
|
||||
container.appendChild(createButton.call(this, 'download', attributes));
|
||||
}
|
||||
|
||||
// Toggle fullscreen button
|
||||
if (control === 'fullscreen') {
|
||||
container.appendChild(createButton.call(this, 'fullscreen', defaultAttributes));
|
||||
}
|
||||
});
|
||||
|
||||
// Set available quality levels
|
||||
if (this.isHTML5) {
|
||||
controls.setQualityMenu.call(this, html5.getQualityOptions.call(this));
|
||||
setQualityMenu.call(this, html5.getQualityOptions.call(this));
|
||||
}
|
||||
|
||||
controls.setSpeedMenu.call(this);
|
||||
setSpeedMenu.call(this);
|
||||
|
||||
return container;
|
||||
},
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
// https://webkit.org/blog/7929/designing-websites-for-iphone-x/
|
||||
// ==========================================================================
|
||||
|
||||
import { repaint } from './utils/animation';
|
||||
import browser from './utils/browser';
|
||||
import { hasClass, toggleClass, trapFocus } from './utils/elements';
|
||||
import { on, triggerEvent } from './utils/events';
|
||||
@@ -73,9 +72,6 @@ function toggleFallback(toggle = false) {
|
||||
.filter(part => part.trim() !== property)
|
||||
.join(',');
|
||||
}
|
||||
|
||||
// Force a repaint as sometimes Safari doesn't want to fill the screen
|
||||
setTimeout(() => repaint(this.target), 100);
|
||||
}
|
||||
|
||||
// Toggle button and fire events
|
||||
|
||||
+6
-5
@@ -44,15 +44,17 @@ const html5 = {
|
||||
|
||||
const player = this;
|
||||
|
||||
// Set aspect ratio if set
|
||||
setAspectRatio.call(player);
|
||||
// Set aspect ratio if fixed
|
||||
if (!is.empty(this.config.ratio)) {
|
||||
setAspectRatio.call(player);
|
||||
}
|
||||
|
||||
// Quality
|
||||
Object.defineProperty(player.media, 'quality', {
|
||||
get() {
|
||||
// Get sources
|
||||
const sources = html5.getSources.call(player);
|
||||
const source = sources.find(source => source.getAttribute('src') === player.source);
|
||||
const source = sources.find(s => s.getAttribute('src') === player.source);
|
||||
|
||||
// Return size, if match is found
|
||||
return source && Number(source.getAttribute('size'));
|
||||
@@ -60,9 +62,8 @@ const html5 = {
|
||||
set(input) {
|
||||
// Get sources
|
||||
const sources = html5.getSources.call(player);
|
||||
|
||||
// Get first match for requested size
|
||||
const source = sources.find(source => Number(source.getAttribute('size')) === input);
|
||||
const source = sources.find(s => Number(s.getAttribute('size')) === input);
|
||||
|
||||
// No matching source found
|
||||
if (!source) {
|
||||
|
||||
+17
-27
@@ -147,7 +147,7 @@ class Listeners {
|
||||
player.loop = !player.loop;
|
||||
break;
|
||||
|
||||
/* case 73:
|
||||
/* case 73:
|
||||
this.setLoop('start');
|
||||
break;
|
||||
|
||||
@@ -275,17 +275,16 @@ class Listeners {
|
||||
elements.container,
|
||||
'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen',
|
||||
event => {
|
||||
const { controls } = elements;
|
||||
const { controls: controlsElement } = elements;
|
||||
|
||||
// Remove button states for fullscreen
|
||||
if (controls && event.type === 'enterfullscreen') {
|
||||
controls.pressed = false;
|
||||
controls.hover = false;
|
||||
if (controlsElement && event.type === 'enterfullscreen') {
|
||||
controlsElement.pressed = false;
|
||||
controlsElement.hover = false;
|
||||
}
|
||||
|
||||
// Show, then hide after a timeout unless another control event occurs
|
||||
const show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type);
|
||||
|
||||
let delay = 0;
|
||||
|
||||
if (show) {
|
||||
@@ -302,14 +301,6 @@ class Listeners {
|
||||
},
|
||||
);
|
||||
|
||||
// Force edge to repaint on exit fullscreen
|
||||
// TODO: Fix weird bug where Edge doesn't re-draw when exiting fullscreen
|
||||
/* if (browser.isEdge) {
|
||||
on.call(player, elements.container, 'exitfullscreen', () => {
|
||||
setTimeout(() => repaint(elements.container), 100);
|
||||
});
|
||||
} */
|
||||
|
||||
// Set a gutter for Vimeo
|
||||
const setGutter = (ratio, padding, toggle) => {
|
||||
if (!player.isVimeo) {
|
||||
@@ -318,7 +309,7 @@ class Listeners {
|
||||
|
||||
const target = player.elements.wrapper.firstChild;
|
||||
const [, y] = ratio;
|
||||
const [videoX, videoY] = getAspectRatio.call(this);
|
||||
const [videoX, videoY] = getAspectRatio.call(player);
|
||||
|
||||
target.style.maxWidth = toggle ? `${(y / videoY) * videoX}px` : null;
|
||||
target.style.margin = toggle ? '0 auto' : null;
|
||||
@@ -338,20 +329,24 @@ class Listeners {
|
||||
};
|
||||
|
||||
const resized = () => {
|
||||
window.clearTimeout(timers.resized);
|
||||
timers.resized = window.setTimeout(setPlayerSize, 50);
|
||||
clearTimeout(timers.resized);
|
||||
timers.resized = setTimeout(setPlayerSize, 50);
|
||||
};
|
||||
|
||||
on.call(player, elements.container, 'enterfullscreen exitfullscreen', event => {
|
||||
const { target, usingNative } = player.fullscreen;
|
||||
|
||||
// Ignore for iOS native
|
||||
if (!player.isEmbed || target !== elements.container) {
|
||||
// Ignore events not from target
|
||||
if (target !== elements.container) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If it's not an embed and no ratio specified
|
||||
if (!player.isEmbed && is.empty(player.config.ratio)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isEnter = event.type === 'enterfullscreen';
|
||||
|
||||
// Set the player size when entering fullscreen to viewport size
|
||||
const { padding, ratio } = setPlayerSize(isEnter);
|
||||
|
||||
@@ -486,7 +481,7 @@ class Listeners {
|
||||
|
||||
// Update download link when ready and if quality changes
|
||||
on.call(player, player.media, 'ready qualitychange', () => {
|
||||
controls.setDownloadLink.call(player);
|
||||
controls.setDownloadUrl.call(player);
|
||||
});
|
||||
|
||||
// Proxy events to container
|
||||
@@ -542,7 +537,6 @@ class Listeners {
|
||||
controls() {
|
||||
const { player } = this;
|
||||
const { elements } = player;
|
||||
|
||||
// IE doesn't support input event, so we fallback to change
|
||||
const inputEvent = browser.isIE ? 'change' : 'input';
|
||||
|
||||
@@ -678,7 +672,6 @@ class Listeners {
|
||||
|
||||
// Was playing before?
|
||||
const play = seek.hasAttribute(attribute);
|
||||
|
||||
// Done seeking
|
||||
const done = ['mouseup', 'touchend', 'keyup'].includes(event.type);
|
||||
|
||||
@@ -706,7 +699,6 @@ class Listeners {
|
||||
inputEvent,
|
||||
event => {
|
||||
const seek = event.currentTarget;
|
||||
|
||||
// If it exists, use seek-value instead of "value" for consistency with tooltip time (#954)
|
||||
let seekTo = seek.getAttribute('seek-value');
|
||||
|
||||
@@ -806,7 +798,7 @@ class Listeners {
|
||||
|
||||
// Show controls when they receive focus (e.g., when using keyboard tab key)
|
||||
this.bind(elements.controls, 'focusin', () => {
|
||||
const { config, elements, timers } = player;
|
||||
const { config, timers } = player;
|
||||
|
||||
// Skip transition to prevent focus from scrolling the parent element
|
||||
toggleClass(elements.controls, config.classNames.noTransition, true);
|
||||
@@ -837,10 +829,8 @@ class Listeners {
|
||||
// Detect "natural" scroll - suppored on OS X Safari only
|
||||
// Other browsers on OS X will be inverted until support improves
|
||||
const inverted = event.webkitDirectionInvertedFromDevice;
|
||||
|
||||
// Get delta from event. Invert if `inverted` is true
|
||||
const [x, y] = [event.deltaX, -event.deltaY].map(value => (inverted ? -value : value));
|
||||
|
||||
// Using the biggest delta, normalize to 1 or -1 (or 0 if no delta)
|
||||
const direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y);
|
||||
|
||||
|
||||
+53
-40
@@ -10,10 +10,24 @@ import { createElement } from '../utils/elements';
|
||||
import { triggerEvent } from '../utils/events';
|
||||
import i18n from '../utils/i18n';
|
||||
import is from '../utils/is';
|
||||
import loadScript from '../utils/loadScript';
|
||||
import loadScript from '../utils/load-script';
|
||||
import { formatTime } from '../utils/time';
|
||||
import { buildUrlParams } from '../utils/urls';
|
||||
|
||||
const destroy = instance => {
|
||||
// Destroy our adsManager
|
||||
if (instance.manager) {
|
||||
instance.manager.destroy();
|
||||
}
|
||||
|
||||
// Destroy our adsManager
|
||||
if (instance.elements.displayContainer) {
|
||||
instance.elements.displayContainer.destroy();
|
||||
}
|
||||
|
||||
instance.elements.container.remove();
|
||||
};
|
||||
|
||||
class Ads {
|
||||
/**
|
||||
* Ads constructor.
|
||||
@@ -63,20 +77,22 @@ class Ads {
|
||||
* Load the IMA SDK
|
||||
*/
|
||||
load() {
|
||||
if (this.enabled) {
|
||||
// Check if the Google IMA3 SDK is loaded or load it ourselves
|
||||
if (!is.object(window.google) || !is.object(window.google.ima)) {
|
||||
loadScript(this.player.config.urls.googleIMA.sdk)
|
||||
.then(() => {
|
||||
this.ready();
|
||||
})
|
||||
.catch(() => {
|
||||
// Script failed to load or is blocked
|
||||
this.trigger('error', new Error('Google IMA SDK failed to load'));
|
||||
});
|
||||
} else {
|
||||
this.ready();
|
||||
}
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the Google IMA3 SDK is loaded or load it ourselves
|
||||
if (!is.object(window.google) || !is.object(window.google.ima)) {
|
||||
loadScript(this.player.config.urls.googleIMA.sdk)
|
||||
.then(() => {
|
||||
this.ready();
|
||||
})
|
||||
.catch(() => {
|
||||
// Script failed to load or is blocked
|
||||
this.trigger('error', new Error('Google IMA SDK failed to load'));
|
||||
});
|
||||
} else {
|
||||
this.ready();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +100,11 @@ class Ads {
|
||||
* Get the ads instance ready
|
||||
*/
|
||||
ready() {
|
||||
// Double check we're enabled
|
||||
if (!this.enabled) {
|
||||
destroy(this);
|
||||
}
|
||||
|
||||
// Start ticking our safety timer. If the whole advertisement
|
||||
// thing doesn't resolve within our set time; we bail
|
||||
this.startSafetyTimer(12000, 'ready()');
|
||||
@@ -240,16 +261,13 @@ class Ads {
|
||||
// Get the cue points for any mid-rolls by filtering out the pre- and post-roll
|
||||
this.cuePoints = this.manager.getCuePoints();
|
||||
|
||||
// Set volume to match player
|
||||
this.manager.setVolume(this.player.volume);
|
||||
|
||||
// Add listeners to the required events
|
||||
// Advertisement error events
|
||||
this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error));
|
||||
|
||||
// Advertisement regular events
|
||||
Object.keys(google.ima.AdEvent.Type).forEach(type => {
|
||||
this.manager.addEventListener(google.ima.AdEvent.Type[type], event => this.onAdEvent(event));
|
||||
this.manager.addEventListener(google.ima.AdEvent.Type[type], e => this.onAdEvent(e));
|
||||
});
|
||||
|
||||
// Resolve our adsManager
|
||||
@@ -285,7 +303,6 @@ class Ads {
|
||||
*/
|
||||
onAdEvent(event) {
|
||||
const { container } = this.player.elements;
|
||||
|
||||
// Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED)
|
||||
// don't have ad object associated
|
||||
const ad = event.getAd();
|
||||
@@ -293,19 +310,18 @@ class Ads {
|
||||
|
||||
// Proxy event
|
||||
const dispatchEvent = type => {
|
||||
const event = `ads${type.replace(/_/g, '').toLowerCase()}`;
|
||||
triggerEvent.call(this.player, this.player.media, event);
|
||||
triggerEvent.call(this.player, this.player.media, `ads${type.replace(/_/g, '').toLowerCase()}`);
|
||||
};
|
||||
|
||||
// Bubble the event
|
||||
dispatchEvent(event.type);
|
||||
|
||||
switch (event.type) {
|
||||
case google.ima.AdEvent.Type.LOADED:
|
||||
// This is the first event sent for an ad - it is possible to determine whether the
|
||||
// ad is a video ad or an overlay
|
||||
this.trigger('loaded');
|
||||
|
||||
// Bubble event
|
||||
dispatchEvent(event.type);
|
||||
|
||||
// Start countdown
|
||||
this.pollCountdown(true);
|
||||
|
||||
@@ -317,15 +333,19 @@ class Ads {
|
||||
|
||||
// console.info('Ad type: ' + event.getAd().getAdPodInfo().getPodIndex());
|
||||
// console.info('Ad time: ' + event.getAd().getAdPodInfo().getTimeOffset());
|
||||
|
||||
break;
|
||||
|
||||
case google.ima.AdEvent.Type.STARTED:
|
||||
// Set volume to match player
|
||||
this.manager.setVolume(this.player.volume);
|
||||
|
||||
break;
|
||||
|
||||
case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:
|
||||
// All ads for the current videos are done. We can now request new advertisements
|
||||
// in case the video is re-played
|
||||
|
||||
// Fire event
|
||||
dispatchEvent(event.type);
|
||||
|
||||
// TODO: Example for what happens when a next video in a playlist would be loaded.
|
||||
// So here we load a new video when all ads are done.
|
||||
// Then we load new ads within a new adsManager. When the video
|
||||
@@ -350,6 +370,7 @@ class Ads {
|
||||
// playing when the IMA SDK is ready or has failed
|
||||
|
||||
this.loadAds();
|
||||
|
||||
break;
|
||||
|
||||
case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:
|
||||
@@ -357,8 +378,6 @@ class Ads {
|
||||
// for example display a pause button and remaining time. Fired when content should
|
||||
// be paused. This usually happens right before an ad is about to cover the content
|
||||
|
||||
dispatchEvent(event.type);
|
||||
|
||||
this.pauseContent();
|
||||
|
||||
break;
|
||||
@@ -369,26 +388,17 @@ class Ads {
|
||||
// Fired when content should be resumed. This usually happens when an ad finishes
|
||||
// or collapses
|
||||
|
||||
dispatchEvent(event.type);
|
||||
|
||||
this.pollCountdown();
|
||||
|
||||
this.resumeContent();
|
||||
|
||||
break;
|
||||
|
||||
case google.ima.AdEvent.Type.STARTED:
|
||||
case google.ima.AdEvent.Type.MIDPOINT:
|
||||
case google.ima.AdEvent.Type.COMPLETE:
|
||||
case google.ima.AdEvent.Type.IMPRESSION:
|
||||
case google.ima.AdEvent.Type.CLICK:
|
||||
dispatchEvent(event.type);
|
||||
break;
|
||||
|
||||
case google.ima.AdEvent.Type.LOG:
|
||||
if (adData.adError) {
|
||||
this.player.debug.warn(`Non-fatal ad error: ${adData.adError.getMessage()}`);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -463,6 +473,9 @@ class Ads {
|
||||
// Play the requested advertisement whenever the adsManager is ready
|
||||
this.managerPromise
|
||||
.then(() => {
|
||||
// Set volume to match player
|
||||
this.manager.setVolume(this.player.volume);
|
||||
|
||||
// Initialize the container. Must be done via a user action on mobile devices
|
||||
this.elements.displayContainer.initialize();
|
||||
|
||||
|
||||
@@ -100,6 +100,10 @@ class PreviewThumbnails {
|
||||
}
|
||||
|
||||
this.getThumbnails().then(() => {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Render DOM elements
|
||||
this.render();
|
||||
|
||||
@@ -121,7 +125,6 @@ class PreviewThumbnails {
|
||||
|
||||
// If string, convert into single-element list
|
||||
const urls = is.string(src) ? [src] : src;
|
||||
|
||||
// Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails
|
||||
const promises = urls.map(u => this.getThumbnail(u));
|
||||
|
||||
@@ -149,9 +152,11 @@ class PreviewThumbnails {
|
||||
// If the URLs don't start with '/', then we need to set their relative path to be the location of the VTT file
|
||||
// If the URLs do start with '/', then they obviously don't need a prefix, so it will remain blank
|
||||
// If the thumbnail URLs start with with none of '/', 'http://' or 'https://', then we need to set their relative path to be the location of the VTT file
|
||||
if (!thumbnail.frames[0].text.startsWith('/') &&
|
||||
if (
|
||||
!thumbnail.frames[0].text.startsWith('/') &&
|
||||
!thumbnail.frames[0].text.startsWith('http://') &&
|
||||
!thumbnail.frames[0].text.startsWith('https://')) {
|
||||
!thumbnail.frames[0].text.startsWith('https://')
|
||||
) {
|
||||
thumbnail.urlPrefix = url.substring(0, url.lastIndexOf('/') + 1);
|
||||
}
|
||||
|
||||
@@ -297,7 +302,9 @@ class PreviewThumbnails {
|
||||
this.elements.thumb.container.appendChild(timeContainer);
|
||||
|
||||
// Inject the whole thumb
|
||||
this.player.elements.progress.appendChild(this.elements.thumb.container);
|
||||
if (is.element(this.player.elements.progress)) {
|
||||
this.player.elements.progress.appendChild(this.elements.thumb.container);
|
||||
}
|
||||
|
||||
// Create HTML element: plyr__preview-scrubbing-container
|
||||
this.elements.scrubbing.container = createElement('div', {
|
||||
@@ -422,7 +429,9 @@ class PreviewThumbnails {
|
||||
if (image.dataset.index !== currentImage.dataset.index && !image.dataset.deleting) {
|
||||
// Wait 200ms, as the new image can take some time to show on certain browsers (even though it was downloaded before showing). This will prevent flicker, and show some generosity towards slower clients
|
||||
// First set attribute 'deleting' to prevent multi-handling of this on repeat firing of this function
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
image.dataset.deleting = true;
|
||||
|
||||
// This has to be set before the timeout - to prevent issues switching between hover and scrub
|
||||
const { currentImageContainer } = this;
|
||||
|
||||
@@ -463,7 +472,6 @@ class PreviewThumbnails {
|
||||
|
||||
const { urlPrefix } = this.thumbnails[0];
|
||||
const thumbURL = urlPrefix + newThumbFilename;
|
||||
|
||||
const previewImage = new Image();
|
||||
previewImage.src = thumbURL;
|
||||
previewImage.onload = () => {
|
||||
@@ -597,11 +605,9 @@ class PreviewThumbnails {
|
||||
const seekbarRect = this.player.elements.progress.getBoundingClientRect();
|
||||
const plyrRect = this.player.elements.container.getBoundingClientRect();
|
||||
const { container } = this.elements.thumb;
|
||||
|
||||
// Find the lowest and highest desired left-position, so we don't slide out the side of the video container
|
||||
const minVal = plyrRect.left - seekbarRect.left + 10;
|
||||
const maxVal = plyrRect.right - seekbarRect.left - container.clientWidth - 10;
|
||||
|
||||
// Set preview container position to: mousepos, minus seekbar.left, minus half of previewContainer.clientWidth
|
||||
let previewPos = this.mousePosX - seekbarRect.left - container.clientWidth / 2;
|
||||
|
||||
@@ -632,9 +638,13 @@ class PreviewThumbnails {
|
||||
// Find difference between height and preview container height
|
||||
const multiplier = this.thumbContainerHeight / frame.h;
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
previewImage.style.height = `${Math.floor(previewImage.naturalHeight * multiplier)}px`;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
previewImage.style.width = `${Math.floor(previewImage.naturalWidth * multiplier)}px`;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
previewImage.style.left = `-${frame.x * multiplier}px`;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
previewImage.style.top = `-${frame.y * multiplier}px`;
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import { createElement, replaceElement, toggleClass } from '../utils/elements';
|
||||
import { triggerEvent } from '../utils/events';
|
||||
import fetch from '../utils/fetch';
|
||||
import is from '../utils/is';
|
||||
import loadScript from '../utils/loadScript';
|
||||
import loadScript from '../utils/load-script';
|
||||
import { extend } from '../utils/objects';
|
||||
import { format, stripHTML } from '../utils/strings';
|
||||
import { setAspectRatio } from '../utils/style';
|
||||
@@ -48,14 +48,14 @@ const vimeo = {
|
||||
// Set intial ratio
|
||||
setAspectRatio.call(this);
|
||||
|
||||
// Load the API if not already
|
||||
// Load the SDK if not already
|
||||
if (!is.object(window.Vimeo)) {
|
||||
loadScript(this.config.urls.vimeo.sdk)
|
||||
.then(() => {
|
||||
vimeo.ready.call(this);
|
||||
})
|
||||
.catch(error => {
|
||||
this.debug.warn('Vimeo API failed to load', error);
|
||||
this.debug.warn('Vimeo SDK (player.js) failed to load', error);
|
||||
});
|
||||
} else {
|
||||
vimeo.ready.call(this);
|
||||
@@ -91,7 +91,6 @@ const vimeo = {
|
||||
}
|
||||
|
||||
const id = parseId(source);
|
||||
|
||||
// Build an iframe
|
||||
const iframe = createElement('iframe');
|
||||
const src = format(player.config.urls.vimeo.iframe, id, params);
|
||||
@@ -102,7 +101,6 @@ const vimeo = {
|
||||
|
||||
// Get poster, if already set
|
||||
const { poster } = player;
|
||||
|
||||
// Inject the package
|
||||
const wrapper = createElement('div', { poster, class: player.config.classNames.embedContainer });
|
||||
wrapper.appendChild(iframe);
|
||||
@@ -259,7 +257,7 @@ const vimeo = {
|
||||
.getVideoUrl()
|
||||
.then(value => {
|
||||
currentSrc = value;
|
||||
controls.setDownloadLink.call(player);
|
||||
controls.setDownloadUrl.call(player);
|
||||
})
|
||||
.catch(error => {
|
||||
this.debug.warn(error);
|
||||
@@ -281,7 +279,7 @@ const vimeo = {
|
||||
// Set aspect ratio based on video size
|
||||
Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => {
|
||||
const [width, height] = dimensions;
|
||||
player.embed.ratio = `${width}:${height}`;
|
||||
player.embed.ratio = [width, height];
|
||||
setAspectRatio.call(this);
|
||||
});
|
||||
|
||||
|
||||
+36
-51
@@ -7,8 +7,8 @@ import { createElement, replaceElement, toggleClass } from '../utils/elements';
|
||||
import { triggerEvent } from '../utils/events';
|
||||
import fetch from '../utils/fetch';
|
||||
import is from '../utils/is';
|
||||
import loadImage from '../utils/loadImage';
|
||||
import loadScript from '../utils/loadScript';
|
||||
import loadImage from '../utils/load-image';
|
||||
import loadScript from '../utils/load-script';
|
||||
import { extend } from '../utils/objects';
|
||||
import { format, generateId } from '../utils/strings';
|
||||
import { setAspectRatio } from '../utils/style';
|
||||
@@ -52,73 +52,60 @@ const youtube = {
|
||||
// Add embed class for responsive
|
||||
toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
|
||||
|
||||
// Set aspect ratio
|
||||
setAspectRatio.call(this);
|
||||
|
||||
// Setup API
|
||||
if (is.object(window.YT) && is.function(window.YT.Player)) {
|
||||
youtube.ready.call(this);
|
||||
} else {
|
||||
// Load the API
|
||||
loadScript(this.config.urls.youtube.sdk).catch(error => {
|
||||
this.debug.warn('YouTube API failed to load', error);
|
||||
});
|
||||
|
||||
// Setup callback for the API
|
||||
// YouTube has it's own system of course...
|
||||
window.onYouTubeReadyCallbacks = window.onYouTubeReadyCallbacks || [];
|
||||
|
||||
// Add to queue
|
||||
window.onYouTubeReadyCallbacks.push(() => {
|
||||
youtube.ready.call(this);
|
||||
});
|
||||
// Reference current global callback
|
||||
const callback = window.onYouTubeIframeAPIReady;
|
||||
|
||||
// Set callback to process queue
|
||||
window.onYouTubeIframeAPIReady = () => {
|
||||
window.onYouTubeReadyCallbacks.forEach(callback => {
|
||||
// Call global callback if set
|
||||
if (is.function(callback)) {
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
youtube.ready.call(this);
|
||||
};
|
||||
|
||||
// Load the SDK
|
||||
loadScript(this.config.urls.youtube.sdk).catch(error => {
|
||||
this.debug.warn('YouTube API failed to load', error);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Get the media title
|
||||
getTitle(videoId) {
|
||||
// Try via undocumented API method first
|
||||
// This method disappears now and then though...
|
||||
// https://github.com/sampotts/plyr/issues/709
|
||||
if (is.function(this.embed.getVideoData)) {
|
||||
const { title } = this.embed.getVideoData();
|
||||
const url = format(this.config.urls.youtube.api, videoId);
|
||||
|
||||
if (is.empty(title)) {
|
||||
this.config.title = title;
|
||||
ui.setTitle.call(this);
|
||||
return;
|
||||
}
|
||||
}
|
||||
fetch(url)
|
||||
.then(data => {
|
||||
if (is.object(data)) {
|
||||
const { title, height, width } = data;
|
||||
|
||||
// Or via Google API
|
||||
const key = this.config.keys.google;
|
||||
if (is.string(key) && !is.empty(key)) {
|
||||
const url = format(this.config.urls.youtube.api, videoId, key);
|
||||
// Set title
|
||||
this.config.title = title;
|
||||
ui.setTitle.call(this);
|
||||
|
||||
fetch(url)
|
||||
.then(result => {
|
||||
if (is.object(result)) {
|
||||
this.config.title = result.items[0].snippet.title;
|
||||
ui.setTitle.call(this);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
// Set aspect ratio
|
||||
this.embed.ratio = [width, height];
|
||||
}
|
||||
|
||||
setAspectRatio.call(this);
|
||||
})
|
||||
.catch(() => {
|
||||
// Set aspect ratio
|
||||
setAspectRatio.call(this);
|
||||
});
|
||||
},
|
||||
|
||||
// API ready
|
||||
ready() {
|
||||
const player = this;
|
||||
|
||||
// Ignore already setup (race condition)
|
||||
const currentId = player.media.getAttribute('id');
|
||||
const currentId = player.media && player.media.getAttribute('id');
|
||||
if (!is.empty(currentId) && currentId.startsWith('youtube-')) {
|
||||
return;
|
||||
}
|
||||
@@ -134,25 +121,23 @@ const youtube = {
|
||||
// Replace the <iframe> with a <div> due to YouTube API issues
|
||||
const videoId = parseId(source);
|
||||
const id = generateId(player.provider);
|
||||
|
||||
// Get poster, if already set
|
||||
const { poster } = player;
|
||||
|
||||
// Replace media element
|
||||
const container = createElement('div', { id, poster });
|
||||
player.media = replaceElement(container, player.media);
|
||||
|
||||
// Id to poster wrapper
|
||||
const posterSrc = format => `https://i.ytimg.com/vi/${videoId}/${format}default.jpg`;
|
||||
const posterSrc = s => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`;
|
||||
|
||||
// Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)
|
||||
loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded
|
||||
.catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3
|
||||
.catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists
|
||||
.then(image => ui.setPoster.call(player, image.src))
|
||||
.then(posterSrc => {
|
||||
.then(src => {
|
||||
// If the image is padded, use background-size "cover" instead (like youtube does too with their posters)
|
||||
if (!posterSrc.includes('maxres')) {
|
||||
if (!src.includes('maxres')) {
|
||||
player.elements.poster.style.backgroundSize = 'cover';
|
||||
}
|
||||
})
|
||||
|
||||
+69
-26
@@ -1,6 +1,6 @@
|
||||
// ==========================================================================
|
||||
// Plyr
|
||||
// plyr.js v3.5.3
|
||||
// plyr.js v3.5.6
|
||||
// https://github.com/sampotts/plyr
|
||||
// License: The MIT License (MIT)
|
||||
// ==========================================================================
|
||||
@@ -15,7 +15,7 @@ import Fullscreen from './fullscreen';
|
||||
import Listeners from './listeners';
|
||||
import media from './media';
|
||||
import Ads from './plugins/ads';
|
||||
import PreviewThumbnails from './plugins/previewThumbnails';
|
||||
import PreviewThumbnails from './plugins/preview-thumbnails';
|
||||
import source from './source';
|
||||
import Storage from './storage';
|
||||
import support from './support';
|
||||
@@ -24,7 +24,8 @@ import { closest } from './utils/arrays';
|
||||
import { createElement, hasClass, removeElement, replaceElement, toggleClass, wrap } from './utils/elements';
|
||||
import { off, on, once, triggerEvent, unbindListeners } from './utils/events';
|
||||
import is from './utils/is';
|
||||
import loadSprite from './utils/loadSprite';
|
||||
import loadSprite from './utils/load-sprite';
|
||||
import { clamp } from './utils/numbers';
|
||||
import { cloneDeep, extend } from './utils/objects';
|
||||
import { getAspectRatio, reduceAspectRatio, setAspectRatio, validateRatio } from './utils/style';
|
||||
import { parseUrl } from './utils/urls';
|
||||
@@ -150,7 +151,6 @@ class Plyr {
|
||||
// Set media type based on tag or data attribute
|
||||
// Supported: video, audio, vimeo, youtube
|
||||
const type = this.media.tagName.toLowerCase();
|
||||
|
||||
// Embed properties
|
||||
let iframe = null;
|
||||
let url = null;
|
||||
@@ -323,27 +323,27 @@ class Plyr {
|
||||
* Types and provider helpers
|
||||
*/
|
||||
get isHTML5() {
|
||||
return Boolean(this.provider === providers.html5);
|
||||
return this.provider === providers.html5;
|
||||
}
|
||||
|
||||
get isEmbed() {
|
||||
return Boolean(this.isYouTube || this.isVimeo);
|
||||
return this.isYouTube || this.isVimeo;
|
||||
}
|
||||
|
||||
get isYouTube() {
|
||||
return Boolean(this.provider === providers.youtube);
|
||||
return this.provider === providers.youtube;
|
||||
}
|
||||
|
||||
get isVimeo() {
|
||||
return Boolean(this.provider === providers.vimeo);
|
||||
return this.provider === providers.vimeo;
|
||||
}
|
||||
|
||||
get isVideo() {
|
||||
return Boolean(this.type === types.video);
|
||||
return this.type === types.video;
|
||||
}
|
||||
|
||||
get isAudio() {
|
||||
return Boolean(this.type === types.audio);
|
||||
return this.type === types.audio;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -513,7 +513,6 @@ class Plyr {
|
||||
get duration() {
|
||||
// Faux duration set via config
|
||||
const fauxDuration = parseFloat(this.config.duration);
|
||||
|
||||
// Media duration can be NaN or Infinity before the media has loaded
|
||||
const realDuration = (this.media || {}).duration;
|
||||
const duration = !is.number(realDuration) || realDuration === Infinity ? 0 : realDuration;
|
||||
@@ -661,24 +660,17 @@ class Plyr {
|
||||
speed = this.config.speed.selected;
|
||||
}
|
||||
|
||||
// Set min/max
|
||||
if (speed < 0.1) {
|
||||
speed = 0.1;
|
||||
}
|
||||
if (speed > 2.0) {
|
||||
speed = 2.0;
|
||||
}
|
||||
|
||||
if (!this.config.speed.options.includes(speed)) {
|
||||
this.debug.warn(`Unsupported speed (${speed})`);
|
||||
return;
|
||||
}
|
||||
// Clamp to min/max
|
||||
const { minimumSpeed: min, maximumSpeed: max } = this;
|
||||
speed = clamp(speed, min, max);
|
||||
|
||||
// Update config
|
||||
this.config.speed.selected = speed;
|
||||
|
||||
// Set media speed
|
||||
this.media.playbackRate = speed;
|
||||
setTimeout(() => {
|
||||
this.media.playbackRate = speed;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -688,6 +680,42 @@ class Plyr {
|
||||
return Number(this.media.playbackRate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the minimum allowed speed
|
||||
*/
|
||||
get minimumSpeed() {
|
||||
if (this.isYouTube) {
|
||||
// https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate
|
||||
return Math.min(...this.options.speed);
|
||||
}
|
||||
|
||||
if (this.isVimeo) {
|
||||
// https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror
|
||||
return 0.5;
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/32320020/1191319
|
||||
return 0.0625;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the maximum allowed speed
|
||||
*/
|
||||
get maximumSpeed() {
|
||||
if (this.isYouTube) {
|
||||
// https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate
|
||||
return Math.max(...this.options.speed);
|
||||
}
|
||||
|
||||
if (this.isVimeo) {
|
||||
// https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror
|
||||
return 2;
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/32320020/1191319
|
||||
return 16;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set playback quality
|
||||
* Currently HTML5 & YouTube only
|
||||
@@ -823,6 +851,19 @@ class Plyr {
|
||||
return is.url(download) ? download : this.source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the download URL
|
||||
*/
|
||||
set download(input) {
|
||||
if (!is.url(input)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.config.urls.download = input;
|
||||
|
||||
controls.setDownloadUrl.call(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the poster image for a video
|
||||
* @param {String} input - the URL for the new poster image
|
||||
@@ -851,6 +892,10 @@ class Plyr {
|
||||
* Get the current aspect ratio in use
|
||||
*/
|
||||
get ratio() {
|
||||
if (!this.isVideo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ratio = reduceAspectRatio(getAspectRatio.call(this));
|
||||
|
||||
return is.array(ratio) ? ratio.join(':') : ratio;
|
||||
@@ -998,10 +1043,8 @@ class Plyr {
|
||||
if (this.supported.ui && !this.isAudio) {
|
||||
// Get state before change
|
||||
const isHidden = hasClass(this.elements.container, this.config.classNames.hideControls);
|
||||
|
||||
// Negate the argument if not undefined since adding the class to hides the controls
|
||||
const force = typeof toggle === 'undefined' ? undefined : !toggle;
|
||||
|
||||
// Apply and get updated state
|
||||
const hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force);
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
// ==========================================================================
|
||||
// Plyr Polyfilled Build
|
||||
// plyr.js v3.5.3
|
||||
// plyr.js v3.5.6
|
||||
// https://github.com/sampotts/plyr
|
||||
// License: The MIT License (MIT)
|
||||
// ==========================================================================
|
||||
|
||||
import 'custom-event-polyfill';
|
||||
import 'url-polyfill';
|
||||
|
||||
import Plyr from './plyr';
|
||||
|
||||
export default Plyr;
|
||||
|
||||
+27
-15
@@ -10,7 +10,7 @@ import { getElement, toggleClass } from './utils/elements';
|
||||
import { ready, triggerEvent } from './utils/events';
|
||||
import i18n from './utils/i18n';
|
||||
import is from './utils/is';
|
||||
import loadImage from './utils/loadImage';
|
||||
import loadImage from './utils/load-image';
|
||||
|
||||
const ui = {
|
||||
addStyleHook() {
|
||||
@@ -67,15 +67,15 @@ const ui = {
|
||||
// Reset mute state
|
||||
this.muted = null;
|
||||
|
||||
// Reset speed
|
||||
this.speed = null;
|
||||
|
||||
// Reset loop state
|
||||
this.loop = null;
|
||||
|
||||
// Reset quality setting
|
||||
this.quality = null;
|
||||
|
||||
// Reset speed
|
||||
this.speed = null;
|
||||
|
||||
// Reset volume display
|
||||
controls.updateVolume.call(this);
|
||||
|
||||
@@ -213,7 +213,7 @@ const ui = {
|
||||
|
||||
// Set state
|
||||
Array.from(this.elements.buttons.play || []).forEach(target => {
|
||||
target.pressed = this.playing;
|
||||
Object.assign(target, { pressed: this.playing });
|
||||
});
|
||||
|
||||
// Only update controls on non timeupdate events
|
||||
@@ -233,25 +233,37 @@ const ui = {
|
||||
clearTimeout(this.timers.loading);
|
||||
|
||||
// Timer to prevent flicker when seeking
|
||||
this.timers.loading = setTimeout(() => {
|
||||
// Update progress bar loading class state
|
||||
toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
|
||||
this.timers.loading = setTimeout(
|
||||
() => {
|
||||
// Update progress bar loading class state
|
||||
toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
|
||||
|
||||
// Update controls visibility
|
||||
ui.toggleControls.call(this);
|
||||
}, this.loading ? 250 : 0);
|
||||
// Update controls visibility
|
||||
ui.toggleControls.call(this);
|
||||
},
|
||||
this.loading ? 250 : 0,
|
||||
);
|
||||
},
|
||||
|
||||
// Toggle controls based on state and `force` argument
|
||||
toggleControls(force) {
|
||||
const { controls } = this.elements;
|
||||
const { controls: controlsElement } = this.elements;
|
||||
|
||||
if (controls && this.config.hideControls) {
|
||||
if (controlsElement && this.config.hideControls) {
|
||||
// Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)
|
||||
const recentTouchSeek = (this.touch && this.lastSeekTime + 2000 > Date.now());
|
||||
const recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now();
|
||||
|
||||
// Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide
|
||||
this.toggleControls(Boolean(force || this.loading || this.paused || controls.pressed || controls.hover || recentTouchSeek));
|
||||
this.toggleControls(
|
||||
Boolean(
|
||||
force ||
|
||||
this.loading ||
|
||||
this.paused ||
|
||||
controlsElement.pressed ||
|
||||
controlsElement.hover ||
|
||||
recentTouchSeek,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// Animation utils
|
||||
// ==========================================================================
|
||||
|
||||
import { toggleHidden } from './elements';
|
||||
import is from './is';
|
||||
|
||||
export const transitionEndEvent = (() => {
|
||||
@@ -21,14 +20,19 @@ export const transitionEndEvent = (() => {
|
||||
})();
|
||||
|
||||
// Force repaint of element
|
||||
export function repaint(element) {
|
||||
export function repaint(element, delay) {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
toggleHidden(element, true);
|
||||
element.offsetHeight; // eslint-disable-line
|
||||
toggleHidden(element, false);
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
element.hidden = true;
|
||||
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
element.offsetHeight;
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
element.hidden = false;
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
}
|
||||
}, 0);
|
||||
}, delay);
|
||||
}
|
||||
|
||||
+12
-17
@@ -4,6 +4,7 @@
|
||||
|
||||
import { toggleListener } from './events';
|
||||
import is from './is';
|
||||
import { extend } from './objects';
|
||||
|
||||
// Wrap an element
|
||||
export function wrap(elements, wrapper) {
|
||||
@@ -16,7 +17,6 @@ export function wrap(elements, wrapper) {
|
||||
.reverse()
|
||||
.forEach((element, index) => {
|
||||
const child = index > 0 ? wrapper.cloneNode(true) : wrapper;
|
||||
|
||||
// Cache the current parent and sibling.
|
||||
const parent = element.parentNode;
|
||||
const sibling = element.nextSibling;
|
||||
@@ -137,30 +137,28 @@ export function getAttributesFromSelector(sel, existingAttributes) {
|
||||
}
|
||||
|
||||
const attributes = {};
|
||||
const existing = existingAttributes;
|
||||
const existing = extend({}, existingAttributes);
|
||||
|
||||
sel.split(',').forEach(s => {
|
||||
// Remove whitespace
|
||||
const selector = s.trim();
|
||||
const className = selector.replace('.', '');
|
||||
const stripped = selector.replace(/[[\]]/g, '');
|
||||
|
||||
// Get the parts and value
|
||||
const parts = stripped.split('=');
|
||||
const key = parts[0];
|
||||
const [key] = parts;
|
||||
const value = parts.length > 1 ? parts[1].replace(/["']/g, '') : '';
|
||||
|
||||
// Get the first character
|
||||
const start = selector.charAt(0);
|
||||
|
||||
switch (start) {
|
||||
case '.':
|
||||
// Add to existing classname
|
||||
if (is.object(existing) && is.string(existing.class)) {
|
||||
existing.class += ` ${className}`;
|
||||
if (is.string(existing.class)) {
|
||||
attributes.class = `${existing.class} ${className}`;
|
||||
} else {
|
||||
attributes.class = className;
|
||||
}
|
||||
|
||||
attributes.class = className;
|
||||
break;
|
||||
|
||||
case '#':
|
||||
@@ -179,7 +177,7 @@ export function getAttributesFromSelector(sel, existingAttributes) {
|
||||
}
|
||||
});
|
||||
|
||||
return attributes;
|
||||
return extend(existing, attributes);
|
||||
}
|
||||
|
||||
// Toggle hidden
|
||||
@@ -194,11 +192,8 @@ export function toggleHidden(element, hidden) {
|
||||
hide = !element.hidden;
|
||||
}
|
||||
|
||||
if (hide) {
|
||||
element.setAttribute('hidden', '');
|
||||
} else {
|
||||
element.removeAttribute('hidden');
|
||||
}
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
element.hidden = hide;
|
||||
}
|
||||
|
||||
// Mirror Element.classList.toggle, with IE compatibility for "force" argument
|
||||
@@ -233,14 +228,14 @@ export function matches(element, selector) {
|
||||
return Array.from(document.querySelectorAll(selector)).includes(this);
|
||||
}
|
||||
|
||||
const matches =
|
||||
const method =
|
||||
prototype.matches ||
|
||||
prototype.webkitMatchesSelector ||
|
||||
prototype.mozMatchesSelector ||
|
||||
prototype.msMatchesSelector ||
|
||||
match;
|
||||
|
||||
return matches.call(element, selector);
|
||||
return method.call(element, selector);
|
||||
}
|
||||
|
||||
// Find all elements
|
||||
|
||||
@@ -35,7 +35,6 @@ export function toggleListener(element, event, callback, toggle = false, passive
|
||||
|
||||
// Allow multiple events
|
||||
const events = event.split(' ');
|
||||
|
||||
// Build options
|
||||
// Default to just the capture boolean for browsers with no passive listener support
|
||||
let options = capture;
|
||||
|
||||
@@ -36,8 +36,8 @@ const i18n = {
|
||||
'{title}': config.title,
|
||||
};
|
||||
|
||||
Object.entries(replace).forEach(([key, value]) => {
|
||||
string = replaceAll(string, key, value);
|
||||
Object.entries(replace).forEach(([k, v]) => {
|
||||
string = replaceAll(string, k, v);
|
||||
});
|
||||
|
||||
return string;
|
||||
|
||||
@@ -15,10 +15,10 @@ export default function loadSprite(url, id) {
|
||||
const prefix = 'cache';
|
||||
const hasId = is.string(id);
|
||||
let isCached = false;
|
||||
|
||||
const exists = () => document.getElementById(id) !== null;
|
||||
|
||||
const update = (container, data) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
container.innerHTML = data;
|
||||
|
||||
// Check again incase of race condition
|
||||
@@ -33,7 +33,6 @@ export default function loadSprite(url, id) {
|
||||
// Only load once if ID set
|
||||
if (!hasId || !exists()) {
|
||||
const useStorage = Storage.supported;
|
||||
|
||||
// Create container
|
||||
const container = document.createElement('div');
|
||||
container.setAttribute('hidden', '');
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Returns a number whose value is limited to the given range.
|
||||
*
|
||||
* Example: limit the output of this computation to between 0 and 255
|
||||
* (x * 255).clamp(0, 255)
|
||||
*
|
||||
* @param {Number} input
|
||||
* @param {Number} min The lower boundary of the output range
|
||||
* @param {Number} max The upper boundary of the output range
|
||||
* @returns A number in the range [min, max]
|
||||
* @type Number
|
||||
*/
|
||||
export function clamp(input = 0, min = 0, max = 255) {
|
||||
return Math.min(Math.max(input, min), max);
|
||||
}
|
||||
|
||||
export default { clamp };
|
||||
+10
-12
@@ -27,15 +27,8 @@ export function reduceAspectRatio(ratio) {
|
||||
}
|
||||
|
||||
export function getAspectRatio(input) {
|
||||
const parse = ratio => {
|
||||
if (!validateRatio(ratio)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ratio.split(':').map(Number);
|
||||
};
|
||||
|
||||
// Provided ratio
|
||||
const parse = ratio => (validateRatio(ratio) ? ratio.split(':').map(Number) : null);
|
||||
// Try provided ratio
|
||||
let ratio = parse(input);
|
||||
|
||||
// Get from config
|
||||
@@ -44,8 +37,14 @@ export function getAspectRatio(input) {
|
||||
}
|
||||
|
||||
// Get from embed
|
||||
if (ratio === null && !is.empty(this.embed) && is.string(this.embed.ratio)) {
|
||||
ratio = parse(this.embed.ratio);
|
||||
if (ratio === null && !is.empty(this.embed) && is.array(this.embed.ratio)) {
|
||||
({ ratio } = this.embed);
|
||||
}
|
||||
|
||||
// Get from HTML5 video
|
||||
if (ratio === null && this.isHTML5) {
|
||||
const { videoWidth, videoHeight } = this.media;
|
||||
ratio = reduceAspectRatio([videoWidth, videoHeight]);
|
||||
}
|
||||
|
||||
return ratio;
|
||||
@@ -58,7 +57,6 @@ export function setAspectRatio(input) {
|
||||
}
|
||||
|
||||
const ratio = getAspectRatio.call(this, input);
|
||||
|
||||
const [w, h] = is.array(ratio) ? ratio : [0, 0];
|
||||
const padding = (100 / w) * h;
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ export function formatTime(time = 0, displayHours = false, inverted = false) {
|
||||
|
||||
// Format time component to add leading zero
|
||||
const format = value => `0${value}`.slice(-2);
|
||||
|
||||
// Breakdown to hours, mins, secs
|
||||
let hours = getHours(time);
|
||||
const mins = getMinutes(time);
|
||||
|
||||
@@ -63,10 +63,6 @@ a.plyr__control {
|
||||
|
||||
// Video control
|
||||
.plyr--video .plyr__control {
|
||||
svg {
|
||||
filter: drop-shadow(0 1px 1px rgba(#000, 0.15));
|
||||
}
|
||||
|
||||
// Hover and tab focus
|
||||
&.plyr__tab-focus,
|
||||
&:hover,
|
||||
@@ -81,7 +77,6 @@ a.plyr__control {
|
||||
background: rgba($plyr-video-control-bg-hover, 0.8);
|
||||
border: 0;
|
||||
border-radius: 100%;
|
||||
box-shadow: 0 1px 1px rgba(#000, 0.15);
|
||||
color: $plyr-video-control-color;
|
||||
display: none;
|
||||
left: 50%;
|
||||
|
||||
@@ -14,42 +14,47 @@
|
||||
justify-content: flex-end;
|
||||
text-align: center;
|
||||
|
||||
.plyr__progress__container {
|
||||
flex: 1;
|
||||
min-width: 0; // Fix for Edge issue where content would overflow
|
||||
}
|
||||
|
||||
// Spacing
|
||||
> .plyr__control,
|
||||
.plyr__progress,
|
||||
.plyr__time,
|
||||
.plyr__menu,
|
||||
.plyr__volume {
|
||||
margin-left: ($plyr-control-spacing / 2);
|
||||
}
|
||||
.plyr__controls__item {
|
||||
margin-left: ($plyr-control-spacing / 4);
|
||||
|
||||
.plyr__menu + .plyr__control,
|
||||
> .plyr__control + .plyr__menu,
|
||||
> .plyr__control + .plyr__control,
|
||||
.plyr__progress + .plyr__control {
|
||||
margin-left: floor($plyr-control-spacing / 4);
|
||||
}
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
> .plyr__control:first-child,
|
||||
> .plyr__control:first-child + [data-plyr='pause'] {
|
||||
margin-left: 0;
|
||||
margin-right: auto;
|
||||
&.plyr__progress__container {
|
||||
padding-left: ($plyr-control-spacing / 4);
|
||||
}
|
||||
|
||||
&.plyr__time {
|
||||
padding: 0 ($plyr-control-spacing / 2);
|
||||
}
|
||||
|
||||
&.plyr__progress__container:first-child,
|
||||
&.plyr__time:first-child,
|
||||
&.plyr__time + .plyr__time {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
&.plyr__volume {
|
||||
padding-right: ($plyr-control-spacing / 2);
|
||||
}
|
||||
|
||||
&.plyr__volume:first-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Hide empty controls
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: $plyr-bp-sm) {
|
||||
> .plyr__control,
|
||||
.plyr__menu,
|
||||
.plyr__progress,
|
||||
.plyr__time,
|
||||
.plyr__volume {
|
||||
margin-left: $plyr-control-spacing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Audio controls
|
||||
@@ -62,10 +67,7 @@
|
||||
|
||||
// Video controls
|
||||
.plyr--video .plyr__controls {
|
||||
background: linear-gradient(
|
||||
rgba($plyr-video-controls-bg, 0),
|
||||
rgba($plyr-video-controls-bg, 0.7)
|
||||
);
|
||||
background: linear-gradient(rgba($plyr-video-controls-bg, 0), rgba($plyr-video-controls-bg, 0.7));
|
||||
border-bottom-left-radius: inherit;
|
||||
border-bottom-right-radius: inherit;
|
||||
bottom: 0;
|
||||
|
||||
@@ -2,18 +2,19 @@
|
||||
// Playback progress
|
||||
// --------------------------------------------------------------
|
||||
|
||||
// Offset the range thumb in order to be able to calculate the relative progress (#954)
|
||||
$plyr-progress-offset: $plyr-range-thumb-height;
|
||||
|
||||
.plyr__progress {
|
||||
flex: 1;
|
||||
left: $plyr-range-thumb-height / 2;
|
||||
margin-right: $plyr-range-thumb-height;
|
||||
left: $plyr-progress-offset / 2;
|
||||
margin-right: $plyr-progress-offset;
|
||||
position: relative;
|
||||
|
||||
input[type='range'],
|
||||
&__buffer {
|
||||
margin-left: -($plyr-range-thumb-height / 2);
|
||||
margin-right: -($plyr-range-thumb-height / 2);
|
||||
// Offset the range thumb in order to be able to calculate the relative progress (#954)
|
||||
width: calc(100% + #{$plyr-range-thumb-height});
|
||||
margin-left: -($plyr-progress-offset / 2);
|
||||
margin-right: -($plyr-progress-offset / 2);
|
||||
width: calc(100% + #{$plyr-progress-offset});
|
||||
}
|
||||
|
||||
input[type='range'] {
|
||||
|
||||
@@ -62,12 +62,13 @@
|
||||
|
||||
.plyr__video-wrapper {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: static;
|
||||
}
|
||||
|
||||
// Vimeo requires some different styling
|
||||
&.plyr--vimeo .plyr__video-wrapper {
|
||||
height: 0;
|
||||
position: relative;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
// The countdown label
|
||||
&::after {
|
||||
background: rgba($plyr-color-gunmetal, 0.8);
|
||||
background: rgba($plyr-color-gray-9, 0.8);
|
||||
border-radius: 2px;
|
||||
bottom: $plyr-control-spacing;
|
||||
color: #fff;
|
||||
|
||||
@@ -7,7 +7,7 @@ $plyr-preview-bg: $plyr-tooltip-bg !default;
|
||||
$plyr-preview-radius: $plyr-tooltip-radius !default;
|
||||
$plyr-preview-shadow: $plyr-tooltip-shadow !default;
|
||||
$plyr-preview-arrow-size: $plyr-tooltip-arrow-size !default;
|
||||
$plyr-preview-image-bg: $plyr-color-heather !default;
|
||||
$plyr-preview-image-bg: $plyr-color-gray-2 !default;
|
||||
$plyr-preview-time-font-size: $plyr-font-size-time !default;
|
||||
$plyr-preview-time-padding: 3px 6px !default;
|
||||
$plyr-preview-time-bg: rgba(0, 0, 0, 0.55);
|
||||
+1
-1
@@ -41,7 +41,7 @@
|
||||
@import 'states/fullscreen';
|
||||
|
||||
@import 'plugins/ads';
|
||||
@import 'plugins/previewThumbnails';
|
||||
@import 'plugins/preview-thumbnails';
|
||||
|
||||
@import 'utils/animation';
|
||||
@import 'utils/hidden';
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
// Badges
|
||||
// ==========================================================================
|
||||
|
||||
$plyr-badge-bg: $plyr-color-fiord !default;
|
||||
$plyr-badge-bg: $plyr-color-gray-7 !default;
|
||||
$plyr-badge-color: #fff !default;
|
||||
|
||||
@@ -2,8 +2,16 @@
|
||||
// Colors
|
||||
// ==========================================================================
|
||||
|
||||
$plyr-color-main: #1aafff !default;
|
||||
$plyr-color-gunmetal: #2f343d !default;
|
||||
$plyr-color-fiord: #4f5b5f !default;
|
||||
$plyr-color-lynch: #6b7d85 !default;
|
||||
$plyr-color-heather: #b7c5cd !default;
|
||||
$plyr-color-main: hsl(198, 100%, 50%) !default;
|
||||
|
||||
// Grayscale
|
||||
$plyr-color-gray-9: hsl(210, 15%, 16%);
|
||||
$plyr-color-gray-8: lighten($plyr-color-gray-9, 9%);
|
||||
$plyr-color-gray-7: lighten($plyr-color-gray-8, 9%);
|
||||
$plyr-color-gray-6: lighten($plyr-color-gray-7, 9%);
|
||||
$plyr-color-gray-5: lighten($plyr-color-gray-6, 9%);
|
||||
$plyr-color-gray-4: lighten($plyr-color-gray-5, 9%);
|
||||
$plyr-color-gray-3: lighten($plyr-color-gray-4, 9%);
|
||||
$plyr-color-gray-2: lighten($plyr-color-gray-3, 9%);
|
||||
$plyr-color-gray-1: lighten($plyr-color-gray-2, 9%);
|
||||
$plyr-color-gray-0: lighten($plyr-color-gray-1, 9%);
|
||||
|
||||
@@ -13,6 +13,6 @@ $plyr-video-control-color-hover: #fff !default;
|
||||
$plyr-video-control-bg-hover: $plyr-color-main !default;
|
||||
|
||||
$plyr-audio-controls-bg: #fff !default;
|
||||
$plyr-audio-control-color: $plyr-color-fiord !default;
|
||||
$plyr-audio-control-color: $plyr-color-gray-7 !default;
|
||||
$plyr-audio-control-color-hover: #fff !default;
|
||||
$plyr-audio-control-bg-hover: $plyr-color-main !default;
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
// ==========================================================================
|
||||
|
||||
$plyr-menu-bg: rgba(#fff, 0.9) !default;
|
||||
$plyr-menu-color: $plyr-color-fiord !default;
|
||||
$plyr-menu-color: $plyr-color-gray-7 !default;
|
||||
$plyr-menu-arrow-size: 6px !default;
|
||||
$plyr-menu-border-color: $plyr-color-heather !default;
|
||||
$plyr-menu-border-color: $plyr-color-gray-2 !default;
|
||||
$plyr-menu-border-shadow-color: #fff !default;
|
||||
$plyr-menu-shadow: 0 1px 2px rgba(#000, 0.15) !default;
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
// Loading
|
||||
$plyr-progress-loading-size: 25px !default;
|
||||
$plyr-progress-loading-bg: rgba($plyr-color-gunmetal, 0.6) !default;
|
||||
$plyr-progress-loading-bg: rgba($plyr-color-gray-9, 0.6) !default;
|
||||
|
||||
// Buffered
|
||||
$plyr-video-progress-buffered-bg: rgba(#fff, 0.25) !default;
|
||||
$plyr-audio-progress-buffered-bg: rgba($plyr-color-heather, 0.66) !default;
|
||||
$plyr-audio-progress-buffered-bg: rgba($plyr-color-gray-2, 0.66) !default;
|
||||
|
||||
@@ -9,7 +9,7 @@ $plyr-range-thumb-active-shadow-width: 3px !default;
|
||||
$plyr-range-thumb-height: 13px !default;
|
||||
$plyr-range-thumb-bg: #fff !default;
|
||||
$plyr-range-thumb-border: 2px solid transparent !default;
|
||||
$plyr-range-thumb-shadow: 0 1px 1px rgba(#000, 0.15), 0 0 0 1px rgba($plyr-color-gunmetal, 0.2) !default;
|
||||
$plyr-range-thumb-shadow: 0 1px 1px rgba(#000, 0.15), 0 0 0 1px rgba($plyr-color-gray-9, 0.2) !default;
|
||||
|
||||
// Track
|
||||
$plyr-range-track-height: 5px !default;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// ==========================================================================
|
||||
|
||||
$plyr-tooltip-bg: rgba(#fff, 0.9) !default;
|
||||
$plyr-tooltip-color: $plyr-color-fiord !default;
|
||||
$plyr-tooltip-color: $plyr-color-gray-7 !default;
|
||||
$plyr-tooltip-padding: ($plyr-control-spacing / 2) !default;
|
||||
$plyr-tooltip-arrow-size: 4px !default;
|
||||
$plyr-tooltip-radius: 3px !default;
|
||||
|
||||
Reference in New Issue
Block a user