Converted to 2 space indentation

This commit is contained in:
Sam Potts
2020-04-11 16:23:14 +10:00
parent 8f5b59c18c
commit 502d5977d7
125 changed files with 10693 additions and 10797 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ root = true
[*] [*]
charset = utf-8 charset = utf-8
end_of_line = lf end_of_line = lf
indent_size = 4 indent_size = 2
indent_style = space indent_style = space
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
+5 -5
View File
@@ -1,7 +1,7 @@
{ {
"useTabs": false, "useTabs": false,
"tabWidth": 4, "tabWidth": 2,
"singleQuote": true, "singleQuote": true,
"trailingComma": "all", "trailingComma": "all",
"printWidth": 120 "printWidth": 120
} }
+22 -22
View File
@@ -1,25 +1,25 @@
{ {
"plugins": ["stylelint-selector-bem-pattern", "stylelint-scss"], "plugins": ["stylelint-selector-bem-pattern", "stylelint-scss"],
"extends": ["stylelint-config-recommended", "stylelint-config-sass-guidelines", "stylelint-config-prettier"], "extends": ["stylelint-config-recommended", "stylelint-config-sass-guidelines", "stylelint-config-prettier"],
"rules": { "rules": {
"selector-class-pattern": null, "selector-class-pattern": null,
"selector-no-qualifying-type": [ "selector-no-qualifying-type": [
true, true,
{ {
"ignore": ["attribute", "class"] "ignore": ["attribute", "class"]
} }
], ],
"string-no-newline": null, "string-no-newline": null,
"indentation": 4, "indentation": 2,
"string-quotes": "single", "string-quotes": "single",
"max-nesting-depth": 2, "max-nesting-depth": 2,
"plugin/selector-bem-pattern": { "plugin/selector-bem-pattern": {
"preset": "bem", "preset": "bem",
"componentName": "(([a-z0-9]+(?!-$)-?)+)", "componentName": "(([a-z0-9]+(?!-$)-?)+)",
"componentSelectors": { "componentSelectors": {
"initial": "\\.{componentName}(((__|--)(([a-z0-9\\[\\]'=]+(?!-$)-?)+))+)?$" "initial": "\\.{componentName}(((__|--)(([a-z0-9\\[\\]'=]+(?!-$)-?)+))+)?$"
}, },
"ignoreSelectors": [".*\\.has-.*", ".*\\.is-.*"] "ignoreSelectors": [".*\\.has-.*", ".*\\.is-.*"]
}
} }
}
} }
+1 -1
View File
File diff suppressed because one or more lines are too long
+70 -70
View File
@@ -21299,19 +21299,19 @@ typeof navigator === "object" && (function () {
return (current / max * 100).toFixed(2); return (current / max * 100).toFixed(2);
} // Replace all occurances of a string in a string } // Replace all occurances of a string in a string
function replaceAll() { var replaceAll = function replaceAll() {
var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
var find = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; var find = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
var replace = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : ''; var replace = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : '';
return input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString()); return input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString());
} // Convert to title case }; // Convert to title case
function toTitleCase() { var toTitleCase = function toTitleCase() {
var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
return input.toString().replace(/\w\S*/g, function (text) { return input.toString().replace(/\w\S*/g, function (text) {
return text.charAt(0).toUpperCase() + text.substr(1).toLowerCase(); return text.charAt(0).toUpperCase() + text.substr(1).toLowerCase();
}); });
} // Convert string to pascalCase }; // Convert string to pascalCase
function toPascalCase() { function toPascalCase() {
var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
@@ -22418,39 +22418,39 @@ typeof navigator === "object" && (function () {
// Set the looping options // Set the looping options
/* setLoopMenu() { /* setLoopMenu() {
// Menu required // Menu required
if (!is.element(this.elements.settings.panels.loop)) { if (!is.element(this.elements.settings.panels.loop)) {
return; return;
} }
const options = ['start', 'end', 'all', 'reset']; const options = ['start', 'end', 'all', 'reset'];
const list = this.elements.settings.panels.loop.querySelector('[role="menu"]'); const list = this.elements.settings.panels.loop.querySelector('[role="menu"]');
// Show the pane and tab // Show the pane and tab
toggleHidden(this.elements.settings.buttons.loop, false); toggleHidden(this.elements.settings.buttons.loop, false);
toggleHidden(this.elements.settings.panels.loop, false); toggleHidden(this.elements.settings.panels.loop, false);
// Toggle the pane and tab // Toggle the pane and tab
const toggle = !is.empty(this.loop.options); const toggle = !is.empty(this.loop.options);
controls.toggleMenuButton.call(this, 'loop', toggle); controls.toggleMenuButton.call(this, 'loop', toggle);
// Empty the menu // Empty the menu
emptyElement(list); emptyElement(list);
options.forEach(option => { options.forEach(option => {
const item = createElement('li'); const item = createElement('li');
const button = createElement( const button = createElement(
'button', 'button',
extend(getAttributesFromSelector(this.config.selectors.buttons.loop), { extend(getAttributesFromSelector(this.config.selectors.buttons.loop), {
type: 'button', type: 'button',
class: this.config.classNames.control, class: this.config.classNames.control,
'data-plyr-loop-action': option, 'data-plyr-loop-action': option,
}), }),
i18n.get(option, this.config) i18n.get(option, this.config)
); );
if (['start', 'end'].includes(option)) { if (['start', 'end'].includes(option)) {
const badge = controls.createBadge.call(this, '00:00'); const badge = controls.createBadge.call(this, '00:00');
button.appendChild(badge); button.appendChild(badge);
} }
item.appendChild(button); item.appendChild(button);
list.appendChild(item); list.appendChild(item);
}); });
}, */ }, */
// Get current selected caption language // Get current selected caption language
// TODO: rework this to user the getter in the API? // TODO: rework this to user the getter in the API?
// Set a list of available captions languages // Set a list of available captions languages
@@ -28987,41 +28987,41 @@ typeof navigator === "object" && (function () {
this.media.loop = toggle; // Set default to be a true toggle this.media.loop = toggle; // Set default to be a true toggle
/* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle'; /* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle';
switch (type) { switch (type) {
case 'start': case 'start':
if (this.config.loop.end && this.config.loop.end <= this.currentTime) { if (this.config.loop.end && this.config.loop.end <= this.currentTime) {
this.config.loop.end = null; this.config.loop.end = null;
} }
this.config.loop.start = this.currentTime; this.config.loop.start = this.currentTime;
// this.config.loop.indicator.start = this.elements.display.played.value; // this.config.loop.indicator.start = this.elements.display.played.value;
break; break;
case 'end': case 'end':
if (this.config.loop.start >= this.currentTime) { if (this.config.loop.start >= this.currentTime) {
return this; return this;
} }
this.config.loop.end = this.currentTime; this.config.loop.end = this.currentTime;
// this.config.loop.indicator.end = this.elements.display.played.value; // this.config.loop.indicator.end = this.elements.display.played.value;
break; break;
case 'all': case 'all':
this.config.loop.start = 0;
this.config.loop.end = this.duration - 2;
this.config.loop.indicator.start = 0;
this.config.loop.indicator.end = 100;
break;
case 'toggle':
if (this.config.loop.active) {
this.config.loop.start = 0;
this.config.loop.end = null;
} else {
this.config.loop.start = 0; this.config.loop.start = 0;
this.config.loop.end = this.duration - 2; this.config.loop.end = this.duration - 2;
} this.config.loop.indicator.start = 0;
break; this.config.loop.indicator.end = 100;
default: break;
this.config.loop.start = 0; case 'toggle':
this.config.loop.end = null; if (this.config.loop.active) {
break; this.config.loop.start = 0;
} */ this.config.loop.end = null;
} else {
this.config.loop.start = 0;
this.config.loop.end = this.duration - 2;
}
break;
default:
this.config.loop.start = 0;
this.config.loop.end = null;
break;
} */
} }
/** /**
* Get current loop state * Get current loop state
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+239 -248
View File
@@ -1,278 +1,269 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>Plyr - A simple, customizable HTML5 Video, Audio, YouTube and Vimeo player</title> <title>Plyr - A simple, customizable HTML5 Video, Audio, YouTube and Vimeo player</title>
<meta <meta
name="description" name="description"
property="og:description" property="og:description"
content="A simple HTML5 media player with custom controls and WebVTT captions." content="A simple HTML5 media player with custom controls and WebVTT captions."
/> />
<meta name="author" content="Sam Potts" /> <meta name="author" content="Sam Potts" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Icons --> <!-- Icons -->
<link rel="icon" href="https://cdn.plyr.io/static/icons/favicon.ico" /> <link rel="icon" href="https://cdn.plyr.io/static/icons/favicon.ico" />
<link rel="icon" type="image/png" href="https://cdn.plyr.io/static/icons/32x32.png" sizes="32x32" /> <link rel="icon" type="image/png" href="https://cdn.plyr.io/static/icons/32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="https://cdn.plyr.io/static/icons/16x16.png" sizes="16x16" /> <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" /> <link rel="apple-touch-icon" sizes="180x180" href="https://cdn.plyr.io/static/icons/180x180.png" />
<!-- Open Graph --> <!-- Open Graph -->
<meta <meta property="og:title" content="Plyr - A simple, customizable HTML5 Video, Audio, YouTube and Vimeo player" />
property="og:title" <meta property="og:site_name" content="Plyr" />
content="Plyr - A simple, customizable HTML5 Video, Audio, YouTube and Vimeo player" <meta property="og:url" content="https://plyr.io" />
/> <meta property="og:image" content="https://cdn.plyr.io/static/icons/1200x630.png" />
<meta property="og:site_name" content="Plyr" />
<meta property="og:url" content="https://plyr.io" />
<meta property="og:image" content="https://cdn.plyr.io/static/icons/1200x630.png" />
<!-- Twitter --> <!-- Twitter -->
<meta name="twitter:card" content="summary" /> <meta name="twitter:card" content="summary" />
<meta name="twitter:site" content="@sam_potts" /> <meta name="twitter:site" content="@sam_potts" />
<meta name="twitter:creator" content="@sam_potts" /> <meta name="twitter:creator" content="@sam_potts" />
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />
<!-- Docs styles --> <!-- Docs styles -->
<link rel="stylesheet" href="dist/demo.css" /> <link rel="stylesheet" href="dist/demo.css" />
<!-- Preload --> <!-- Preload -->
<link <link
rel="preload" rel="preload"
as="font" as="font"
crossorigin crossorigin
type="font/woff2" type="font/woff2"
href="https://cdn.plyr.io/static/fonts/gordita-medium.woff2" href="https://cdn.plyr.io/static/fonts/gordita-medium.woff2"
/> />
<link <link
rel="preload" rel="preload"
as="font" as="font"
crossorigin crossorigin
type="font/woff2" type="font/woff2"
href="https://cdn.plyr.io/static/fonts/gordita-bold.woff2" href="https://cdn.plyr.io/static/fonts/gordita-bold.woff2"
/> />
<!-- Google Analytics--> <!-- Google Analytics-->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-132699580-1"></script> <script async src="https://www.googletagmanager.com/gtag/js?id=UA-132699580-1"></script>
<script> <script>
window.dataLayer = window.dataLayer || []; window.dataLayer = window.dataLayer || [];
function gtag() { function gtag() {
dataLayer.push(arguments); dataLayer.push(arguments);
} }
gtag('js', new Date()); gtag('js', new Date());
gtag('config', 'UA-132699580-1'); gtag('config', 'UA-132699580-1');
</script> </script>
</head> </head>
<body> <body>
<div class="grid"> <div class="grid">
<header> <header>
<h1>Pl<span>a</span>y<span>e</span>r</h1> <h1>Pl<span>a</span>y<span>e</span>r</h1>
<p> <p>
A simple, accessible and customisable media player for A simple, accessible and customisable media player for
<button type="button" class="faux-link" data-source="video"> <button type="button" class="faux-link" data-source="video">
<svg class="icon"> <svg class="icon">
<title>HTML5</title> <title>HTML5</title>
<path <path
d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z" d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"
></path></svg ></path></svg
>Video</button >Video</button
>, >,
<button type="button" class="faux-link" data-source="audio"> <button type="button" class="faux-link" data-source="audio">
<svg class="icon"> <svg class="icon">
<title>HTML5</title> <title>HTML5</title>
<path <path
d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z" d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"
></path></svg ></path></svg
>Audio</button >Audio</button
>, >,
<button type="button" class="faux-link" data-source="youtube"> <button type="button" class="faux-link" data-source="youtube">
<svg class="icon" role="presentation"> <svg class="icon" role="presentation">
<title>YouTube</title> <title>YouTube</title>
<path <path
d="M15.8,4.8c-0.2-1.3-0.8-2.2-2.2-2.4C11.4,2,8,2,8,2S4.6,2,2.4,2.4C1,2.6,0.3,3.5,0.2,4.8C0,6.1,0,8,0,8 d="M15.8,4.8c-0.2-1.3-0.8-2.2-2.2-2.4C11.4,2,8,2,8,2S4.6,2,2.4,2.4C1,2.6,0.3,3.5,0.2,4.8C0,6.1,0,8,0,8
s0,1.9,0.2,3.2c0.2,1.3,0.8,2.2,2.2,2.4C4.6,14,8,14,8,14s3.4,0,5.6-0.4c1.4-0.3,2-1.1,2.2-2.4C16,9.9,16,8,16,8S16,6.1,15.8,4.8z s0,1.9,0.2,3.2c0.2,1.3,0.8,2.2,2.2,2.4C4.6,14,8,14,8,14s3.4,0,5.6-0.4c1.4-0.3,2-1.1,2.2-2.4C16,9.9,16,8,16,8S16,6.1,15.8,4.8z
M6,11V5l5,3L6,11z" M6,11V5l5,3L6,11z"
></path></svg ></path></svg
>YouTube >YouTube
</button> </button>
and and
<button type="button" class="faux-link" data-source="vimeo"> <button type="button" class="faux-link" data-source="vimeo">
<svg class="icon" role="presentation"> <svg class="icon" role="presentation">
<title>Vimeo</title> <title>Vimeo</title>
<path <path
d="M16,4.3c-0.1,1.6-1.2,3.7-3.3,6.4c-2.2,2.8-4,4.2-5.5,4.2c-0.9,0-1.7-0.9-2.4-2.6C4,9.9,3.4,5,2,5 d="M16,4.3c-0.1,1.6-1.2,3.7-3.3,6.4c-2.2,2.8-4,4.2-5.5,4.2c-0.9,0-1.7-0.9-2.4-2.6C4,9.9,3.4,5,2,5
C1.9,5,1.5,5.3,0.8,5.8L0,4.8c0.8-0.7,3.5-3.4,4.7-3.5C5.9,1.2,6.7,2,7,3.8c0.3,2,0.8,6.1,1.8,6.1c0.9,0,2.5-3.4,2.6-4 C1.9,5,1.5,5.3,0.8,5.8L0,4.8c0.8-0.7,3.5-3.4,4.7-3.5C5.9,1.2,6.7,2,7,3.8c0.3,2,0.8,6.1,1.8,6.1c0.9,0,2.5-3.4,2.6-4
c0.1-0.9-0.3-1.9-2.3-1.1c0.8-2.6,2.3-3.8,4.5-3.8C15.3,1.1,16.1,2.2,16,4.3z" c0.1-0.9-0.3-1.9-2.3-1.1c0.8-2.6,2.3-3.8,4.5-3.8C15.3,1.1,16.1,2.2,16,4.3z"
></path></svg ></path></svg
>Vimeo >Vimeo
</button> </button>
</p> </p>
<p> <p>
Premium video monetization from Premium video monetization from
<a href="https://vi.ai/publisher-video-monetization/?aid=plyrio" target="_blank" class="no-border"> <a href="https://vi.ai/publisher-video-monetization/?aid=plyrio" target="_blank" class="no-border">
<img src="https://cdn.plyr.io/static/vi-logo-24x24.svg" alt="ai.vi" /> <img src="https://cdn.plyr.io/static/vi-logo-24x24.svg" alt="ai.vi" />
<span class="sr-only">ai.vi</span> <span class="sr-only">ai.vi</span>
</a> </a>
</p> </p>
<div class="call-to-action"> <div class="call-to-action">
<a href="https://github.com/sampotts/plyr" target="_blank" class="button js-shr"> <a href="https://github.com/sampotts/plyr" target="_blank" class="button js-shr">
<svg class="icon" role="presentation"> <svg class="icon" role="presentation">
<title>GitHub</title> <title>GitHub</title>
<path <path
d="M8,0.2c-4.4,0-8,3.6-8,8c0,3.5,2.3,6.5,5.5,7.6 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 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 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 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.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" 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> ></path>
</svg> </svg>
Download on GitHub Download on GitHub
</a> </a>
</div> </div>
</header> </header>
<main> <main>
<!-- style="--plyr-color-main: #47bb4d; --plyr-video-control-background-hover: var(--plyr-color-main); " --> <!-- style="--plyr-color-main: #47bb4d; --plyr-video-control-background-hover: var(--plyr-color-main); " -->
<div id="container" style="--plyr-color-main: #1ac266;"> <div id="container" style="--plyr-color-main: #1ac266;">
<video <video
controls controls
crossorigin crossorigin
playsinline playsinline
poster="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg" poster="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg"
id="player" id="player"
> >
<!-- Video files --> <!-- Video files -->
<source <source
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4"
type="video/mp4" type="video/mp4"
size="576" size="576"
/> />
<source <source
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4"
type="video/mp4" type="video/mp4"
size="720" size="720"
/> />
<source <source
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4"
type="video/mp4" type="video/mp4"
size="1080" size="1080"
/> />
<!-- Caption files --> <!-- Caption files -->
<track <track
kind="captions" kind="captions"
label="English" label="English"
srclang="en" srclang="en"
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt"
default default
/> />
<track <track
kind="captions" kind="captions"
label="Français" label="Français"
srclang="fr" srclang="fr"
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt"
/> />
<!-- Fallback for browsers that don't support the <video> element --> <!-- Fallback for browsers that don't support the <video> element -->
<a href="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" download <a href="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" download>Download</a>
>Download</a </video>
>
</video>
</div>
<ul>
<li class="plyr__cite plyr__cite--video" hidden>
<small>
<svg class="icon">
<title>HTML5</title>
<path
d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"
></path>
</svg>
<a
href="https://itunes.apple.com/au/movie/view-from-a-blue-moon/id1041586323"
target="_blank"
>View From A Blue Moon</a
>
&copy; Brainfarm
</small>
</li>
<li class="plyr__cite plyr__cite--audio" hidden>
<small>
<svg class="icon" title="HTML5">
<title>HTML5</title>
<path
d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"
></path>
</svg>
<a href="http://www.kishibashi.com/" target="_blank"
>Kishi Bashi &ndash; &ldquo;It All Began With A Burst&rdquo;</a
>
&copy; Kishi Bashi
</small>
</li>
<li class="plyr__cite plyr__cite--youtube" hidden>
<small>
<a href="https://www.youtube.com/watch?v=bTqVqk7FSmY" target="_blank"
>View From A Blue Moon</a
>
on&nbsp;
<span class="color--youtube">
<svg class="icon" role="presentation">
<title>YouTube</title>
<path
d="M15.8,4.8c-0.2-1.3-0.8-2.2-2.2-2.4C11.4,2,8,2,8,2S4.6,2,2.4,2.4C1,2.6,0.3,3.5,0.2,4.8C0,6.1,0,8,0,8
s0,1.9,0.2,3.2c0.2,1.3,0.8,2.2,2.2,2.4C4.6,14,8,14,8,14s3.4,0,5.6-0.4c1.4-0.3,2-1.1,2.2-2.4C16,9.9,16,8,16,8S16,6.1,15.8,4.8z
M6,11V5l5,3L6,11z"
></path></svg
>YouTube
</span>
</small>
</li>
<li class="plyr__cite plyr__cite--vimeo" hidden>
<small>
<a href="https://vimeo.com/40648169" target="_blank">Toob “Wavaphon” Music Video</a>
on&nbsp;
<span class="color--vimeo">
<svg class="icon" role="presentation">
<title>Vimeo</title>
<path
d="M16,4.3c-0.1,1.6-1.2,3.7-3.3,6.4c-2.2,2.8-4,4.2-5.5,4.2c-0.9,0-1.7-0.9-2.4-2.6C4,9.9,3.4,5,2,5
C1.9,5,1.5,5.3,0.8,5.8L0,4.8c0.8-0.7,3.5-3.4,4.7-3.5C5.9,1.2,6.7,2,7,3.8c0.3,2,0.8,6.1,1.8,6.1c0.9,0,2.5-3.4,2.6-4
c0.1-0.9-0.3-1.9-2.3-1.1c0.8-2.6,2.3-3.8,4.5-3.8C15.3,1.1,16.1,2.2,16,4.3z"
></path></svg
>Vimeo
</span>
</small>
</li>
</ul>
</main>
</div> </div>
<aside> <ul>
<svg class="icon"> <li class="plyr__cite plyr__cite--video" hidden>
<title>Twitter</title> <small>
<svg class="icon">
<title>HTML5</title>
<path <path
d="M16,3c-0.6,0.3-1.2,0.4-1.9,0.5c0.7-0.4,1.2-1,1.4-1.8c-0.6,0.4-1.3,0.6-2.1,0.8c-0.6-0.6-1.5-1-2.4-1 d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"
></path>
</svg>
<a href="https://itunes.apple.com/au/movie/view-from-a-blue-moon/id1041586323" target="_blank"
>View From A Blue Moon</a
>
&copy; Brainfarm
</small>
</li>
<li class="plyr__cite plyr__cite--audio" hidden>
<small>
<svg class="icon" title="HTML5">
<title>HTML5</title>
<path
d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"
></path>
</svg>
<a href="http://www.kishibashi.com/" target="_blank"
>Kishi Bashi &ndash; &ldquo;It All Began With A Burst&rdquo;</a
>
&copy; Kishi Bashi
</small>
</li>
<li class="plyr__cite plyr__cite--youtube" hidden>
<small>
<a href="https://www.youtube.com/watch?v=bTqVqk7FSmY" target="_blank">View From A Blue Moon</a>
on&nbsp;
<span class="color--youtube">
<svg class="icon" role="presentation">
<title>YouTube</title>
<path
d="M15.8,4.8c-0.2-1.3-0.8-2.2-2.2-2.4C11.4,2,8,2,8,2S4.6,2,2.4,2.4C1,2.6,0.3,3.5,0.2,4.8C0,6.1,0,8,0,8
s0,1.9,0.2,3.2c0.2,1.3,0.8,2.2,2.2,2.4C4.6,14,8,14,8,14s3.4,0,5.6-0.4c1.4-0.3,2-1.1,2.2-2.4C16,9.9,16,8,16,8S16,6.1,15.8,4.8z
M6,11V5l5,3L6,11z"
></path></svg
>YouTube
</span>
</small>
</li>
<li class="plyr__cite plyr__cite--vimeo" hidden>
<small>
<a href="https://vimeo.com/40648169" target="_blank">Toob “Wavaphon” Music Video</a>
on&nbsp;
<span class="color--vimeo">
<svg class="icon" role="presentation">
<title>Vimeo</title>
<path
d="M16,4.3c-0.1,1.6-1.2,3.7-3.3,6.4c-2.2,2.8-4,4.2-5.5,4.2c-0.9,0-1.7-0.9-2.4-2.6C4,9.9,3.4,5,2,5
C1.9,5,1.5,5.3,0.8,5.8L0,4.8c0.8-0.7,3.5-3.4,4.7-3.5C5.9,1.2,6.7,2,7,3.8c0.3,2,0.8,6.1,1.8,6.1c0.9,0,2.5-3.4,2.6-4
c0.1-0.9-0.3-1.9-2.3-1.1c0.8-2.6,2.3-3.8,4.5-3.8C15.3,1.1,16.1,2.2,16,4.3z"
></path></svg
>Vimeo
</span>
</small>
</li>
</ul>
</main>
</div>
<aside>
<svg class="icon">
<title>Twitter</title>
<path
d="M16,3c-0.6,0.3-1.2,0.4-1.9,0.5c0.7-0.4,1.2-1,1.4-1.8c-0.6,0.4-1.3,0.6-2.1,0.8c-0.6-0.6-1.5-1-2.4-1
C9.3,1.5,7.8,3,7.8,4.8c0,0.3,0,0.5,0.1,0.7C5.2,5.4,2.7,4.1,1.1,2.1c-0.3,0.5-0.4,1-0.4,1.7c0,1.1,0.6,2.1,1.5,2.7 C9.3,1.5,7.8,3,7.8,4.8c0,0.3,0,0.5,0.1,0.7C5.2,5.4,2.7,4.1,1.1,2.1c-0.3,0.5-0.4,1-0.4,1.7c0,1.1,0.6,2.1,1.5,2.7
c-0.5,0-1-0.2-1.5-0.4c0,0,0,0,0,0c0,1.6,1.1,2.9,2.6,3.2C3,9.4,2.7,9.4,2.4,9.4c-0.2,0-0.4,0-0.6-0.1c0.4,1.3,1.6,2.3,3.1,2.3 c-0.5,0-1-0.2-1.5-0.4c0,0,0,0,0,0c0,1.6,1.1,2.9,2.6,3.2C3,9.4,2.7,9.4,2.4,9.4c-0.2,0-0.4,0-0.6-0.1c0.4,1.3,1.6,2.3,3.1,2.3
c-1.1,0.9-2.5,1.4-4.1,1.4c-0.3,0-0.5,0-0.8,0c1.5,0.9,3.2,1.5,5,1.5c6,0,9.3-5,9.3-9.3c0-0.1,0-0.3,0-0.4C15,4.3,15.6,3.7,16,3z" c-1.1,0.9-2.5,1.4-4.1,1.4c-0.3,0-0.5,0-0.8,0c1.5,0.9,3.2,1.5,5,1.5c6,0,9.3-5,9.3-9.3c0-0.1,0-0.3,0-0.4C15,4.3,15.6,3.7,16,3z"
></path> ></path>
</svg> </svg>
<p> <p>
If you think Plyr's good, If you think Plyr's good,
<a <a
href="https://twitter.com/intent/tweet?text=A+simple+HTML5+media+player+with+custom+controls+and+WebVTT+captions.&amp;url=http%3A%2F%2Fplyr.io&amp;via=Sam_Potts" href="https://twitter.com/intent/tweet?text=A+simple+HTML5+media+player+with+custom+controls+and+WebVTT+captions.&amp;url=http%3A%2F%2Fplyr.io&amp;via=Sam_Potts"
target="_blank" target="_blank"
class="js-shr" class="js-shr"
>tweet it</a >tweet it</a
> >
👍 👍
</p> </p>
</aside> </aside>
<script src="dist/demo.js" crossorigin="anonymous"></script> <script src="dist/demo.js" crossorigin="anonymous"></script>
</body> </body>
</html> </html>
+119 -122
View File
@@ -16,137 +16,134 @@ import sources from './sources';
import toggleClass from './toggle-class'; import toggleClass from './toggle-class';
(() => { (() => {
const production = 'plyr.io'; const production = 'plyr.io';
// Sentry for demo site (https://plyr.io) only // Sentry for demo site (https://plyr.io) only
if (window.location.host === production) { if (window.location.host === production) {
Sentry.init({ Sentry.init({
dsn: 'https://d4ad9866ad834437a4754e23937071e4@sentry.io/305555', dsn: 'https://d4ad9866ad834437a4754e23937071e4@sentry.io/305555',
whitelistUrls: [production].map(d => new RegExp(`https://(([a-z0-9])+(.))*${d}`)), whitelistUrls: [production].map(d => new RegExp(`https://(([a-z0-9])+(.))*${d}`)),
}); });
}
document.addEventListener('DOMContentLoaded', () => {
const selector = '#player';
// Setup share buttons
Shr.setup('.js-shr', {
count: {
className: 'button__count',
},
wrapper: {
className: 'button--with-count',
},
});
// Setup the player
const player = new Plyr(selector, {
debug: true,
title: 'View From A Blue Moon',
iconUrl: 'dist/demo.svg',
keyboard: {
global: true,
},
tooltips: {
controls: true,
},
captions: {
active: true,
},
ads: {
enabled: window.location.host.includes(production),
publisherId: '918848828995742',
},
previewThumbnails: {
enabled: true,
src: ['https://cdn.plyr.io/static/demo/thumbs/100p.vtt', 'https://cdn.plyr.io/static/demo/thumbs/240p.vtt'],
},
vimeo: {
// Prevent Vimeo blocking plyr.io demo site
referrerPolicy: 'no-referrer',
},
});
// Expose for tinkering in the console
window.player = player;
// Setup type toggle
const buttons = document.querySelectorAll('[data-source]');
const types = Object.keys(sources);
const historySupport = Boolean(window.history && window.history.pushState);
let currentType = window.location.hash.substring(1);
const hasInitialType = currentType.length;
function render(type) {
// Remove active classes
Array.from(buttons).forEach(button => toggleClass(button.parentElement, 'active', false));
// Set active on parent
toggleClass(document.querySelector(`[data-source="${type}"]`), 'active', true);
// Show cite
Array.from(document.querySelectorAll('.plyr__cite')).forEach(cite => {
// eslint-disable-next-line no-param-reassign
cite.hidden = true;
});
document.querySelector(`.plyr__cite--${type}`).hidden = false;
} }
document.addEventListener('DOMContentLoaded', () => { // Set a new source
const selector = '#player'; 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;
}
// Setup share buttons // Set the new source
Shr.setup('.js-shr', { player.source = sources[type];
count: {
className: 'button__count',
},
wrapper: {
className: 'button--with-count',
},
});
// Setup the player // Set the current type for next time
const player = new Plyr(selector, { currentType = type;
debug: true,
title: 'View From A Blue Moon',
iconUrl: 'dist/demo.svg',
keyboard: {
global: true,
},
tooltips: {
controls: true,
},
captions: {
active: true,
},
ads: {
enabled: window.location.host.includes(production),
publisherId: '918848828995742',
},
previewThumbnails: {
enabled: true,
src: [
'https://cdn.plyr.io/static/demo/thumbs/100p.vtt',
'https://cdn.plyr.io/static/demo/thumbs/240p.vtt',
],
},
vimeo: {
// Prevent Vimeo blocking plyr.io demo site
referrerPolicy: 'no-referrer',
},
});
// Expose for tinkering in the console render(type);
window.player = player; }
// Setup type toggle // Bind to each button
const buttons = document.querySelectorAll('[data-source]'); Array.from(buttons).forEach(button => {
const types = Object.keys(sources); button.addEventListener('click', () => {
const historySupport = Boolean(window.history && window.history.pushState); const type = button.getAttribute('data-source');
let currentType = window.location.hash.substring(1);
const hasInitialType = currentType.length;
function render(type) { setSource(type);
// Remove active classes
Array.from(buttons).forEach(button => toggleClass(button.parentElement, 'active', false));
// Set active on parent if (historySupport) {
toggleClass(document.querySelector(`[data-source="${type}"]`), 'active', true); window.history.pushState({ type }, '', `#${type}`);
// Show cite
Array.from(document.querySelectorAll('.plyr__cite')).forEach(cite => {
// eslint-disable-next-line no-param-reassign
cite.hidden = true;
});
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
Array.from(buttons).forEach(button => {
button.addEventListener('click', () => {
const type = button.getAttribute('data-source');
setSource(type);
if (historySupport) {
window.history.pushState({ type }, '', `#${type}`);
}
});
});
// List for backwards/forwards
window.addEventListener('popstate', event => {
if (event.state && Object.keys(event.state).includes('type')) {
setSource(event.state.type);
}
});
// If there's no current type set, assume video
if (!hasInitialType) {
currentType = 'video';
}
// Replace current history state
if (historySupport && types.includes(currentType)) {
window.history.replaceState({ type: currentType }, '', hasInitialType ? `#${currentType}` : '');
}
// If it's not video, load the source
if (currentType !== 'video') {
setSource(currentType, true);
}
render(currentType);
}); });
// List for backwards/forwards
window.addEventListener('popstate', event => {
if (event.state && Object.keys(event.state).includes('type')) {
setSource(event.state.type);
}
});
// If there's no current type set, assume video
if (!hasInitialType) {
currentType = 'video';
}
// Replace current history state
if (historySupport && types.includes(currentType)) {
window.history.replaceState({ type: currentType }, '', hasInitialType ? `#${currentType}` : '');
}
// If it's not video, load the source
if (currentType !== 'video') {
setSource(currentType, true);
}
render(currentType);
});
})(); })();
+74 -74
View File
@@ -1,78 +1,78 @@
const sources = { const sources = {
video: { video: {
type: 'video', type: 'video',
title: 'View From A Blue Moon', title: 'View From A Blue Moon',
sources: [ sources: [
{ {
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4', src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4',
type: 'video/mp4', type: 'video/mp4',
size: 576, size: 576,
}, },
{ {
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4', src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4',
type: 'video/mp4', type: 'video/mp4',
size: 720, size: 720,
}, },
{ {
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4', src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4',
type: 'video/mp4', type: 'video/mp4',
size: 1080, size: 1080,
}, },
{ {
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1440p.mp4', src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1440p.mp4',
type: 'video/mp4', type: 'video/mp4',
size: 1440, size: 1440,
}, },
], ],
poster: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg', poster: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg',
tracks: [ tracks: [
{ {
kind: 'captions', kind: 'captions',
label: 'English', label: 'English',
srclang: 'en', srclang: 'en',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt', src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',
default: true, default: true,
}, },
{ {
kind: 'captions', kind: 'captions',
label: 'French', label: 'French',
srclang: 'fr', srclang: 'fr',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt', src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt',
}, },
], ],
}, },
audio: { audio: {
type: 'audio', type: 'audio',
title: 'Kishi Bashi &ndash; &ldquo;It All Began With A Burst&rdquo;', title: 'Kishi Bashi &ndash; &ldquo;It All Began With A Burst&rdquo;',
sources: [ sources: [
{ {
src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.mp3', src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.mp3',
type: 'audio/mp3', type: 'audio/mp3',
}, },
{ {
src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.ogg', src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.ogg',
type: 'audio/ogg', type: 'audio/ogg',
}, },
], ],
}, },
youtube: { youtube: {
type: 'video', type: 'video',
sources: [ sources: [
{ {
src: 'https://youtube.com/watch?v=bTqVqk7FSmY', src: 'https://youtube.com/watch?v=bTqVqk7FSmY',
provider: 'youtube', provider: 'youtube',
}, },
], ],
}, },
vimeo: { vimeo: {
type: 'video', type: 'video',
sources: [ sources: [
{ {
src: 'https://vimeo.com/40648169', src: 'https://vimeo.com/40648169',
provider: 'vimeo', provider: 'vimeo',
}, },
], ],
}, },
}; };
export default sources; export default sources;
+17 -17
View File
@@ -4,28 +4,28 @@ const tabClassName = 'tab-focus';
// Remove class on blur // Remove class on blur
document.addEventListener('focusout', event => { document.addEventListener('focusout', event => {
if (!event.target.classList || container.contains(event.target)) { if (!event.target.classList || container.contains(event.target)) {
return; return;
} }
event.target.classList.remove(tabClassName); event.target.classList.remove(tabClassName);
}); });
// Add classname to tabbed elements // Add classname to tabbed elements
document.addEventListener('keydown', event => { document.addEventListener('keydown', event => {
if (event.keyCode !== 9) { if (event.keyCode !== 9) {
return; 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;
} }
// Delay the adding of classname until the focus has changed focused.classList.add(tabClassName);
// This event fires before the focusin event }, 10);
setTimeout(() => {
const focused = document.activeElement;
if (!focused || !focused.classList || container.contains(focused)) {
return;
}
focused.classList.add(tabClassName);
}, 10);
}); });
+1 -1
View File
@@ -1,5 +1,5 @@
// Toggle class on an element // Toggle class on an element
const toggleClass = (element, className = '', toggle = false) => const toggleClass = (element, className = '', toggle = false) =>
element && element.classList[toggle ? 'add' : 'remove'](className); element && element.classList[toggle ? 'add' : 'remove'](className);
export default toggleClass; export default toggleClass;
+55 -55
View File
@@ -5,80 +5,80 @@
// Shared // Shared
.button, .button,
.button__count { .button__count {
align-items: center; align-items: center;
border: 0; border: 0;
border-radius: $border-radius-base; border-radius: $border-radius-base;
box-shadow: 0 1px 1px rgba(#000, 0.1); box-shadow: 0 1px 1px rgba(#000, 0.1);
display: inline-flex; display: inline-flex;
padding: ($spacing-base * 0.75); padding: ($spacing-base * 0.75);
position: relative; position: relative;
text-shadow: none; text-shadow: none;
user-select: none; user-select: none;
vertical-align: middle; vertical-align: middle;
} }
// Buttons // Buttons
.button { .button {
background: $color-button-background; background: $color-button-background;
color: $color-button-text; color: $color-button-text;
font-weight: $font-weight-bold; font-weight: $font-weight-bold;
padding-left: ($spacing-base * 1.25); padding-left: ($spacing-base * 1.25);
padding-right: ($spacing-base * 1.25); padding-right: ($spacing-base * 1.25);
transition: all 0.2s ease; transition: all 0.2s ease;
&:hover, &:hover,
&:focus { &:focus {
background: $color-button-background-hover; background: $color-button-background-hover;
// Remove the underline/border // Remove the underline/border
&::after { &::after {
display: none; display: none;
}
} }
}
&:hover { &:hover {
box-shadow: 0 2px 2px rgba(#000, 0.1); box-shadow: 0 2px 2px rgba(#000, 0.1);
} }
&:focus { &:focus {
outline: 0; outline: 0;
} }
&.tab-focus { &.tab-focus {
@include tab-focus(); @include tab-focus();
} }
&:active { &:active {
top: 1px; top: 1px;
} }
} }
// Button group // Button group
.button--with-count { .button--with-count {
display: inline-flex; display: inline-flex;
.button .icon { .button .icon {
flex-shrink: 0; flex-shrink: 0;
} }
} }
// Count bubble // Count bubble
.button__count { .button__count {
animation: fadein 0.2s ease; animation: fadein 0.2s ease;
background: $color-button-count-background; background: $color-button-count-background;
color: $color-button-count-text; color: $color-button-count-text;
margin-left: ($spacing-base * 0.75); margin-left: ($spacing-base * 0.75);
&::before { &::before {
border: $arrow-size solid transparent; border: $arrow-size solid transparent;
border-left-width: 0; border-left-width: 0;
border-right-color: $color-button-count-background; border-right-color: $color-button-count-background;
content: ''; content: '';
height: 0; height: 0;
position: absolute; position: absolute;
right: 100%; right: 100%;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
width: 0; width: 0;
} }
} }
+22 -22
View File
@@ -3,28 +3,28 @@
// ========================================================================== // ==========================================================================
header { header {
padding-bottom: $spacing-base; padding-bottom: $spacing-base;
text-align: center; text-align: center;
h1 span { h1 span {
animation: shrinkHide 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) 2s forwards; animation: shrinkHide 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) 2s forwards;
display: inline-block; display: inline-block;
font-weight: $font-weight-light; font-weight: $font-weight-light;
opacity: 0.5; opacity: 0.5;
} }
.call-to-action { .call-to-action {
margin-top: ($spacing-base * 1.5); margin-top: ($spacing-base * 1.5);
} }
@media only screen and (min-width: $screen-md) { @media only screen and (min-width: $screen-md) {
margin-right: ($spacing-base * 3); margin-right: ($spacing-base * 3);
max-width: 360px; max-width: 360px;
padding-bottom: ($spacing-base * 2); padding-bottom: ($spacing-base * 2);
text-align: left; text-align: left;
p:first-of-type { p:first-of-type {
@include font-size($font-size-base + 1); @include font-size($font-size-base + 1);
}
} }
}
} }
+6 -6
View File
@@ -4,20 +4,20 @@
// Base size icon styles // Base size icon styles
.icon { .icon {
fill: currentColor; fill: currentColor;
height: $icon-size; height: $icon-size;
vertical-align: -3px; vertical-align: -3px;
width: $icon-size; width: $icon-size;
} }
// Within elements // Within elements
a svg, a svg,
button svg, button svg,
label svg { label svg {
pointer-events: none; pointer-events: none;
} }
a .icon, a .icon,
.btn .icon { .btn .icon {
margin-right: ($spacing-base / 2); margin-right: ($spacing-base / 2);
} }
+32 -32
View File
@@ -4,45 +4,45 @@
// Make a <button> look like an <a> // Make a <button> look like an <a>
button.faux-link { button.faux-link {
@extend a; // stylelint-disable-line @extend a; // stylelint-disable-line
@include cancel-button-styles(); @include cancel-button-styles();
} }
// Links // Links
a { a {
border-bottom: 1px dotted currentColor; border-bottom: 1px dotted currentColor;
color: $color-link; color: $color-link;
position: relative; position: relative;
text-decoration: none; text-decoration: none;
transition: all 0.2s ease; transition: all 0.2s ease;
&::after {
background: currentColor;
content: '';
height: 1px;
left: 50%;
position: absolute;
top: 100%;
transform: translateX(-50%);
transition: width 0.2s ease;
width: 0;
}
&:hover,
&:focus {
border-bottom-color: transparent;
outline: 0;
&::after { &::after {
background: currentColor; width: 100%;
content: '';
height: 1px;
left: 50%;
position: absolute;
top: 100%;
transform: translateX(-50%);
transition: width 0.2s ease;
width: 0;
} }
}
&:hover, &.tab-focus {
&:focus { @include tab-focus();
border-bottom-color: transparent; }
outline: 0;
&::after { &.no-border::after {
width: 100%; display: none;
} }
}
&.tab-focus {
@include tab-focus();
}
&.no-border::after {
display: none;
}
} }
+3 -3
View File
@@ -5,7 +5,7 @@
// Lists // Lists
ul, ul,
li { li {
list-style: none; list-style: none;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
+2 -2
View File
@@ -5,6 +5,6 @@
img, img,
video, video,
audio { audio {
max-width: 100%; max-width: 100%;
vertical-align: middle; vertical-align: middle;
} }
+3 -3
View File
@@ -3,7 +3,7 @@
// ========================================================================== // ==========================================================================
nav { nav {
display: flex; display: flex;
justify-content: center; justify-content: center;
margin-bottom: $spacing-base; margin-bottom: $spacing-base;
} }
+20 -20
View File
@@ -4,33 +4,33 @@
// Example players // Example players
.plyr { .plyr {
border-radius: $border-radius-base; border-radius: $border-radius-base;
box-shadow: 0 2px 15px rgba(#000, 0.1); box-shadow: 0 2px 15px rgba(#000, 0.1);
margin: $spacing-base auto; margin: $spacing-base auto;
&.plyr--audio { &.plyr--audio {
max-width: 480px; max-width: 480px;
} }
} }
.plyr__video-wrapper::after { .plyr__video-wrapper::after {
border: 1px solid rgba(#000, 0.15); border: 1px solid rgba(#000, 0.15);
border-radius: inherit; border-radius: inherit;
bottom: 0; bottom: 0;
content: ''; content: '';
left: 0; left: 0;
pointer-events: none; pointer-events: none;
position: absolute; position: absolute;
right: 0; right: 0;
top: 0; top: 0;
z-index: 3; z-index: 3;
} }
// Style full supported player // Style full supported player
.plyr__cite { .plyr__cite {
color: $color-gray-500; color: $color-gray-500;
.icon { .icon {
margin-right: ceil($spacing-base / 6); margin-right: ceil($spacing-base / 6);
} }
} }
+39 -39
View File
@@ -4,60 +4,60 @@
html, html,
body { body {
display: flex; display: flex;
width: 100%; width: 100%;
} }
html { html {
background: $page-background; background: $page-background;
background-attachment: fixed; background-attachment: fixed;
height: 100%; height: 100%;
} }
body { body {
align-items: center; align-items: center;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 100%; min-height: 100%;
} }
.grid { .grid {
flex: 1; flex: 1;
overflow: auto; overflow: auto;
} }
main { main {
margin: auto; margin: auto;
padding-bottom: 1px; // Collapsing margins padding-bottom: 1px; // Collapsing margins
text-align: center; text-align: center;
} }
aside { aside {
align-items: center; align-items: center;
background: #fff; background: #fff;
display: flex; display: flex;
flex-shrink: 0; flex-shrink: 0;
justify-content: center; justify-content: center;
padding: $spacing-base; padding: $spacing-base;
position: relative; position: relative;
text-align: center; text-align: center;
text-shadow: none; text-shadow: none;
width: 100%; width: 100%;
.icon { .icon {
fill: $color-twitter; fill: $color-twitter;
margin-right: ($spacing-base / 2); margin-right: ($spacing-base / 2);
} }
p { p {
margin: 0; margin: 0;
} }
a { a {
color: $color-twitter; color: $color-twitter;
&.tab-focus { &.tab-focus {
@include tab-focus($color-twitter); @include tab-focus($color-twitter);
}
} }
}
} }
+12 -12
View File
@@ -5,26 +5,26 @@
// Error page // Error page
html.error, html.error,
.error body { .error body {
height: 100%; height: 100%;
} }
html.error { html.error {
background: $page-background; background: $page-background;
background-attachment: fixed; background-attachment: fixed;
} }
.error body { .error body {
align-items: center; align-items: center;
display: flex; display: flex;
width: 100%; width: 100%;
} }
.error main { .error main {
padding: $spacing-base; padding: $spacing-base;
text-align: center; text-align: center;
width: 100%; width: 100%;
p { p {
@include font-size($font-size-large); @include font-size($font-size-large);
} }
} }
+10 -10
View File
@@ -3,17 +3,17 @@
// ========================================================================== // ==========================================================================
.grid { .grid {
margin: 0 auto; margin: 0 auto;
padding: $spacing-base; padding: $spacing-base;
@media only screen and (min-width: $screen-md) { @media only screen and (min-width: $screen-md) {
align-items: center; align-items: center;
display: flex; display: flex;
max-width: $container-max-width; max-width: $container-max-width;
width: 100%; width: 100%;
> * { > * {
flex: 1; flex: 1;
}
} }
}
} }
+17 -17
View File
@@ -4,24 +4,24 @@
// Fade // Fade
@keyframes fadein { @keyframes fadein {
0% { 0% {
opacity: 0; opacity: 0;
} }
100% { 100% {
opacity: 1; opacity: 1;
} }
} }
@keyframes shrinkHide { @keyframes shrinkHide {
0% { 0% {
opacity: 0.5; opacity: 0.5;
width: 38px; width: 38px;
} }
20% { 20% {
width: 45px; width: 45px;
} }
100% { 100% {
opacity: 0; opacity: 0;
width: 0; width: 0;
} }
} }
+30 -30
View File
@@ -3,46 +3,46 @@
// ========================================================================== // ==========================================================================
@font-face { @font-face {
font-display: swap; font-display: swap;
font-family: 'Gordita'; font-family: 'Gordita';
font-style: normal; font-style: normal;
font-weight: $font-weight-light; font-weight: $font-weight-light;
src: url('https://cdn.plyr.io/static/fonts/gordita-light.woff2') format('woff2'), src: url('https://cdn.plyr.io/static/fonts/gordita-light.woff2') format('woff2'),
url('https://cdn.plyr.io/static/fonts/gordita-light.woff') format('woff'); url('https://cdn.plyr.io/static/fonts/gordita-light.woff') format('woff');
} }
@font-face { @font-face {
font-display: swap; font-display: swap;
font-family: 'Gordita'; font-family: 'Gordita';
font-style: normal; font-style: normal;
font-weight: $font-weight-regular; font-weight: $font-weight-regular;
src: url('https://cdn.plyr.io/static/fonts/gordita-regular.woff2') format('woff2'), src: url('https://cdn.plyr.io/static/fonts/gordita-regular.woff2') format('woff2'),
url('https://cdn.plyr.io/static/fonts/gordita-regular.woff') format('woff'); url('https://cdn.plyr.io/static/fonts/gordita-regular.woff') format('woff');
} }
@font-face { @font-face {
font-display: swap; font-display: swap;
font-family: 'Gordita'; font-family: 'Gordita';
font-style: normal; font-style: normal;
font-weight: $font-weight-medium; font-weight: $font-weight-medium;
src: url('https://cdn.plyr.io/static/fonts/gordita-medium.woff2') format('woff2'), src: url('https://cdn.plyr.io/static/fonts/gordita-medium.woff2') format('woff2'),
url('https://cdn.plyr.io/static/fonts/gordita-medium.woff') format('woff'); url('https://cdn.plyr.io/static/fonts/gordita-medium.woff') format('woff');
} }
@font-face { @font-face {
font-display: swap; font-display: swap;
font-family: 'Gordita'; font-family: 'Gordita';
font-style: normal; font-style: normal;
font-weight: $font-weight-bold; font-weight: $font-weight-bold;
src: url('https://cdn.plyr.io/static/fonts/gordita-bold.woff2') format('woff2'), src: url('https://cdn.plyr.io/static/fonts/gordita-bold.woff2') format('woff2'),
url('https://cdn.plyr.io/static/fonts/gordita-bold.woff') format('woff'); url('https://cdn.plyr.io/static/fonts/gordita-bold.woff') format('woff');
} }
@font-face { @font-face {
font-display: swap; font-display: swap;
font-family: 'Gordita'; font-family: 'Gordita';
font-style: normal; font-style: normal;
font-weight: $font-weight-black; font-weight: $font-weight-black;
src: url('https://cdn.plyr.io/static/fonts/gordita-black.woff2') format('woff2'), src: url('https://cdn.plyr.io/static/fonts/gordita-black.woff2') format('woff2'),
url('https://cdn.plyr.io/static/fonts/gordita-black.woff') format('woff'); url('https://cdn.plyr.io/static/fonts/gordita-black.woff') format('woff');
} }
+27 -27
View File
@@ -5,50 +5,50 @@
// Convert a <button> into an <a> // Convert a <button> into an <a>
// --------------------------------------- // ---------------------------------------
@mixin cancel-button-styles() { @mixin cancel-button-styles() {
background: transparent; background: transparent;
border: 0; border: 0;
border-radius: 0; border-radius: 0;
cursor: pointer; cursor: pointer;
font: inherit; font: inherit;
line-height: $line-height-base; line-height: $line-height-base;
margin: 0; margin: 0;
padding: 0; padding: 0;
position: relative; position: relative;
text-align: inherit; text-align: inherit;
text-shadow: inherit; text-shadow: inherit;
-moz-user-select: text; // stylelint-disable-line -moz-user-select: text; // stylelint-disable-line
vertical-align: baseline; vertical-align: baseline;
width: auto; width: auto;
} }
// Nicer focus styles // Nicer focus styles
// --------------------------------------- // ---------------------------------------
@mixin tab-focus($color: $tab-focus-default-color) { @mixin tab-focus($color: $tab-focus-default-color) {
box-shadow: 0 0 0 3px rgba($color, 0.35); box-shadow: 0 0 0 3px rgba($color, 0.35);
outline: 0; outline: 0;
} }
// Use rems for font sizing // Use rems for font sizing
// Leave <body> at 100%/16px // Leave <body> at 100%/16px
// --------------------------------------- // ---------------------------------------
@function calculate-rem($size) { @function calculate-rem($size) {
$rem: $size / 16; $rem: $size / 16;
@return #{$rem}rem; @return #{$rem}rem;
} }
@mixin font-size($size: $font-size-base) { @mixin font-size($size: $font-size-base) {
font-size: $size * 1px; // Fallback in px font-size: $size * 1px; // Fallback in px
font-size: calculate-rem($size); font-size: calculate-rem($size);
} }
// Font smoothing // Font smoothing
// --------------------------------------- // ---------------------------------------
@mixin font-smoothing($enabled: true) { @mixin font-smoothing($enabled: true) {
@if $enabled { @if $enabled {
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} @else { } @else {
-moz-osx-font-smoothing: auto; -moz-osx-font-smoothing: auto;
-webkit-font-smoothing: subpixel-antialiased; -webkit-font-smoothing: subpixel-antialiased;
} }
} }
+74 -74
View File
@@ -10,9 +10,9 @@
*/ */
html { html {
line-height: 1.15; /* 1 */ line-height: 1.15; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */ -ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */ -webkit-text-size-adjust: 100%; /* 2 */
} }
/* Sections /* Sections
@@ -23,7 +23,7 @@ html {
*/ */
body { body {
margin: 0; margin: 0;
} }
/** /**
@@ -36,7 +36,7 @@ footer,
header, header,
nav, nav,
section { section {
display: block; display: block;
} }
/** /**
@@ -45,8 +45,8 @@ section {
*/ */
h1 { h1 {
font-size: 2em; font-size: 2em;
margin: 0.67em 0; margin: 0.67em 0;
} }
/* Grouping content /* Grouping content
@@ -60,8 +60,8 @@ h1 {
figcaption, figcaption,
figure, figure,
main { main {
/* 1 */ /* 1 */
display: block; display: block;
} }
/** /**
@@ -69,7 +69,7 @@ main {
*/ */
figure { figure {
margin: 1em 40px; margin: 1em 40px;
} }
/** /**
@@ -78,9 +78,9 @@ figure {
*/ */
hr { hr {
box-sizing: content-box; /* 1 */ box-sizing: content-box; /* 1 */
height: 0; /* 1 */ height: 0; /* 1 */
overflow: visible; /* 2 */ overflow: visible; /* 2 */
} }
/** /**
@@ -89,8 +89,8 @@ hr {
*/ */
pre { pre {
font-family: monospace, monospace; /* 1 */ font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */ font-size: 1em; /* 2 */
} }
/* Text-level semantics /* Text-level semantics
@@ -102,8 +102,8 @@ pre {
*/ */
a { a {
background-color: transparent; /* 1 */ background-color: transparent; /* 1 */
-webkit-text-decoration-skip: objects; /* 2 */ -webkit-text-decoration-skip: objects; /* 2 */
} }
/** /**
@@ -112,9 +112,9 @@ a {
*/ */
abbr[title] { abbr[title] {
border-bottom: none; /* 1 */ border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */ text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */ text-decoration: underline dotted; /* 2 */
} }
/** /**
@@ -123,7 +123,7 @@ abbr[title] {
b, b,
strong { strong {
font-weight: inherit; font-weight: inherit;
} }
/** /**
@@ -132,7 +132,7 @@ strong {
b, b,
strong { strong {
font-weight: bolder; font-weight: bolder;
} }
/** /**
@@ -143,8 +143,8 @@ strong {
code, code,
kbd, kbd,
samp { samp {
font-family: monospace, monospace; /* 1 */ font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */ font-size: 1em; /* 2 */
} }
/** /**
@@ -152,7 +152,7 @@ samp {
*/ */
dfn { dfn {
font-style: italic; font-style: italic;
} }
/** /**
@@ -160,8 +160,8 @@ dfn {
*/ */
mark { mark {
background-color: #ff0; background-color: #ff0;
color: #000; color: #000;
} }
/** /**
@@ -169,7 +169,7 @@ mark {
*/ */
small { small {
font-size: 80%; font-size: 80%;
} }
/** /**
@@ -179,18 +179,18 @@ small {
sub, sub,
sup { sup {
font-size: 75%; font-size: 75%;
line-height: 0; line-height: 0;
position: relative; position: relative;
vertical-align: baseline; vertical-align: baseline;
} }
sub { sub {
bottom: -0.25em; bottom: -0.25em;
} }
sup { sup {
top: -0.5em; top: -0.5em;
} }
/* Embedded content /* Embedded content
@@ -202,7 +202,7 @@ sup {
audio, audio,
video { video {
display: inline-block; display: inline-block;
} }
/** /**
@@ -210,8 +210,8 @@ video {
*/ */
audio:not([controls]) { audio:not([controls]) {
display: none; display: none;
height: 0; height: 0;
} }
/** /**
@@ -219,7 +219,7 @@ audio:not([controls]) {
*/ */
img { img {
border-style: none; border-style: none;
} }
/** /**
@@ -227,7 +227,7 @@ img {
*/ */
svg:not(:root) { svg:not(:root) {
overflow: hidden; overflow: hidden;
} }
/* Forms /* Forms
@@ -243,10 +243,10 @@ input,
optgroup, optgroup,
select, select,
textarea { textarea {
font-family: sans-serif; /* 1 */ font-family: sans-serif; /* 1 */
font-size: 100%; /* 1 */ font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */ line-height: 1.15; /* 1 */
margin: 0; /* 2 */ margin: 0; /* 2 */
} }
/** /**
@@ -256,8 +256,8 @@ textarea {
button, button,
input { input {
/* 1 */ /* 1 */
overflow: visible; overflow: visible;
} }
/** /**
@@ -267,8 +267,8 @@ input {
button, button,
select { select {
/* 1 */ /* 1 */
text-transform: none; text-transform: none;
} }
/** /**
@@ -281,7 +281,7 @@ button,
html [type='button'], html [type='button'],
[type='reset'], [type='reset'],
[type='submit'] { [type='submit'] {
-webkit-appearance: button; /* 2 */ -webkit-appearance: button; /* 2 */
} }
/** /**
@@ -292,8 +292,8 @@ button::-moz-focus-inner,
[type='button']::-moz-focus-inner, [type='button']::-moz-focus-inner,
[type='reset']::-moz-focus-inner, [type='reset']::-moz-focus-inner,
[type='submit']::-moz-focus-inner { [type='submit']::-moz-focus-inner {
border-style: none; border-style: none;
padding: 0; padding: 0;
} }
/** /**
@@ -304,7 +304,7 @@ button:-moz-focusring,
[type='button']:-moz-focusring, [type='button']:-moz-focusring,
[type='reset']:-moz-focusring, [type='reset']:-moz-focusring,
[type='submit']:-moz-focusring { [type='submit']:-moz-focusring {
outline: 1px dotted ButtonText; outline: 1px dotted ButtonText;
} }
/** /**
@@ -312,7 +312,7 @@ button:-moz-focusring,
*/ */
fieldset { fieldset {
padding: 0.35em 0.75em 0.625em; padding: 0.35em 0.75em 0.625em;
} }
/** /**
@@ -323,12 +323,12 @@ fieldset {
*/ */
legend { legend {
box-sizing: border-box; /* 1 */ box-sizing: border-box; /* 1 */
color: inherit; /* 2 */ color: inherit; /* 2 */
display: table; /* 1 */ display: table; /* 1 */
max-width: 100%; /* 1 */ max-width: 100%; /* 1 */
padding: 0; /* 3 */ padding: 0; /* 3 */
white-space: normal; /* 1 */ white-space: normal; /* 1 */
} }
/** /**
@@ -337,8 +337,8 @@ legend {
*/ */
progress { progress {
display: inline-block; /* 1 */ display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */ vertical-align: baseline; /* 2 */
} }
/** /**
@@ -346,7 +346,7 @@ progress {
*/ */
textarea { textarea {
overflow: auto; overflow: auto;
} }
/** /**
@@ -356,8 +356,8 @@ textarea {
[type='checkbox'], [type='checkbox'],
[type='radio'] { [type='radio'] {
box-sizing: border-box; /* 1 */ box-sizing: border-box; /* 1 */
padding: 0; /* 2 */ padding: 0; /* 2 */
} }
/** /**
@@ -366,7 +366,7 @@ textarea {
[type='number']::-webkit-inner-spin-button, [type='number']::-webkit-inner-spin-button,
[type='number']::-webkit-outer-spin-button { [type='number']::-webkit-outer-spin-button {
height: auto; height: auto;
} }
/** /**
@@ -375,8 +375,8 @@ textarea {
*/ */
[type='search'] { [type='search'] {
-webkit-appearance: textfield; /* 1 */ -webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */ outline-offset: -2px; /* 2 */
} }
/** /**
@@ -385,7 +385,7 @@ textarea {
[type='search']::-webkit-search-cancel-button, [type='search']::-webkit-search-cancel-button,
[type='search']::-webkit-search-decoration { [type='search']::-webkit-search-decoration {
-webkit-appearance: none; -webkit-appearance: none;
} }
/** /**
@@ -394,8 +394,8 @@ textarea {
*/ */
::-webkit-file-upload-button { ::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */ -webkit-appearance: button; /* 1 */
font: inherit; /* 2 */ font: inherit; /* 2 */
} }
/* Interactive /* Interactive
@@ -408,7 +408,7 @@ textarea {
details, details,
menu { menu {
display: block; display: block;
} }
/* /*
@@ -416,7 +416,7 @@ menu {
*/ */
summary { summary {
display: list-item; display: list-item;
} }
/* Scripting /* Scripting
@@ -427,7 +427,7 @@ summary {
*/ */
canvas { canvas {
display: inline-block; display: inline-block;
} }
/** /**
@@ -435,7 +435,7 @@ canvas {
*/ */
template { template {
display: none; display: none;
} }
/* Hidden /* Hidden
@@ -446,5 +446,5 @@ template {
*/ */
[hidden] { [hidden] {
display: none; display: none;
} }
+1 -1
View File
@@ -7,5 +7,5 @@
*, *,
*::after, *::after,
*::before { *::before {
box-sizing: border-box; box-sizing: border-box;
} }
+6 -5
View File
@@ -2,16 +2,17 @@
// Plyr Settings // Plyr Settings
// ========================================================================== // ==========================================================================
// Font // Font sizes
$plyr-font-family: inherit;
// Sizes
$plyr-font-size-base: 13px; $plyr-font-size-base: 13px;
$plyr-font-size-small: 12px; $plyr-font-size-small: 12px;
$plyr-font-size-time: 11px; $plyr-font-size-time: 11px;
$plyr-font-size-badges: 9px; $plyr-font-size-badges: 9px;
// Other // Font weight
$plyr-font-weight-regular: 500;
$plyr-font-weight-bold: 600;
// Font smoothing
$plyr-font-smoothing: true; $plyr-font-smoothing: true;
// Colors // Colors
+1 -1
View File
@@ -3,7 +3,7 @@
// ========================================================================== // ==========================================================================
$font-sans-serif: 'Gordita', 'Avenir', 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', $font-sans-serif: 'Gordita', 'Avenir', 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol'; 'Segoe UI Symbol';
$font-size-base: 15; $font-size-base: 15;
$font-size-small: 13; $font-size-small: 13;
+11 -11
View File
@@ -4,31 +4,31 @@
// Set to 100% for rem sizing // Set to 100% for rem sizing
html { html {
font-size: 100%; font-size: 100%;
} }
body { body {
@include font-smoothing(); @include font-smoothing();
@include font-size($font-size-base); @include font-size($font-size-base);
color: $color-text; color: $color-text;
font-family: $font-sans-serif; font-family: $font-sans-serif;
font-weight: $font-weight-medium; font-weight: $font-weight-medium;
line-height: $line-height-base; line-height: $line-height-base;
} }
button, button,
input, input,
select, select,
textarea { textarea {
font: inherit; font: inherit;
} }
p, p,
small { small {
margin: 0 0 ($spacing-base * 1.5); margin: 0 0 ($spacing-base * 1.5);
} }
small { small {
@include font-size($font-size-small); @include font-size($font-size-small);
display: block; display: block;
} }
+6 -6
View File
@@ -3,10 +3,10 @@
// ========================================================================== // ==========================================================================
h1 { h1 {
@include font-size($font-size-h1); @include font-size($font-size-h1);
color: $color-headings; color: $color-headings;
font-weight: $font-weight-bold; font-weight: $font-weight-bold;
letter-spacing: $letter-spacing-headings; letter-spacing: $letter-spacing-headings;
line-height: 1.2; line-height: 1.2;
margin: 0 0 ($spacing-base * 1.5); margin: 0 0 ($spacing-base * 1.5);
} }
+1 -1
View File
@@ -3,5 +3,5 @@
// ========================================================================== // ==========================================================================
.no-border { .no-border {
border: 0; border: 0;
} }
+10 -10
View File
@@ -3,18 +3,18 @@
// ========================================================================== // ==========================================================================
[hidden] { [hidden] {
display: none; display: none;
} }
// Hide only visually, but have it available for screen readers: h5bp.com/v // Hide only visually, but have it available for screen readers: h5bp.com/v
.sr-only { .sr-only {
border: 0; border: 0;
clip: rect(0 0 0 0); clip: rect(0 0 0 0);
height: 1px; height: 1px;
margin: -1px; margin: -1px;
opacity: 0.001; opacity: 0.001;
overflow: hidden; overflow: hidden;
padding: 0; padding: 0;
position: absolute; position: absolute;
width: 1px; width: 1px;
} }
+1 -1
View File
File diff suppressed because one or more lines are too long
+70 -70
View File
@@ -1334,19 +1334,19 @@ typeof navigator === "object" && (function (global, factory) {
return (current / max * 100).toFixed(2); return (current / max * 100).toFixed(2);
} // Replace all occurances of a string in a string } // Replace all occurances of a string in a string
function replaceAll() { var replaceAll = function replaceAll() {
var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
var find = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; var find = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
var replace = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : ''; var replace = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : '';
return input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString()); return input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString());
} // Convert to title case }; // Convert to title case
function toTitleCase() { var toTitleCase = function toTitleCase() {
var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
return input.toString().replace(/\w\S*/g, function (text) { return input.toString().replace(/\w\S*/g, function (text) {
return text.charAt(0).toUpperCase() + text.substr(1).toLowerCase(); return text.charAt(0).toUpperCase() + text.substr(1).toLowerCase();
}); });
} // Convert string to pascalCase }; // Convert string to pascalCase
function toPascalCase() { function toPascalCase() {
var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
@@ -2444,39 +2444,39 @@ typeof navigator === "object" && (function (global, factory) {
// Set the looping options // Set the looping options
/* setLoopMenu() { /* setLoopMenu() {
// Menu required // Menu required
if (!is.element(this.elements.settings.panels.loop)) { if (!is.element(this.elements.settings.panels.loop)) {
return; return;
} }
const options = ['start', 'end', 'all', 'reset']; const options = ['start', 'end', 'all', 'reset'];
const list = this.elements.settings.panels.loop.querySelector('[role="menu"]'); const list = this.elements.settings.panels.loop.querySelector('[role="menu"]');
// Show the pane and tab // Show the pane and tab
toggleHidden(this.elements.settings.buttons.loop, false); toggleHidden(this.elements.settings.buttons.loop, false);
toggleHidden(this.elements.settings.panels.loop, false); toggleHidden(this.elements.settings.panels.loop, false);
// Toggle the pane and tab // Toggle the pane and tab
const toggle = !is.empty(this.loop.options); const toggle = !is.empty(this.loop.options);
controls.toggleMenuButton.call(this, 'loop', toggle); controls.toggleMenuButton.call(this, 'loop', toggle);
// Empty the menu // Empty the menu
emptyElement(list); emptyElement(list);
options.forEach(option => { options.forEach(option => {
const item = createElement('li'); const item = createElement('li');
const button = createElement( const button = createElement(
'button', 'button',
extend(getAttributesFromSelector(this.config.selectors.buttons.loop), { extend(getAttributesFromSelector(this.config.selectors.buttons.loop), {
type: 'button', type: 'button',
class: this.config.classNames.control, class: this.config.classNames.control,
'data-plyr-loop-action': option, 'data-plyr-loop-action': option,
}), }),
i18n.get(option, this.config) i18n.get(option, this.config)
); );
if (['start', 'end'].includes(option)) { if (['start', 'end'].includes(option)) {
const badge = controls.createBadge.call(this, '00:00'); const badge = controls.createBadge.call(this, '00:00');
button.appendChild(badge); button.appendChild(badge);
} }
item.appendChild(button); item.appendChild(button);
list.appendChild(item); list.appendChild(item);
}); });
}, */ }, */
// Get current selected caption language // Get current selected caption language
// TODO: rework this to user the getter in the API? // TODO: rework this to user the getter in the API?
// Set a list of available captions languages // Set a list of available captions languages
@@ -8900,41 +8900,41 @@ typeof navigator === "object" && (function (global, factory) {
this.media.loop = toggle; // Set default to be a true toggle this.media.loop = toggle; // Set default to be a true toggle
/* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle'; /* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle';
switch (type) { switch (type) {
case 'start': case 'start':
if (this.config.loop.end && this.config.loop.end <= this.currentTime) { if (this.config.loop.end && this.config.loop.end <= this.currentTime) {
this.config.loop.end = null; this.config.loop.end = null;
} }
this.config.loop.start = this.currentTime; this.config.loop.start = this.currentTime;
// this.config.loop.indicator.start = this.elements.display.played.value; // this.config.loop.indicator.start = this.elements.display.played.value;
break; break;
case 'end': case 'end':
if (this.config.loop.start >= this.currentTime) { if (this.config.loop.start >= this.currentTime) {
return this; return this;
} }
this.config.loop.end = this.currentTime; this.config.loop.end = this.currentTime;
// this.config.loop.indicator.end = this.elements.display.played.value; // this.config.loop.indicator.end = this.elements.display.played.value;
break; break;
case 'all': case 'all':
this.config.loop.start = 0;
this.config.loop.end = this.duration - 2;
this.config.loop.indicator.start = 0;
this.config.loop.indicator.end = 100;
break;
case 'toggle':
if (this.config.loop.active) {
this.config.loop.start = 0;
this.config.loop.end = null;
} else {
this.config.loop.start = 0; this.config.loop.start = 0;
this.config.loop.end = this.duration - 2; this.config.loop.end = this.duration - 2;
} this.config.loop.indicator.start = 0;
break; this.config.loop.indicator.end = 100;
default: break;
this.config.loop.start = 0; case 'toggle':
this.config.loop.end = null; if (this.config.loop.active) {
break; this.config.loop.start = 0;
} */ this.config.loop.end = null;
} else {
this.config.loop.start = 0;
this.config.loop.end = this.duration - 2;
}
break;
default:
this.config.loop.start = 0;
this.config.loop.end = null;
break;
} */
} }
/** /**
* Get current loop state * Get current loop state
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+70 -70
View File
@@ -1328,19 +1328,19 @@ function getPercentage(current, max) {
return (current / max * 100).toFixed(2); return (current / max * 100).toFixed(2);
} // Replace all occurances of a string in a string } // Replace all occurances of a string in a string
function replaceAll() { var replaceAll = function replaceAll() {
var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
var find = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; var find = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
var replace = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : ''; var replace = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : '';
return input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString()); return input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString());
} // Convert to title case }; // Convert to title case
function toTitleCase() { var toTitleCase = function toTitleCase() {
var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
return input.toString().replace(/\w\S*/g, function (text) { return input.toString().replace(/\w\S*/g, function (text) {
return text.charAt(0).toUpperCase() + text.substr(1).toLowerCase(); return text.charAt(0).toUpperCase() + text.substr(1).toLowerCase();
}); });
} // Convert string to pascalCase }; // Convert string to pascalCase
function toPascalCase() { function toPascalCase() {
var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
@@ -2438,39 +2438,39 @@ var controls = {
// Set the looping options // Set the looping options
/* setLoopMenu() { /* setLoopMenu() {
// Menu required // Menu required
if (!is.element(this.elements.settings.panels.loop)) { if (!is.element(this.elements.settings.panels.loop)) {
return; return;
} }
const options = ['start', 'end', 'all', 'reset']; const options = ['start', 'end', 'all', 'reset'];
const list = this.elements.settings.panels.loop.querySelector('[role="menu"]'); const list = this.elements.settings.panels.loop.querySelector('[role="menu"]');
// Show the pane and tab // Show the pane and tab
toggleHidden(this.elements.settings.buttons.loop, false); toggleHidden(this.elements.settings.buttons.loop, false);
toggleHidden(this.elements.settings.panels.loop, false); toggleHidden(this.elements.settings.panels.loop, false);
// Toggle the pane and tab // Toggle the pane and tab
const toggle = !is.empty(this.loop.options); const toggle = !is.empty(this.loop.options);
controls.toggleMenuButton.call(this, 'loop', toggle); controls.toggleMenuButton.call(this, 'loop', toggle);
// Empty the menu // Empty the menu
emptyElement(list); emptyElement(list);
options.forEach(option => { options.forEach(option => {
const item = createElement('li'); const item = createElement('li');
const button = createElement( const button = createElement(
'button', 'button',
extend(getAttributesFromSelector(this.config.selectors.buttons.loop), { extend(getAttributesFromSelector(this.config.selectors.buttons.loop), {
type: 'button', type: 'button',
class: this.config.classNames.control, class: this.config.classNames.control,
'data-plyr-loop-action': option, 'data-plyr-loop-action': option,
}), }),
i18n.get(option, this.config) i18n.get(option, this.config)
); );
if (['start', 'end'].includes(option)) { if (['start', 'end'].includes(option)) {
const badge = controls.createBadge.call(this, '00:00'); const badge = controls.createBadge.call(this, '00:00');
button.appendChild(badge); button.appendChild(badge);
} }
item.appendChild(button); item.appendChild(button);
list.appendChild(item); list.appendChild(item);
}); });
}, */ }, */
// Get current selected caption language // Get current selected caption language
// TODO: rework this to user the getter in the API? // TODO: rework this to user the getter in the API?
// Set a list of available captions languages // Set a list of available captions languages
@@ -8894,41 +8894,41 @@ var Plyr = /*#__PURE__*/function () {
this.media.loop = toggle; // Set default to be a true toggle this.media.loop = toggle; // Set default to be a true toggle
/* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle'; /* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle';
switch (type) { switch (type) {
case 'start': case 'start':
if (this.config.loop.end && this.config.loop.end <= this.currentTime) { if (this.config.loop.end && this.config.loop.end <= this.currentTime) {
this.config.loop.end = null; this.config.loop.end = null;
} }
this.config.loop.start = this.currentTime; this.config.loop.start = this.currentTime;
// this.config.loop.indicator.start = this.elements.display.played.value; // this.config.loop.indicator.start = this.elements.display.played.value;
break; break;
case 'end': case 'end':
if (this.config.loop.start >= this.currentTime) { if (this.config.loop.start >= this.currentTime) {
return this; return this;
} }
this.config.loop.end = this.currentTime; this.config.loop.end = this.currentTime;
// this.config.loop.indicator.end = this.elements.display.played.value; // this.config.loop.indicator.end = this.elements.display.played.value;
break; break;
case 'all': case 'all':
this.config.loop.start = 0;
this.config.loop.end = this.duration - 2;
this.config.loop.indicator.start = 0;
this.config.loop.indicator.end = 100;
break;
case 'toggle':
if (this.config.loop.active) {
this.config.loop.start = 0;
this.config.loop.end = null;
} else {
this.config.loop.start = 0; this.config.loop.start = 0;
this.config.loop.end = this.duration - 2; this.config.loop.end = this.duration - 2;
} this.config.loop.indicator.start = 0;
break; this.config.loop.indicator.end = 100;
default: break;
this.config.loop.start = 0; case 'toggle':
this.config.loop.end = null; if (this.config.loop.active) {
break; this.config.loop.start = 0;
} */ this.config.loop.end = null;
} else {
this.config.loop.start = 0;
this.config.loop.end = this.duration - 2;
}
break;
default:
this.config.loop.start = 0;
this.config.loop.end = null;
break;
} */
} }
/** /**
* Get current loop state * Get current loop state
+70 -70
View File
@@ -7640,19 +7640,19 @@ typeof navigator === "object" && (function (global, factory) {
return (current / max * 100).toFixed(2); return (current / max * 100).toFixed(2);
} // Replace all occurances of a string in a string } // Replace all occurances of a string in a string
function replaceAll() { var replaceAll = function replaceAll() {
var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
var find = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; var find = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
var replace = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : ''; var replace = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : '';
return input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString()); return input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString());
} // Convert to title case }; // Convert to title case
function toTitleCase() { var toTitleCase = function toTitleCase() {
var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
return input.toString().replace(/\w\S*/g, function (text) { return input.toString().replace(/\w\S*/g, function (text) {
return text.charAt(0).toUpperCase() + text.substr(1).toLowerCase(); return text.charAt(0).toUpperCase() + text.substr(1).toLowerCase();
}); });
} // Convert string to pascalCase }; // Convert string to pascalCase
function toPascalCase() { function toPascalCase() {
var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
@@ -8759,39 +8759,39 @@ typeof navigator === "object" && (function (global, factory) {
// Set the looping options // Set the looping options
/* setLoopMenu() { /* setLoopMenu() {
// Menu required // Menu required
if (!is.element(this.elements.settings.panels.loop)) { if (!is.element(this.elements.settings.panels.loop)) {
return; return;
} }
const options = ['start', 'end', 'all', 'reset']; const options = ['start', 'end', 'all', 'reset'];
const list = this.elements.settings.panels.loop.querySelector('[role="menu"]'); const list = this.elements.settings.panels.loop.querySelector('[role="menu"]');
// Show the pane and tab // Show the pane and tab
toggleHidden(this.elements.settings.buttons.loop, false); toggleHidden(this.elements.settings.buttons.loop, false);
toggleHidden(this.elements.settings.panels.loop, false); toggleHidden(this.elements.settings.panels.loop, false);
// Toggle the pane and tab // Toggle the pane and tab
const toggle = !is.empty(this.loop.options); const toggle = !is.empty(this.loop.options);
controls.toggleMenuButton.call(this, 'loop', toggle); controls.toggleMenuButton.call(this, 'loop', toggle);
// Empty the menu // Empty the menu
emptyElement(list); emptyElement(list);
options.forEach(option => { options.forEach(option => {
const item = createElement('li'); const item = createElement('li');
const button = createElement( const button = createElement(
'button', 'button',
extend(getAttributesFromSelector(this.config.selectors.buttons.loop), { extend(getAttributesFromSelector(this.config.selectors.buttons.loop), {
type: 'button', type: 'button',
class: this.config.classNames.control, class: this.config.classNames.control,
'data-plyr-loop-action': option, 'data-plyr-loop-action': option,
}), }),
i18n.get(option, this.config) i18n.get(option, this.config)
); );
if (['start', 'end'].includes(option)) { if (['start', 'end'].includes(option)) {
const badge = controls.createBadge.call(this, '00:00'); const badge = controls.createBadge.call(this, '00:00');
button.appendChild(badge); button.appendChild(badge);
} }
item.appendChild(button); item.appendChild(button);
list.appendChild(item); list.appendChild(item);
}); });
}, */ }, */
// Get current selected caption language // Get current selected caption language
// TODO: rework this to user the getter in the API? // TODO: rework this to user the getter in the API?
// Set a list of available captions languages // Set a list of available captions languages
@@ -15328,41 +15328,41 @@ typeof navigator === "object" && (function (global, factory) {
this.media.loop = toggle; // Set default to be a true toggle this.media.loop = toggle; // Set default to be a true toggle
/* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle'; /* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle';
switch (type) { switch (type) {
case 'start': case 'start':
if (this.config.loop.end && this.config.loop.end <= this.currentTime) { if (this.config.loop.end && this.config.loop.end <= this.currentTime) {
this.config.loop.end = null; this.config.loop.end = null;
} }
this.config.loop.start = this.currentTime; this.config.loop.start = this.currentTime;
// this.config.loop.indicator.start = this.elements.display.played.value; // this.config.loop.indicator.start = this.elements.display.played.value;
break; break;
case 'end': case 'end':
if (this.config.loop.start >= this.currentTime) { if (this.config.loop.start >= this.currentTime) {
return this; return this;
} }
this.config.loop.end = this.currentTime; this.config.loop.end = this.currentTime;
// this.config.loop.indicator.end = this.elements.display.played.value; // this.config.loop.indicator.end = this.elements.display.played.value;
break; break;
case 'all': case 'all':
this.config.loop.start = 0;
this.config.loop.end = this.duration - 2;
this.config.loop.indicator.start = 0;
this.config.loop.indicator.end = 100;
break;
case 'toggle':
if (this.config.loop.active) {
this.config.loop.start = 0;
this.config.loop.end = null;
} else {
this.config.loop.start = 0; this.config.loop.start = 0;
this.config.loop.end = this.duration - 2; this.config.loop.end = this.duration - 2;
} this.config.loop.indicator.start = 0;
break; this.config.loop.indicator.end = 100;
default: break;
this.config.loop.start = 0; case 'toggle':
this.config.loop.end = null; if (this.config.loop.active) {
break; this.config.loop.start = 0;
} */ this.config.loop.end = null;
} else {
this.config.loop.start = 0;
this.config.loop.end = this.duration - 2;
}
break;
default:
this.config.loop.start = 0;
this.config.loop.end = null;
break;
} */
} }
/** /**
* Get current loop state * Get current loop state
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+70 -70
View File
@@ -7634,19 +7634,19 @@ function getPercentage(current, max) {
return (current / max * 100).toFixed(2); return (current / max * 100).toFixed(2);
} // Replace all occurances of a string in a string } // Replace all occurances of a string in a string
function replaceAll() { var replaceAll = function replaceAll() {
var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
var find = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; var find = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
var replace = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : ''; var replace = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : '';
return input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString()); return input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString());
} // Convert to title case }; // Convert to title case
function toTitleCase() { var toTitleCase = function toTitleCase() {
var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
return input.toString().replace(/\w\S*/g, function (text) { return input.toString().replace(/\w\S*/g, function (text) {
return text.charAt(0).toUpperCase() + text.substr(1).toLowerCase(); return text.charAt(0).toUpperCase() + text.substr(1).toLowerCase();
}); });
} // Convert string to pascalCase }; // Convert string to pascalCase
function toPascalCase() { function toPascalCase() {
var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
@@ -8753,39 +8753,39 @@ var controls = {
// Set the looping options // Set the looping options
/* setLoopMenu() { /* setLoopMenu() {
// Menu required // Menu required
if (!is.element(this.elements.settings.panels.loop)) { if (!is.element(this.elements.settings.panels.loop)) {
return; return;
} }
const options = ['start', 'end', 'all', 'reset']; const options = ['start', 'end', 'all', 'reset'];
const list = this.elements.settings.panels.loop.querySelector('[role="menu"]'); const list = this.elements.settings.panels.loop.querySelector('[role="menu"]');
// Show the pane and tab // Show the pane and tab
toggleHidden(this.elements.settings.buttons.loop, false); toggleHidden(this.elements.settings.buttons.loop, false);
toggleHidden(this.elements.settings.panels.loop, false); toggleHidden(this.elements.settings.panels.loop, false);
// Toggle the pane and tab // Toggle the pane and tab
const toggle = !is.empty(this.loop.options); const toggle = !is.empty(this.loop.options);
controls.toggleMenuButton.call(this, 'loop', toggle); controls.toggleMenuButton.call(this, 'loop', toggle);
// Empty the menu // Empty the menu
emptyElement(list); emptyElement(list);
options.forEach(option => { options.forEach(option => {
const item = createElement('li'); const item = createElement('li');
const button = createElement( const button = createElement(
'button', 'button',
extend(getAttributesFromSelector(this.config.selectors.buttons.loop), { extend(getAttributesFromSelector(this.config.selectors.buttons.loop), {
type: 'button', type: 'button',
class: this.config.classNames.control, class: this.config.classNames.control,
'data-plyr-loop-action': option, 'data-plyr-loop-action': option,
}), }),
i18n.get(option, this.config) i18n.get(option, this.config)
); );
if (['start', 'end'].includes(option)) { if (['start', 'end'].includes(option)) {
const badge = controls.createBadge.call(this, '00:00'); const badge = controls.createBadge.call(this, '00:00');
button.appendChild(badge); button.appendChild(badge);
} }
item.appendChild(button); item.appendChild(button);
list.appendChild(item); list.appendChild(item);
}); });
}, */ }, */
// Get current selected caption language // Get current selected caption language
// TODO: rework this to user the getter in the API? // TODO: rework this to user the getter in the API?
// Set a list of available captions languages // Set a list of available captions languages
@@ -15322,41 +15322,41 @@ var Plyr = /*#__PURE__*/function () {
this.media.loop = toggle; // Set default to be a true toggle this.media.loop = toggle; // Set default to be a true toggle
/* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle'; /* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle';
switch (type) { switch (type) {
case 'start': case 'start':
if (this.config.loop.end && this.config.loop.end <= this.currentTime) { if (this.config.loop.end && this.config.loop.end <= this.currentTime) {
this.config.loop.end = null; this.config.loop.end = null;
} }
this.config.loop.start = this.currentTime; this.config.loop.start = this.currentTime;
// this.config.loop.indicator.start = this.elements.display.played.value; // this.config.loop.indicator.start = this.elements.display.played.value;
break; break;
case 'end': case 'end':
if (this.config.loop.start >= this.currentTime) { if (this.config.loop.start >= this.currentTime) {
return this; return this;
} }
this.config.loop.end = this.currentTime; this.config.loop.end = this.currentTime;
// this.config.loop.indicator.end = this.elements.display.played.value; // this.config.loop.indicator.end = this.elements.display.played.value;
break; break;
case 'all': case 'all':
this.config.loop.start = 0;
this.config.loop.end = this.duration - 2;
this.config.loop.indicator.start = 0;
this.config.loop.indicator.end = 100;
break;
case 'toggle':
if (this.config.loop.active) {
this.config.loop.start = 0;
this.config.loop.end = null;
} else {
this.config.loop.start = 0; this.config.loop.start = 0;
this.config.loop.end = this.duration - 2; this.config.loop.end = this.duration - 2;
} this.config.loop.indicator.start = 0;
break; this.config.loop.indicator.end = 100;
default: break;
this.config.loop.start = 0; case 'toggle':
this.config.loop.end = null; if (this.config.loop.active) {
break; this.config.loop.start = 0;
} */ this.config.loop.end = null;
} else {
this.config.loop.start = 0;
this.config.loop.end = this.duration - 2;
}
break;
default:
this.config.loop.start = 0;
this.config.loop.end = null;
break;
} */
} }
/** /**
* Get current loop state * Get current loop state
+98 -98
View File
@@ -1,100 +1,100 @@
{ {
"name": "plyr", "name": "plyr",
"version": "3.5.10", "version": "3.5.10",
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player", "description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
"homepage": "https://plyr.io", "homepage": "https://plyr.io",
"author": "Sam Potts <sam@potts.es>", "author": "Sam Potts <sam@potts.es>",
"main": "dist/plyr.js", "main": "dist/plyr.js",
"types": "src/js/plyr.d.ts", "types": "src/js/plyr.d.ts",
"module": "dist/plyr.min.mjs", "module": "dist/plyr.min.mjs",
"jsnext:main": "dist/plyr.min.mjs", "jsnext:main": "dist/plyr.min.mjs",
"browser": "dist/plyr.min.js", "browser": "dist/plyr.min.js",
"sass": "src/sass/plyr.scss", "sass": "src/sass/plyr.scss",
"style": "dist/plyr.css", "style": "dist/plyr.css",
"keywords": [ "keywords": [
"HTML5 Video", "HTML5 Video",
"HTML5 Audio", "HTML5 Audio",
"Media Player", "Media Player",
"DASH", "DASH",
"Shaka", "Shaka",
"WordPress", "WordPress",
"HLS" "HLS"
], ],
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git://github.com/sampotts/plyr.git" "url": "git://github.com/sampotts/plyr.git"
}, },
"bugs": { "bugs": {
"url": "https://github.com/sampotts/plyr/issues" "url": "https://github.com/sampotts/plyr/issues"
}, },
"browserslist": "> 1%", "browserslist": "> 1%",
"scripts": { "scripts": {
"build": "gulp build", "build": "gulp build",
"lint": "eslint src/js && npm run-script remark", "lint": "eslint src/js && npm run-script remark",
"lint:fix": "eslint --fix src/js", "lint:fix": "eslint --fix src/js",
"remark": "remark -f --use 'validate-links=repository:\"sampotts/plyr\"' '{,!(node_modules),.?**/}*.md'", "remark": "remark -f --use 'validate-links=repository:\"sampotts/plyr\"' '{,!(node_modules),.?**/}*.md'",
"deploy": "yarn lint && gulp version && gulp build && gulp deploy", "deploy": "yarn lint && gulp version && gulp build && gulp deploy",
"format": "prettier --write ./src/js/**/*.js" "format": "prettier --write \"./{src,demo/src}/**/*.{js,scss}\""
}, },
"devDependencies": { "devDependencies": {
"ansi-colors": "^4.1.1", "ansi-colors": "^4.1.1",
"autoprefixer": "^9.7.5", "autoprefixer": "^9.7.5",
"aws-sdk": "^2.648.0", "aws-sdk": "^2.649.0",
"@babel/core": "^7.9.0", "@babel/core": "^7.9.0",
"@babel/preset-env": "^7.9.0", "@babel/preset-env": "^7.9.0",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"browser-sync": "^2.26.7", "browser-sync": "^2.26.7",
"del": "^5.1.0", "del": "^5.1.0",
"eslint": "^6.8.0", "eslint": "^6.8.0",
"eslint-config-airbnb-base": "^14.1.0", "eslint-config-airbnb-base": "^14.1.0",
"eslint-config-prettier": "^6.10.1", "eslint-config-prettier": "^6.10.1",
"eslint-plugin-import": "^2.20.1", "eslint-plugin-import": "^2.20.1",
"eslint-plugin-simple-import-sort": "^5.0.2", "eslint-plugin-simple-import-sort": "^5.0.2",
"fancy-log": "^1.3.3", "fancy-log": "^1.3.3",
"fastly-purge": "^1.0.1", "fastly-purge": "^1.0.1",
"git-branch": "^2.0.1", "git-branch": "^2.0.1",
"gulp": "^4.0.2", "gulp": "^4.0.2",
"gulp-awspublish": "^4.1.1", "gulp-awspublish": "^4.1.1",
"gulp-better-rollup": "^4.0.1", "gulp-better-rollup": "^4.0.1",
"gulp-filter": "^6.0.0", "gulp-filter": "^6.0.0",
"gulp-header": "^2.0.9", "gulp-header": "^2.0.9",
"gulp-hub": "^4.2.0", "gulp-hub": "^4.2.0",
"gulp-imagemin": "^7.1.0", "gulp-imagemin": "^7.1.0",
"gulp-open": "^3.0.1", "gulp-open": "^3.0.1",
"gulp-plumber": "^1.2.1", "gulp-plumber": "^1.2.1",
"gulp-postcss": "^8.0.0", "gulp-postcss": "^8.0.0",
"gulp-rename": "^2.0.0", "gulp-rename": "^2.0.0",
"gulp-replace": "^1.0.0", "gulp-replace": "^1.0.0",
"gulp-sass": "^4.0.2", "gulp-sass": "^4.0.2",
"gulp-size": "^3.0.0", "gulp-size": "^3.0.0",
"gulp-sourcemaps": "^2.6.5", "gulp-sourcemaps": "^2.6.5",
"gulp-svgstore": "^7.0.1", "gulp-svgstore": "^7.0.1",
"gulp-terser": "^1.2.0", "gulp-terser": "^1.2.0",
"postcss-clean": "^1.1.0", "postcss-clean": "^1.1.0",
"postcss-custom-properties": "^9.1.1", "postcss-custom-properties": "^9.1.1",
"prettier-eslint": "^9.0.1", "prettier-eslint": "^9.0.1",
"prettier-stylelint": "^0.4.2", "prettier-stylelint": "^0.4.2",
"remark-cli": "^7.0.1", "remark-cli": "^8.0.0",
"remark-validate-links": "^10.0.0", "remark-validate-links": "^10.0.0",
"rollup": "^2.2.0", "rollup": "^2.3.1",
"rollup-plugin-babel": "^4.4.0", "rollup-plugin-babel": "^4.4.0",
"rollup-plugin-commonjs": "^10.1.0", "rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-node-resolve": "^5.2.0", "rollup-plugin-node-resolve": "^5.2.0",
"stylelint": "^13.2.1", "stylelint": "^13.2.1",
"stylelint-config-prettier": "^8.0.1", "stylelint-config-prettier": "^8.0.1",
"stylelint-config-recommended": "^3.0.0", "stylelint-config-recommended": "^3.0.0",
"stylelint-config-sass-guidelines": "^7.0.0", "stylelint-config-sass-guidelines": "^7.0.0",
"stylelint-order": "^4.0.0", "stylelint-order": "^4.0.0",
"stylelint-scss": "^3.16.0", "stylelint-scss": "^3.16.0",
"stylelint-selector-bem-pattern": "^2.1.0", "stylelint-selector-bem-pattern": "^2.1.0",
"through2": "^3.0.1" "through2": "^3.0.1"
}, },
"dependencies": { "dependencies": {
"core-js": "^3.6.4", "core-js": "^3.6.4",
"custom-event-polyfill": "^1.0.7", "custom-event-polyfill": "^1.0.7",
"loadjs": "^4.2.0", "loadjs": "^4.2.0",
"rangetouch": "^2.0.1", "rangetouch": "^2.0.1",
"url-polyfill": "^1.1.8" "url-polyfill": "^1.1.8"
} }
} }
+314 -314
View File
@@ -8,12 +8,12 @@ import support from './support';
import { dedupe } from './utils/arrays'; import { dedupe } from './utils/arrays';
import browser from './utils/browser'; import browser from './utils/browser';
import { import {
createElement, createElement,
emptyElement, emptyElement,
getAttributesFromSelector, getAttributesFromSelector,
insertAfter, insertAfter,
removeElement, removeElement,
toggleClass, toggleClass,
} from './utils/elements'; } from './utils/elements';
import { on, triggerEvent } from './utils/events'; import { on, triggerEvent } from './utils/events';
import fetch from './utils/fetch'; import fetch from './utils/fetch';
@@ -23,371 +23,371 @@ import { getHTML } from './utils/strings';
import { parseUrl } from './utils/urls'; import { parseUrl } from './utils/urls';
const captions = { const captions = {
// Setup captions // Setup captions
setup() { setup() {
// Requires UI support // Requires UI support
if (!this.supported.ui) { if (!this.supported.ui) {
return; return;
} }
// Only Vimeo and HTML5 video supported at this point // Only Vimeo and HTML5 video supported at this point
if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) { if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) {
// Clear menu and hide // Clear menu and hide
if ( if (
is.array(this.config.controls) && is.array(this.config.controls) &&
this.config.controls.includes('settings') && this.config.controls.includes('settings') &&
this.config.settings.includes('captions') this.config.settings.includes('captions')
) { ) {
controls.setCaptionsMenu.call(this); controls.setCaptionsMenu.call(this);
} }
return; return;
} }
// Inject the container // Inject the container
if (!is.element(this.elements.captions)) { if (!is.element(this.elements.captions)) {
this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions)); this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions));
insertAfter(this.elements.captions, this.elements.wrapper); insertAfter(this.elements.captions, this.elements.wrapper);
} }
// Fix IE captions if CORS is used // Fix IE captions if CORS is used
// Fetch captions and inject as blobs instead (data URIs not supported!) // Fetch captions and inject as blobs instead (data URIs not supported!)
if (browser.isIE && window.URL) { if (browser.isIE && window.URL) {
const elements = this.media.querySelectorAll('track'); const elements = this.media.querySelectorAll('track');
Array.from(elements).forEach(track => { Array.from(elements).forEach(track => {
const src = track.getAttribute('src'); const src = track.getAttribute('src');
const url = parseUrl(src); const url = parseUrl(src);
if ( if (
url !== null && url !== null &&
url.hostname !== window.location.href.hostname && url.hostname !== window.location.href.hostname &&
['http:', 'https:'].includes(url.protocol) ['http:', 'https:'].includes(url.protocol)
) { ) {
fetch(src, 'blob') fetch(src, 'blob')
.then(blob => { .then(blob => {
track.setAttribute('src', window.URL.createObjectURL(blob)); track.setAttribute('src', window.URL.createObjectURL(blob));
}) })
.catch(() => { .catch(() => {
removeElement(track); removeElement(track);
});
}
}); });
} }
});
}
// Get and set initial data // Get and set initial data
// The "preferred" options are not realized unless / until the wanted language has a match // The "preferred" options are not realized unless / until the wanted language has a match
// * languages: Array of user's browser languages. // * languages: Array of user's browser languages.
// * language: The language preferred by user settings or config // * language: The language preferred by user settings or config
// * active: The state preferred by user settings or config // * active: The state preferred by user settings or config
// * toggled: The real captions state // * toggled: The real captions state
const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en']; const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];
const languages = dedupe(browserLanguages.map(language => language.split('-')[0])); const languages = dedupe(browserLanguages.map(language => language.split('-')[0]));
let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase(); let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase();
// Use first browser language when language is 'auto' // Use first browser language when language is 'auto'
if (language === 'auto') { if (language === 'auto') {
[language] = languages; [language] = languages;
} }
let active = this.storage.get('captions'); let active = this.storage.get('captions');
if (!is.boolean(active)) { if (!is.boolean(active)) {
({ active } = this.config.captions); ({ active } = this.config.captions);
} }
Object.assign(this.captions, { Object.assign(this.captions, {
toggled: false, toggled: false,
active, active,
language, language,
languages, languages,
});
// Watch changes to textTracks and update captions menu
if (this.isHTML5) {
const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';
on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this));
}
// Update available languages in list next tick (the event must not be triggered before the listeners)
setTimeout(captions.update.bind(this), 0);
},
// Update available language options in settings based on tracks
update() {
const tracks = captions.getTracks.call(this, true);
// Get the wanted language
const { active, language, meta, currentTrackNode } = this.captions;
const languageExists = Boolean(tracks.find(track => track.language === language));
// 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',
});
// 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));
}); });
}
// Watch changes to textTracks and update captions menu // Update language first time it matches, or if the previous matching track was removed
if (this.isHTML5) { if ((languageExists && this.language !== language) || !tracks.includes(currentTrackNode)) {
const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack'; captions.setLanguage.call(this, language);
on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this)); captions.toggle.call(this, active && languageExists);
} }
// Update available languages in list next tick (the event must not be triggered before the listeners) // Enable or disable captions based on track length
setTimeout(captions.update.bind(this), 0); toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks));
},
// Update available language options in settings based on tracks // Update available languages in list
update() { if (
const tracks = captions.getTracks.call(this, true); is.array(this.config.controls) &&
// Get the wanted language this.config.controls.includes('settings') &&
const { active, language, meta, currentTrackNode } = this.captions; this.config.settings.includes('captions')
const languageExists = Boolean(tracks.find(track => track.language === language)); ) {
controls.setCaptionsMenu.call(this);
}
},
// Handle tracks (add event listener and "pseudo"-default) // Toggle captions display
if (this.isHTML5 && this.isVideo) { // Used internally for the toggleCaptions method, with the passive option forced to false
tracks toggle(input, passive = true) {
.filter(track => !meta.get(track)) // If there's no full support
.forEach(track => { if (!this.supported.ui) {
this.debug.log('Track added', track); return;
// 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 const { toggled } = this.captions; // Current state
// eslint-disable-next-line no-param-reassign const activeClass = this.config.classNames.captions.active;
track.mode = 'hidden'; // Get the next state
// If the method is called without parameter, toggle based on current value
const active = is.nullOrUndefined(input) ? !toggled : input;
// Add event listener for cue changes // Update state and trigger event
on.call(this, track, 'cuechange', () => captions.updateCues.call(this)); if (active !== toggled) {
}); // When passive, don't override user preferences
} if (!passive) {
this.captions.active = active;
this.storage.set({ captions: active });
}
// Update language first time it matches, or if the previous matching track was removed // Force language if the call isn't passive and there is no matching language to toggle to
if ((languageExists && this.language !== language) || !tracks.includes(currentTrackNode)) { if (!this.language && active && !passive) {
captions.setLanguage.call(this, language);
captions.toggle.call(this, active && languageExists);
}
// Enable or disable captions based on track length
toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks));
// Update available languages in list
if (
is.array(this.config.controls) &&
this.config.controls.includes('settings') &&
this.config.settings.includes('captions')
) {
controls.setCaptionsMenu.call(this);
}
},
// Toggle captions display
// Used internally for the toggleCaptions method, with the passive option forced to false
toggle(input, passive = true) {
// If there's no full support
if (!this.supported.ui) {
return;
}
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;
// Update state and trigger event
if (active !== toggled) {
// When passive, don't override user preferences
if (!passive) {
this.captions.active = active;
this.storage.set({ captions: active });
}
// Force language if the call isn't passive and there is no matching language to toggle to
if (!this.language && active && !passive) {
const tracks = captions.getTracks.call(this);
const track = captions.findTrack.call(this, [this.captions.language, ...this.captions.languages], true);
// Override user preferences to avoid switching languages if a matching track is added
this.captions.language = track.language;
// Set caption, but don't store in localStorage as user preference
captions.set.call(this, tracks.indexOf(track));
return;
}
// Toggle button if it's enabled
if (this.elements.buttons.captions) {
this.elements.buttons.captions.pressed = active;
}
// Add class hook
toggleClass(this.elements.container, activeClass, active);
this.captions.toggled = active;
// Update settings menu
controls.updateSetting.call(this, 'captions');
// Trigger event (not used internally)
triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');
}
},
// Set captions by track index
// Used internally for the currentTrack setter with the passive option forced to false
set(index, passive = true) {
const tracks = captions.getTracks.call(this); const tracks = captions.getTracks.call(this);
const track = captions.findTrack.call(this, [this.captions.language, ...this.captions.languages], true);
// Disable captions if setting to -1 // Override user preferences to avoid switching languages if a matching track is added
if (index === -1) { this.captions.language = track.language;
captions.toggle.call(this, false, passive);
return;
}
if (!is.number(index)) { // Set caption, but don't store in localStorage as user preference
this.debug.warn('Invalid caption argument', index); captions.set.call(this, tracks.indexOf(track));
return; return;
} }
if (!(index in tracks)) { // Toggle button if it's enabled
this.debug.warn('Track not found', index); if (this.elements.buttons.captions) {
return; this.elements.buttons.captions.pressed = active;
} }
if (this.captions.currentTrack !== index) { // Add class hook
this.captions.currentTrack = index; toggleClass(this.elements.container, activeClass, active);
const track = tracks[index];
const { language } = track || {};
// Store reference to node for invalidation on remove this.captions.toggled = active;
this.captions.currentTrackNode = track;
// Update settings menu // Update settings menu
controls.updateSetting.call(this, 'captions'); controls.updateSetting.call(this, 'captions');
// When passive, don't override user preferences // Trigger event (not used internally)
if (!passive) { triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');
this.captions.language = language; }
this.storage.set({ language }); },
}
// Handle Vimeo captions // Set captions by track index
if (this.isVimeo) { // Used internally for the currentTrack setter with the passive option forced to false
this.embed.enableTextTrack(language); set(index, passive = true) {
} const tracks = captions.getTracks.call(this);
// Trigger event // Disable captions if setting to -1
triggerEvent.call(this, this.media, 'languagechange'); if (index === -1) {
} captions.toggle.call(this, false, passive);
return;
}
// Show captions if (!is.number(index)) {
captions.toggle.call(this, true, passive); this.debug.warn('Invalid caption argument', index);
return;
}
if (this.isHTML5 && this.isVideo) { if (!(index in tracks)) {
// If we change the active track while a cue is already displayed we need to update it this.debug.warn('Track not found', index);
captions.updateCues.call(this); return;
} }
},
// Set captions by language if (this.captions.currentTrack !== index) {
// Used internally for the language setter with the passive option forced to false this.captions.currentTrack = index;
setLanguage(input, passive = true) { const track = tracks[index];
if (!is.string(input)) { const { language } = track || {};
this.debug.warn('Invalid language argument', input);
return; // Store reference to node for invalidation on remove
} this.captions.currentTrackNode = track;
// Normalize
const language = input.toLowerCase(); // Update settings menu
controls.updateSetting.call(this, 'captions');
// When passive, don't override user preferences
if (!passive) {
this.captions.language = language; this.captions.language = language;
this.storage.set({ language });
}
// Set currentTrack // Handle Vimeo captions
const tracks = captions.getTracks.call(this); if (this.isVimeo) {
const track = captions.findTrack.call(this, [language]); this.embed.enableTextTrack(language);
captions.set.call(this, tracks.indexOf(track), passive); }
},
// Get current valid caption tracks // Trigger event
// If update is false it will also ignore tracks without metadata triggerEvent.call(this, this.media, 'languagechange');
// This is used to "freeze" the language options when captions.update is false }
getTracks(update = false) {
// Handle media or textTracks missing or null
const tracks = Array.from((this.media || {}).textTracks || []);
// For HTML5, use cache instead of current tracks when it exists (if captions.update is false)
// Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)
return tracks
.filter(track => !this.isHTML5 || update || this.captions.meta.has(track))
.filter(track => ['captions', 'subtitles'].includes(track.kind));
},
// Match tracks based on languages and get the first // Show captions
findTrack(languages, force = false) { captions.toggle.call(this, true, passive);
const tracks = captions.getTracks.call(this);
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 => { if (this.isHTML5 && this.isVideo) {
track = sorted.find(t => t.language === language); // If we change the active track while a cue is already displayed we need to update it
return !track; // Break iteration if there is a match captions.updateCues.call(this);
}); }
},
// If no match is found but is required, get first // Set captions by language
return track || (force ? sorted[0] : undefined); // Used internally for the language setter with the passive option forced to false
}, setLanguage(input, passive = true) {
if (!is.string(input)) {
this.debug.warn('Invalid language argument', input);
return;
}
// Normalize
const language = input.toLowerCase();
this.captions.language = language;
// Get the current track // Set currentTrack
getCurrentTrack() { const tracks = captions.getTracks.call(this);
return captions.getTracks.call(this)[this.currentTrack]; const track = captions.findTrack.call(this, [language]);
}, captions.set.call(this, tracks.indexOf(track), passive);
},
// Get UI label for track // Get current valid caption tracks
getLabel(track) { // If update is false it will also ignore tracks without metadata
let currentTrack = track; // This is used to "freeze" the language options when captions.update is false
getTracks(update = false) {
// Handle media or textTracks missing or null
const tracks = Array.from((this.media || {}).textTracks || []);
// For HTML5, use cache instead of current tracks when it exists (if captions.update is false)
// Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)
return tracks
.filter(track => !this.isHTML5 || update || this.captions.meta.has(track))
.filter(track => ['captions', 'subtitles'].includes(track.kind));
},
if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) { // Match tracks based on languages and get the first
currentTrack = captions.getCurrentTrack.call(this); findTrack(languages, force = false) {
} const tracks = captions.getTracks.call(this);
const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default);
const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));
let track;
if (is.track(currentTrack)) { languages.every(language => {
if (!is.empty(currentTrack.label)) { track = sorted.find(t => t.language === language);
return currentTrack.label; return !track; // Break iteration if there is a match
} });
if (!is.empty(currentTrack.language)) { // If no match is found but is required, get first
return track.language.toUpperCase(); return track || (force ? sorted[0] : undefined);
} },
return i18n.get('enabled', this.config); // Get the current track
} getCurrentTrack() {
return captions.getTracks.call(this)[this.currentTrack];
},
return i18n.get('disabled', this.config); // Get UI label for track
}, getLabel(track) {
let currentTrack = track;
// Update captions using current track's active cues if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) {
// Also optional array argument in case there isn't any track (ex: vimeo) currentTrack = captions.getCurrentTrack.call(this);
updateCues(input) { }
// Requires UI
if (!this.supported.ui) {
return;
}
if (!is.element(this.elements.captions)) { if (is.track(currentTrack)) {
this.debug.warn('No captions element to render to'); if (!is.empty(currentTrack.label)) {
return; return currentTrack.label;
} }
// Only accept array or empty input if (!is.empty(currentTrack.language)) {
if (!is.nullOrUndefined(input) && !Array.isArray(input)) { return track.language.toUpperCase();
this.debug.warn('updateCues: Invalid input', input); }
return;
}
let cues = input; return i18n.get('enabled', this.config);
}
// Get cues from track return i18n.get('disabled', this.config);
if (!cues) { },
const track = captions.getCurrentTrack.call(this);
cues = Array.from((track || {}).activeCues || []) // Update captions using current track's active cues
.map(cue => cue.getCueAsHTML()) // Also optional array argument in case there isn't any track (ex: vimeo)
.map(getHTML); updateCues(input) {
} // Requires UI
if (!this.supported.ui) {
return;
}
// Set new caption text if (!is.element(this.elements.captions)) {
const content = cues.map(cueText => cueText.trim()).join('\n'); this.debug.warn('No captions element to render to');
const changed = content !== this.elements.captions.innerHTML; return;
}
if (changed) { // Only accept array or empty input
// Empty the container and create a new child element if (!is.nullOrUndefined(input) && !Array.isArray(input)) {
emptyElement(this.elements.captions); this.debug.warn('updateCues: Invalid input', input);
const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption)); return;
caption.innerHTML = content; }
this.elements.captions.appendChild(caption);
// Trigger event let cues = input;
triggerEvent.call(this, this.media, 'cuechange');
} // Get cues from track
}, if (!cues) {
const track = captions.getCurrentTrack.call(this);
cues = Array.from((track || {}).activeCues || [])
.map(cue => cue.getCueAsHTML())
.map(getHTML);
}
// Set new caption text
const content = cues.map(cueText => cueText.trim()).join('\n');
const changed = content !== this.elements.captions.innerHTML;
if (changed) {
// Empty the container and create a new child element
emptyElement(this.elements.captions);
const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption));
caption.innerHTML = content;
this.elements.captions.appendChild(caption);
// Trigger event
triggerEvent.call(this, this.media, 'cuechange');
}
},
}; };
export default captions; export default captions;
+422 -422
View File
@@ -3,437 +3,437 @@
// ========================================================================== // ==========================================================================
const defaults = { const defaults = {
// Disable // Disable
enabled: true,
// Custom media title
title: '',
// Logging to console
debug: false,
// Auto play (if supported)
autoplay: false,
// Only allow one media playing at once (vimeo only)
autopause: true,
// Allow inline playback on iOS (this effects YouTube/Vimeo - HTML5 requires the attribute present)
// TODO: Remove iosNative fullscreen option in favour of this (logic needs work)
playsinline: true,
// Default time to skip when rewind/fast forward
seekTime: 10,
// Default volume
volume: 1,
muted: false,
// Pass a custom duration
duration: null,
// Display the media duration on load in the current time position
// If you have opted to display both duration and currentTime, this is ignored
displayDuration: true,
// Invert the current time to be a countdown
invertTime: true,
// Clicking the currentTime inverts it's value to show time left rather than elapsed
toggleInvert: true,
// Force an aspect ratio
// The format must be `'w:h'` (e.g. `'16:9'`)
ratio: null,
// Click video container to play/pause
clickToPlay: true,
// Auto hide the controls
hideControls: true,
// Reset to start when playback ended
resetOnEnd: false,
// Disable the standard context menu
disableContextMenu: true,
// Sprite (for icons)
loadSprite: true,
iconPrefix: 'plyr',
iconUrl: 'https://cdn.plyr.io/3.5.10/plyr.svg',
// Blank video (used to prevent errors on source change)
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
// Quality default
quality: {
default: 576,
// The options to display in the UI, if available for the source media
options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240],
forced: false,
onChange: null,
},
// Set loops
loop: {
active: false,
// start: null,
// end: null,
},
// Speed default and options to display
speed: {
selected: 1,
// The options to display in the UI, if available for the source media (e.g. Vimeo and YouTube only support 0.5x-4x)
options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4],
},
// Keyboard shortcut settings
keyboard: {
focused: true,
global: false,
},
// Display tooltips
tooltips: {
controls: false,
seek: true,
},
// Captions settings
captions: {
active: false,
language: 'auto',
// Listen to new tracks added after Plyr is initialized.
// This is needed for streaming captions, but may result in unselectable options
update: false,
},
// Fullscreen settings
fullscreen: {
enabled: true, // Allow fullscreen?
fallback: true, // Fallback using full viewport/window
iosNative: false, // Use the native fullscreen in iOS (disables custom controls)
},
// Local storage
storage: {
enabled: true, enabled: true,
key: 'plyr',
},
// Custom media title // Default controls
title: '', controls: [
'play-large',
// 'restart',
// 'rewind',
'play',
// 'fast-forward',
'progress',
'current-time',
// 'duration',
'mute',
'volume',
'captions',
'settings',
'pip',
'airplay',
// 'download',
'fullscreen',
],
settings: ['captions', 'quality', 'speed'],
// Logging to console // Localisation
debug: false, i18n: {
restart: 'Restart',
// Auto play (if supported) rewind: 'Rewind {seektime}s',
autoplay: false, play: 'Play',
pause: 'Pause',
// Only allow one media playing at once (vimeo only) fastForward: 'Forward {seektime}s',
autopause: true, seek: 'Seek',
seekLabel: '{currentTime} of {duration}',
// Allow inline playback on iOS (this effects YouTube/Vimeo - HTML5 requires the attribute present) played: 'Played',
// TODO: Remove iosNative fullscreen option in favour of this (logic needs work) buffered: 'Buffered',
playsinline: true, currentTime: 'Current time',
duration: 'Duration',
// Default time to skip when rewind/fast forward volume: 'Volume',
seekTime: 10, mute: 'Mute',
unmute: 'Unmute',
// Default volume enableCaptions: 'Enable captions',
volume: 1, disableCaptions: 'Disable captions',
muted: false, download: 'Download',
enterFullscreen: 'Enter fullscreen',
// Pass a custom duration exitFullscreen: 'Exit fullscreen',
duration: null, frameTitle: 'Player for {title}',
captions: 'Captions',
// Display the media duration on load in the current time position settings: 'Settings',
// If you have opted to display both duration and currentTime, this is ignored pip: 'PIP',
displayDuration: true, menuBack: 'Go back to previous menu',
speed: 'Speed',
// Invert the current time to be a countdown normal: 'Normal',
invertTime: true, quality: 'Quality',
loop: 'Loop',
// Clicking the currentTime inverts it's value to show time left rather than elapsed start: 'Start',
toggleInvert: true, end: 'End',
all: 'All',
// Force an aspect ratio reset: 'Reset',
// The format must be `'w:h'` (e.g. `'16:9'`) disabled: 'Disabled',
ratio: null, enabled: 'Enabled',
advertisement: 'Ad',
// Click video container to play/pause qualityBadge: {
clickToPlay: true, 2160: '4K',
1440: 'HD',
// Auto hide the controls 1080: 'HD',
hideControls: true, 720: 'HD',
576: 'SD',
// Reset to start when playback ended 480: 'SD',
resetOnEnd: false,
// Disable the standard context menu
disableContextMenu: true,
// Sprite (for icons)
loadSprite: true,
iconPrefix: 'plyr',
iconUrl: 'https://cdn.plyr.io/3.5.10/plyr.svg',
// Blank video (used to prevent errors on source change)
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
// Quality default
quality: {
default: 576,
// The options to display in the UI, if available for the source media
options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240],
forced: false,
onChange: null,
}, },
},
// Set loops // URLs
loop: { urls: {
active: false, download: null,
// start: null,
// end: null,
},
// Speed default and options to display
speed: {
selected: 1,
// The options to display in the UI, if available for the source media (e.g. Vimeo and YouTube only support 0.5x-4x)
options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4],
},
// Keyboard shortcut settings
keyboard: {
focused: true,
global: false,
},
// Display tooltips
tooltips: {
controls: false,
seek: true,
},
// Captions settings
captions: {
active: false,
language: 'auto',
// Listen to new tracks added after Plyr is initialized.
// This is needed for streaming captions, but may result in unselectable options
update: false,
},
// Fullscreen settings
fullscreen: {
enabled: true, // Allow fullscreen?
fallback: true, // Fallback using full viewport/window
iosNative: false, // Use the native fullscreen in iOS (disables custom controls)
},
// Local storage
storage: {
enabled: true,
key: 'plyr',
},
// Default controls
controls: [
'play-large',
// 'restart',
// 'rewind',
'play',
// 'fast-forward',
'progress',
'current-time',
// 'duration',
'mute',
'volume',
'captions',
'settings',
'pip',
'airplay',
// 'download',
'fullscreen',
],
settings: ['captions', 'quality', 'speed'],
// Localisation
i18n: {
restart: 'Restart',
rewind: 'Rewind {seektime}s',
play: 'Play',
pause: 'Pause',
fastForward: 'Forward {seektime}s',
seek: 'Seek',
seekLabel: '{currentTime} of {duration}',
played: 'Played',
buffered: 'Buffered',
currentTime: 'Current time',
duration: 'Duration',
volume: 'Volume',
mute: 'Mute',
unmute: 'Unmute',
enableCaptions: 'Enable captions',
disableCaptions: 'Disable captions',
download: 'Download',
enterFullscreen: 'Enter fullscreen',
exitFullscreen: 'Exit fullscreen',
frameTitle: 'Player for {title}',
captions: 'Captions',
settings: 'Settings',
pip: 'PIP',
menuBack: 'Go back to previous menu',
speed: 'Speed',
normal: 'Normal',
quality: 'Quality',
loop: 'Loop',
start: 'Start',
end: 'End',
all: 'All',
reset: 'Reset',
disabled: 'Disabled',
enabled: 'Enabled',
advertisement: 'Ad',
qualityBadge: {
2160: '4K',
1440: 'HD',
1080: 'HD',
720: 'HD',
576: 'SD',
480: 'SD',
},
},
// URLs
urls: {
download: null,
vimeo: {
sdk: 'https://player.vimeo.com/api/player.js',
iframe: 'https://player.vimeo.com/video/{0}?{1}',
api: 'https://vimeo.com/api/v2/video/{0}.json',
},
youtube: {
sdk: 'https://www.youtube.com/iframe_api',
api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}',
},
googleIMA: {
sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
},
},
// Custom control listeners
listeners: {
seek: null,
play: null,
pause: null,
restart: null,
rewind: null,
fastForward: null,
mute: null,
volume: null,
captions: null,
download: null,
fullscreen: null,
pip: null,
airplay: null,
speed: null,
quality: null,
loop: null,
language: null,
},
// Events to watch and bubble
events: [
// Events to watch on HTML5 media elements and bubble
// https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events
'ended',
'progress',
'stalled',
'playing',
'waiting',
'canplay',
'canplaythrough',
'loadstart',
'loadeddata',
'loadedmetadata',
'timeupdate',
'volumechange',
'play',
'pause',
'error',
'seeking',
'seeked',
'emptied',
'ratechange',
'cuechange',
// Custom events
'download',
'enterfullscreen',
'exitfullscreen',
'captionsenabled',
'captionsdisabled',
'languagechange',
'controlshidden',
'controlsshown',
'ready',
// YouTube
'statechange',
// Quality
'qualitychange',
// Ads
'adsloaded',
'adscontentpause',
'adscontentresume',
'adstarted',
'adsmidpoint',
'adscomplete',
'adsallcomplete',
'adsimpression',
'adsclick',
],
// Selectors
// Change these to match your template if using custom HTML
selectors: {
editable: 'input, textarea, select, [contenteditable]',
container: '.plyr',
controls: {
container: null,
wrapper: '.plyr__controls',
},
labels: '[data-plyr]',
buttons: {
play: '[data-plyr="play"]',
pause: '[data-plyr="pause"]',
restart: '[data-plyr="restart"]',
rewind: '[data-plyr="rewind"]',
fastForward: '[data-plyr="fast-forward"]',
mute: '[data-plyr="mute"]',
captions: '[data-plyr="captions"]',
download: '[data-plyr="download"]',
fullscreen: '[data-plyr="fullscreen"]',
pip: '[data-plyr="pip"]',
airplay: '[data-plyr="airplay"]',
settings: '[data-plyr="settings"]',
loop: '[data-plyr="loop"]',
},
inputs: {
seek: '[data-plyr="seek"]',
volume: '[data-plyr="volume"]',
speed: '[data-plyr="speed"]',
language: '[data-plyr="language"]',
quality: '[data-plyr="quality"]',
},
display: {
currentTime: '.plyr__time--current',
duration: '.plyr__time--duration',
buffer: '.plyr__progress__buffer',
loop: '.plyr__progress__loop', // Used later
volume: '.plyr__volume--display',
},
progress: '.plyr__progress',
captions: '.plyr__captions',
caption: '.plyr__caption',
},
// Class hooks added to the player in different states
classNames: {
type: 'plyr--{0}',
provider: 'plyr--{0}',
video: 'plyr__video-wrapper',
embed: 'plyr__video-embed',
videoFixedRatio: 'plyr__video-wrapper--fixed-ratio',
embedContainer: 'plyr__video-embed__container',
poster: 'plyr__poster',
posterEnabled: 'plyr__poster-enabled',
ads: 'plyr__ads',
control: 'plyr__control',
controlPressed: 'plyr__control--pressed',
playing: 'plyr--playing',
paused: 'plyr--paused',
stopped: 'plyr--stopped',
loading: 'plyr--loading',
hover: 'plyr--hover',
tooltip: 'plyr__tooltip',
cues: 'plyr__cues',
hidden: 'plyr__sr-only',
hideControls: 'plyr--hide-controls',
isIos: 'plyr--is-ios',
isTouch: 'plyr--is-touch',
uiSupported: 'plyr--full-ui',
noTransition: 'plyr--no-transition',
display: {
time: 'plyr__time',
},
menu: {
value: 'plyr__menu__value',
badge: 'plyr__badge',
open: 'plyr--menu-open',
},
captions: {
enabled: 'plyr--captions-enabled',
active: 'plyr--captions-active',
},
fullscreen: {
enabled: 'plyr--fullscreen-enabled',
fallback: 'plyr--fullscreen-fallback',
},
pip: {
supported: 'plyr--pip-supported',
active: 'plyr--pip-active',
},
airplay: {
supported: 'plyr--airplay-supported',
active: 'plyr--airplay-active',
},
tabFocus: 'plyr__tab-focus',
previewThumbnails: {
// Tooltip thumbs
thumbContainer: 'plyr__preview-thumb',
thumbContainerShown: 'plyr__preview-thumb--is-shown',
imageContainer: 'plyr__preview-thumb__image-container',
timeContainer: 'plyr__preview-thumb__time-container',
// Scrubbing
scrubbingContainer: 'plyr__preview-scrubbing',
scrubbingContainerShown: 'plyr__preview-scrubbing--is-shown',
},
},
// Embed attributes
attributes: {
embed: {
provider: 'data-plyr-provider',
id: 'data-plyr-embed-id',
},
},
// Advertisements plugin
// Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio
ads: {
enabled: false,
publisherId: '',
tagUrl: '',
},
// Preview Thumbnails plugin
previewThumbnails: {
enabled: false,
src: '',
},
// Vimeo plugin
vimeo: { vimeo: {
byline: false, sdk: 'https://player.vimeo.com/api/player.js',
portrait: false, iframe: 'https://player.vimeo.com/video/{0}?{1}',
title: false, api: 'https://vimeo.com/api/v2/video/{0}.json',
speed: true,
transparent: false,
// These settings require a pro or premium account to work
sidedock: false,
controls: false,
// Custom settings from Plyr
referrerPolicy: null, // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy
}, },
// YouTube plugin
youtube: { youtube: {
noCookie: false, // Whether to use an alternative version of YouTube without cookies sdk: 'https://www.youtube.com/iframe_api',
rel: 0, // No related vids api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}',
showinfo: 0, // Hide info
iv_load_policy: 3, // Hide annotations
modestbranding: 1, // Hide logos as much as possible (they still show one in the corner when paused)
}, },
googleIMA: {
sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
},
},
// Custom control listeners
listeners: {
seek: null,
play: null,
pause: null,
restart: null,
rewind: null,
fastForward: null,
mute: null,
volume: null,
captions: null,
download: null,
fullscreen: null,
pip: null,
airplay: null,
speed: null,
quality: null,
loop: null,
language: null,
},
// Events to watch and bubble
events: [
// Events to watch on HTML5 media elements and bubble
// https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events
'ended',
'progress',
'stalled',
'playing',
'waiting',
'canplay',
'canplaythrough',
'loadstart',
'loadeddata',
'loadedmetadata',
'timeupdate',
'volumechange',
'play',
'pause',
'error',
'seeking',
'seeked',
'emptied',
'ratechange',
'cuechange',
// Custom events
'download',
'enterfullscreen',
'exitfullscreen',
'captionsenabled',
'captionsdisabled',
'languagechange',
'controlshidden',
'controlsshown',
'ready',
// YouTube
'statechange',
// Quality
'qualitychange',
// Ads
'adsloaded',
'adscontentpause',
'adscontentresume',
'adstarted',
'adsmidpoint',
'adscomplete',
'adsallcomplete',
'adsimpression',
'adsclick',
],
// Selectors
// Change these to match your template if using custom HTML
selectors: {
editable: 'input, textarea, select, [contenteditable]',
container: '.plyr',
controls: {
container: null,
wrapper: '.plyr__controls',
},
labels: '[data-plyr]',
buttons: {
play: '[data-plyr="play"]',
pause: '[data-plyr="pause"]',
restart: '[data-plyr="restart"]',
rewind: '[data-plyr="rewind"]',
fastForward: '[data-plyr="fast-forward"]',
mute: '[data-plyr="mute"]',
captions: '[data-plyr="captions"]',
download: '[data-plyr="download"]',
fullscreen: '[data-plyr="fullscreen"]',
pip: '[data-plyr="pip"]',
airplay: '[data-plyr="airplay"]',
settings: '[data-plyr="settings"]',
loop: '[data-plyr="loop"]',
},
inputs: {
seek: '[data-plyr="seek"]',
volume: '[data-plyr="volume"]',
speed: '[data-plyr="speed"]',
language: '[data-plyr="language"]',
quality: '[data-plyr="quality"]',
},
display: {
currentTime: '.plyr__time--current',
duration: '.plyr__time--duration',
buffer: '.plyr__progress__buffer',
loop: '.plyr__progress__loop', // Used later
volume: '.plyr__volume--display',
},
progress: '.plyr__progress',
captions: '.plyr__captions',
caption: '.plyr__caption',
},
// Class hooks added to the player in different states
classNames: {
type: 'plyr--{0}',
provider: 'plyr--{0}',
video: 'plyr__video-wrapper',
embed: 'plyr__video-embed',
videoFixedRatio: 'plyr__video-wrapper--fixed-ratio',
embedContainer: 'plyr__video-embed__container',
poster: 'plyr__poster',
posterEnabled: 'plyr__poster-enabled',
ads: 'plyr__ads',
control: 'plyr__control',
controlPressed: 'plyr__control--pressed',
playing: 'plyr--playing',
paused: 'plyr--paused',
stopped: 'plyr--stopped',
loading: 'plyr--loading',
hover: 'plyr--hover',
tooltip: 'plyr__tooltip',
cues: 'plyr__cues',
hidden: 'plyr__sr-only',
hideControls: 'plyr--hide-controls',
isIos: 'plyr--is-ios',
isTouch: 'plyr--is-touch',
uiSupported: 'plyr--full-ui',
noTransition: 'plyr--no-transition',
display: {
time: 'plyr__time',
},
menu: {
value: 'plyr__menu__value',
badge: 'plyr__badge',
open: 'plyr--menu-open',
},
captions: {
enabled: 'plyr--captions-enabled',
active: 'plyr--captions-active',
},
fullscreen: {
enabled: 'plyr--fullscreen-enabled',
fallback: 'plyr--fullscreen-fallback',
},
pip: {
supported: 'plyr--pip-supported',
active: 'plyr--pip-active',
},
airplay: {
supported: 'plyr--airplay-supported',
active: 'plyr--airplay-active',
},
tabFocus: 'plyr__tab-focus',
previewThumbnails: {
// Tooltip thumbs
thumbContainer: 'plyr__preview-thumb',
thumbContainerShown: 'plyr__preview-thumb--is-shown',
imageContainer: 'plyr__preview-thumb__image-container',
timeContainer: 'plyr__preview-thumb__time-container',
// Scrubbing
scrubbingContainer: 'plyr__preview-scrubbing',
scrubbingContainerShown: 'plyr__preview-scrubbing--is-shown',
},
},
// Embed attributes
attributes: {
embed: {
provider: 'data-plyr-provider',
id: 'data-plyr-embed-id',
},
},
// Advertisements plugin
// Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio
ads: {
enabled: false,
publisherId: '',
tagUrl: '',
},
// Preview Thumbnails plugin
previewThumbnails: {
enabled: false,
src: '',
},
// Vimeo plugin
vimeo: {
byline: false,
portrait: false,
title: false,
speed: true,
transparent: false,
// These settings require a pro or premium account to work
sidedock: false,
controls: false,
// Custom settings from Plyr
referrerPolicy: null, // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy
},
// YouTube plugin
youtube: {
noCookie: false, // Whether to use an alternative version of YouTube without cookies
rel: 0, // No related vids
showinfo: 0, // Hide info
iv_load_policy: 3, // Hide annotations
modestbranding: 1, // Hide logos as much as possible (they still show one in the corner when paused)
},
}; };
export default defaults; export default defaults;
+2 -2
View File
@@ -3,8 +3,8 @@
// ========================================================================== // ==========================================================================
export const pip = { export const pip = {
active: 'picture-in-picture', active: 'picture-in-picture',
inactive: 'inline', inactive: 'inline',
}; };
export default { pip }; export default { pip };
+14 -14
View File
@@ -3,14 +3,14 @@
// ========================================================================== // ==========================================================================
export const providers = { export const providers = {
html5: 'html5', html5: 'html5',
youtube: 'youtube', youtube: 'youtube',
vimeo: 'vimeo', vimeo: 'vimeo',
}; };
export const types = { export const types = {
audio: 'audio', audio: 'audio',
video: 'video', video: 'video',
}; };
/** /**
@@ -18,17 +18,17 @@ export const types = {
* @param {String} url * @param {String} url
*/ */
export function getProviderByUrl(url) { export function getProviderByUrl(url) {
// YouTube // YouTube
if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtube-nocookie\.com|youtu\.?be)\/.+$/.test(url)) { if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtube-nocookie\.com|youtu\.?be)\/.+$/.test(url)) {
return providers.youtube; return providers.youtube;
} }
// Vimeo // Vimeo
if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) { if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) {
return providers.vimeo; return providers.vimeo;
} }
return null; return null;
} }
export default { providers, types }; export default { providers, types };
+17 -17
View File
@@ -5,26 +5,26 @@
const noop = () => {}; const noop = () => {};
export default class Console { export default class Console {
constructor(enabled = false) { constructor(enabled = false) {
this.enabled = window.console && enabled; this.enabled = window.console && enabled;
if (this.enabled) { if (this.enabled) {
this.log('Debugging enabled'); this.log('Debugging enabled');
}
} }
}
get log() { get log() {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
return this.enabled ? Function.prototype.bind.call(console.log, console) : noop; return this.enabled ? Function.prototype.bind.call(console.log, console) : noop;
} }
get warn() { get warn() {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop; return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop;
} }
get error() { get error() {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
return this.enabled ? Function.prototype.bind.call(console.error, console) : noop; return this.enabled ? Function.prototype.bind.call(console.error, console) : noop;
} }
} }
+1582 -1584
View File
File diff suppressed because it is too large Load Diff
+255 -258
View File
@@ -11,283 +11,280 @@ import is from './utils/is';
import { silencePromise } from './utils/promise'; import { silencePromise } from './utils/promise';
class Fullscreen { class Fullscreen {
constructor(player) { constructor(player) {
// Keep reference to parent // Keep reference to parent
this.player = player; this.player = player;
// Get prefix // Get prefix
this.prefix = Fullscreen.prefix; this.prefix = Fullscreen.prefix;
this.property = Fullscreen.property; this.property = Fullscreen.property;
// Scroll position // Scroll position
this.scrollPosition = { x: 0, y: 0 }; this.scrollPosition = { x: 0, y: 0 };
// Force the use of 'full window/browser' rather than fullscreen // Force the use of 'full window/browser' rather than fullscreen
this.forceFallback = player.config.fullscreen.fallback === 'force'; this.forceFallback = player.config.fullscreen.fallback === 'force';
// Register event listeners // Register event listeners
// Handle event (incase user presses escape etc) // Handle event (incase user presses escape etc)
on.call( on.call(
this.player, this.player,
document, document,
this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`, this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`,
() => { () => {
// TODO: Filter for target?? // TODO: Filter for target??
this.onChange();
},
);
// Fullscreen toggle on double click
on.call(this.player, this.player.elements.container, 'dblclick', event => {
// Ignore double click in controls
if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {
return;
}
this.toggle();
});
// Tap focus when in fullscreen
on.call(this, this.player.elements.container, 'keydown', event => this.trapFocus(event));
// Update the UI
this.update();
}
// Determine if native supported
static get native() {
return !!(
document.fullscreenEnabled ||
document.webkitFullscreenEnabled ||
document.mozFullScreenEnabled ||
document.msFullscreenEnabled
);
}
// If we're actually using native
get usingNative() {
return Fullscreen.native && !this.forceFallback;
}
// Get the prefix for handlers
static get prefix() {
// No prefix
if (is.function(document.exitFullscreen)) {
return '';
}
// Check for fullscreen support by vendor prefix
let value = '';
const prefixes = ['webkit', 'moz', 'ms'];
prefixes.some(pre => {
if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) {
value = pre;
return true;
}
return false;
});
return value;
}
static get property() {
return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';
}
// Determine if fullscreen is enabled
get enabled() {
return (
(Fullscreen.native || this.player.config.fullscreen.fallback) &&
this.player.config.fullscreen.enabled &&
this.player.supported.ui &&
this.player.isVideo
);
}
// Get active state
get active() {
if (!this.enabled) {
return false;
}
// Fallback using classname
if (!Fullscreen.native || this.forceFallback) {
return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
}
const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.property}Element`];
return element && element.shadowRoot ? element === this.target.getRootNode().host : element === this.target;
}
// Get target element
get target() {
return browser.isIos && this.player.config.fullscreen.iosNative
? this.player.media
: this.player.elements.container;
}
onChange() {
if (!this.enabled) {
return;
}
// Update toggle button
const button = this.player.elements.buttons.fullscreen;
if (is.element(button)) {
button.pressed = this.active;
}
// Trigger an event
triggerEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
}
toggleFallback(toggle = false) {
// Store or restore scroll position
if (toggle) {
this.scrollPosition = {
x: window.scrollX || 0,
y: window.scrollY || 0,
};
} else {
window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);
}
// Toggle scroll
document.body.style.overflow = toggle ? 'hidden' : '';
// Toggle class hook
toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);
// Force full viewport on iPhone X+
if (browser.isIos) {
let viewport = document.head.querySelector('meta[name="viewport"]');
const property = 'viewport-fit=cover';
// Inject the viewport meta if required
if (!viewport) {
viewport = document.createElement('meta');
viewport.setAttribute('name', 'viewport');
}
// Check if the property already exists
const hasProperty = is.string(viewport.content) && viewport.content.includes(property);
if (toggle) {
this.cleanupViewport = !hasProperty;
if (!hasProperty) {
viewport.content += `,${property}`;
}
} else if (this.cleanupViewport) {
viewport.content = viewport.content
.split(',')
.filter(part => part.trim() !== property)
.join(',');
}
}
// Toggle button and fire events
this.onChange(); this.onChange();
},
);
// Fullscreen toggle on double click
on.call(this.player, this.player.elements.container, 'dblclick', event => {
// Ignore double click in controls
if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {
return;
}
this.toggle();
});
// Tap focus when in fullscreen
on.call(this, this.player.elements.container, 'keydown', event => this.trapFocus(event));
// Update the UI
this.update();
}
// Determine if native supported
static get native() {
return !!(
document.fullscreenEnabled ||
document.webkitFullscreenEnabled ||
document.mozFullScreenEnabled ||
document.msFullscreenEnabled
);
}
// If we're actually using native
get usingNative() {
return Fullscreen.native && !this.forceFallback;
}
// Get the prefix for handlers
static get prefix() {
// No prefix
if (is.function(document.exitFullscreen)) {
return '';
} }
// Trap focus inside container // Check for fullscreen support by vendor prefix
trapFocus(event) { let value = '';
// Bail if iOS, not active, not the tab key const prefixes = ['webkit', 'moz', 'ms'];
if (browser.isIos || !this.active || event.key !== 'Tab' || event.keyCode !== 9) {
return;
}
// Get the current focused element prefixes.some(pre => {
const focused = document.activeElement; if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) {
const focusable = getElements.call( value = pre;
this.player, return true;
'a[href], button:not(:disabled), input:not(:disabled), [tabindex]', }
);
const [first] = focusable;
const last = focusable[focusable.length - 1];
if (focused === last && !event.shiftKey) { return false;
// Move focus to first element that can be tabbed if Shift isn't used });
first.focus();
event.preventDefault(); return value;
} else if (focused === first && event.shiftKey) { }
// Move focus to last element that can be tabbed if Shift is used
last.focus(); static get property() {
event.preventDefault(); return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';
} }
// Determine if fullscreen is enabled
get enabled() {
return (
(Fullscreen.native || this.player.config.fullscreen.fallback) &&
this.player.config.fullscreen.enabled &&
this.player.supported.ui &&
this.player.isVideo
);
}
// Get active state
get active() {
if (!this.enabled) {
return false;
} }
// Update UI // Fallback using classname
update() { if (!Fullscreen.native || this.forceFallback) {
if (this.enabled) { return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
let mode;
if (this.forceFallback) {
mode = 'Fallback (forced)';
} else if (Fullscreen.native) {
mode = 'Native';
} else {
mode = 'Fallback';
}
this.player.debug.log(`${mode} fullscreen enabled`);
} else {
this.player.debug.log('Fullscreen not supported and fallback disabled');
}
// Add styling hook to show button
toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.enabled);
} }
// Make an element fullscreen const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.property}Element`];
enter() {
if (!this.enabled) {
return;
}
// iOS native fullscreen doesn't need the request step return element && element.shadowRoot ? element === this.target.getRootNode().host : element === this.target;
if (browser.isIos && this.player.config.fullscreen.iosNative) { }
this.target.webkitEnterFullscreen();
} else if (!Fullscreen.native || this.forceFallback) { // Get target element
this.toggleFallback(true); get target() {
} else if (!this.prefix) { return browser.isIos && this.player.config.fullscreen.iosNative
this.target.requestFullscreen({ navigationUI: 'hide' }); ? this.player.media
} else if (!is.empty(this.prefix)) { : this.player.elements.container;
this.target[`${this.prefix}Request${this.property}`](); }
}
onChange() {
if (!this.enabled) {
return;
} }
// Bail from fullscreen // Update toggle button
exit() { const button = this.player.elements.buttons.fullscreen;
if (!this.enabled) { if (is.element(button)) {
return; button.pressed = this.active;
}
// iOS native fullscreen
if (browser.isIos && this.player.config.fullscreen.iosNative) {
this.target.webkitExitFullscreen();
silencePromise(this.player.play());
} else if (!Fullscreen.native || this.forceFallback) {
this.toggleFallback(false);
} else if (!this.prefix) {
(document.cancelFullScreen || document.exitFullscreen).call(document);
} else if (!is.empty(this.prefix)) {
const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';
document[`${this.prefix}${action}${this.property}`]();
}
} }
// Toggle state // Trigger an event
toggle() { triggerEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
if (!this.active) { }
this.enter();
} else { toggleFallback(toggle = false) {
this.exit(); // Store or restore scroll position
} if (toggle) {
this.scrollPosition = {
x: window.scrollX || 0,
y: window.scrollY || 0,
};
} else {
window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);
} }
// Toggle scroll
document.body.style.overflow = toggle ? 'hidden' : '';
// Toggle class hook
toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);
// Force full viewport on iPhone X+
if (browser.isIos) {
let viewport = document.head.querySelector('meta[name="viewport"]');
const property = 'viewport-fit=cover';
// Inject the viewport meta if required
if (!viewport) {
viewport = document.createElement('meta');
viewport.setAttribute('name', 'viewport');
}
// Check if the property already exists
const hasProperty = is.string(viewport.content) && viewport.content.includes(property);
if (toggle) {
this.cleanupViewport = !hasProperty;
if (!hasProperty) {
viewport.content += `,${property}`;
}
} else if (this.cleanupViewport) {
viewport.content = viewport.content
.split(',')
.filter(part => part.trim() !== property)
.join(',');
}
}
// Toggle button and fire events
this.onChange();
}
// Trap focus inside container
trapFocus(event) {
// Bail if iOS, not active, not the tab key
if (browser.isIos || !this.active || event.key !== 'Tab' || event.keyCode !== 9) {
return;
}
// Get the current focused element
const focused = document.activeElement;
const focusable = getElements.call(this.player, 'a[href], button:not(:disabled), input:not(:disabled), [tabindex]');
const [first] = focusable;
const last = focusable[focusable.length - 1];
if (focused === last && !event.shiftKey) {
// Move focus to first element that can be tabbed if Shift isn't used
first.focus();
event.preventDefault();
} else if (focused === first && event.shiftKey) {
// Move focus to last element that can be tabbed if Shift is used
last.focus();
event.preventDefault();
}
}
// Update UI
update() {
if (this.enabled) {
let mode;
if (this.forceFallback) {
mode = 'Fallback (forced)';
} else if (Fullscreen.native) {
mode = 'Native';
} else {
mode = 'Fallback';
}
this.player.debug.log(`${mode} fullscreen enabled`);
} else {
this.player.debug.log('Fullscreen not supported and fallback disabled');
}
// Add styling hook to show button
toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.enabled);
}
// Make an element fullscreen
enter() {
if (!this.enabled) {
return;
}
// iOS native fullscreen doesn't need the request step
if (browser.isIos && this.player.config.fullscreen.iosNative) {
this.target.webkitEnterFullscreen();
} else if (!Fullscreen.native || this.forceFallback) {
this.toggleFallback(true);
} else if (!this.prefix) {
this.target.requestFullscreen({ navigationUI: 'hide' });
} else if (!is.empty(this.prefix)) {
this.target[`${this.prefix}Request${this.property}`]();
}
}
// Bail from fullscreen
exit() {
if (!this.enabled) {
return;
}
// iOS native fullscreen
if (browser.isIos && this.player.config.fullscreen.iosNative) {
this.target.webkitExitFullscreen();
silencePromise(this.player.play());
} else if (!Fullscreen.native || this.forceFallback) {
this.toggleFallback(false);
} else if (!this.prefix) {
(document.cancelFullScreen || document.exitFullscreen).call(document);
} else if (!is.empty(this.prefix)) {
const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';
document[`${this.prefix}${action}${this.property}`]();
}
}
// Toggle state
toggle() {
if (!this.active) {
this.enter();
} else {
this.exit();
}
}
} }
export default Fullscreen; export default Fullscreen;
+122 -122
View File
@@ -10,138 +10,138 @@ import { silencePromise } from './utils/promise';
import { setAspectRatio } from './utils/style'; import { setAspectRatio } from './utils/style';
const html5 = { const html5 = {
getSources() { getSources() {
if (!this.isHTML5) { if (!this.isHTML5) {
return []; return [];
}
const sources = Array.from(this.media.querySelectorAll('source'));
// Filter out unsupported sources (if type is specified)
return sources.filter(source => {
const type = source.getAttribute('type');
if (is.empty(type)) {
return true;
}
return support.mime.call(this, type);
});
},
// Get quality levels
getQualityOptions() {
// Whether we're forcing all options (e.g. for streaming)
if (this.config.quality.forced) {
return this.config.quality.options;
}
// Get sizes from <source> elements
return html5.getSources
.call(this)
.map(source => Number(source.getAttribute('size')))
.filter(Boolean);
},
setup() {
if (!this.isHTML5) {
return;
}
const player = this;
// Set speed options from config
player.options.speed = player.config.speed.options;
// 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(s => s.getAttribute('src') === player.source);
// Return size, if match is found
return source && Number(source.getAttribute('size'));
},
set(input) {
if (player.quality === input) {
return;
} }
const sources = Array.from(this.media.querySelectorAll('source')); // If we're using an an external handler...
if (player.config.quality.forced && is.function(player.config.quality.onChange)) {
player.config.quality.onChange(input);
} else {
// Get sources
const sources = html5.getSources.call(player);
// Get first match for requested size
const source = sources.find(s => Number(s.getAttribute('size')) === input);
// Filter out unsupported sources (if type is specified) // No matching source found
return sources.filter(source => { if (!source) {
const type = source.getAttribute('type');
if (is.empty(type)) {
return true;
}
return support.mime.call(this, type);
});
},
// Get quality levels
getQualityOptions() {
// Whether we're forcing all options (e.g. for streaming)
if (this.config.quality.forced) {
return this.config.quality.options;
}
// Get sizes from <source> elements
return html5.getSources
.call(this)
.map(source => Number(source.getAttribute('size')))
.filter(Boolean);
},
setup() {
if (!this.isHTML5) {
return; return;
}
// Get current state
const { currentTime, paused, preload, readyState, playbackRate } = player.media;
// Set new source
player.media.src = source.getAttribute('src');
// Prevent loading if preload="none" and the current source isn't loaded (#1044)
if (preload !== 'none' || readyState) {
// Restore time
player.once('loadedmetadata', () => {
player.speed = playbackRate;
player.currentTime = currentTime;
// Resume playing
if (!paused) {
silencePromise(player.play());
}
});
// Load new source
player.media.load();
}
} }
const player = this; // Trigger change event
triggerEvent.call(player, player.media, 'qualitychange', false, {
// Set speed options from config quality: input,
player.options.speed = player.config.speed.options;
// 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(s => s.getAttribute('src') === player.source);
// Return size, if match is found
return source && Number(source.getAttribute('size'));
},
set(input) {
if (player.quality === input) {
return;
}
// If we're using an an external handler...
if (player.config.quality.forced && is.function(player.config.quality.onChange)) {
player.config.quality.onChange(input);
} else {
// Get sources
const sources = html5.getSources.call(player);
// Get first match for requested size
const source = sources.find(s => Number(s.getAttribute('size')) === input);
// No matching source found
if (!source) {
return;
}
// Get current state
const { currentTime, paused, preload, readyState, playbackRate } = player.media;
// Set new source
player.media.src = source.getAttribute('src');
// Prevent loading if preload="none" and the current source isn't loaded (#1044)
if (preload !== 'none' || readyState) {
// Restore time
player.once('loadedmetadata', () => {
player.speed = playbackRate;
player.currentTime = currentTime;
// Resume playing
if (!paused) {
silencePromise(player.play());
}
});
// Load new source
player.media.load();
}
}
// Trigger change event
triggerEvent.call(player, player.media, 'qualitychange', false, {
quality: input,
});
},
}); });
}, },
});
},
// Cancel current network requests // Cancel current network requests
// See https://github.com/sampotts/plyr/issues/174
cancelRequests() {
if (!this.isHTML5) {
return;
}
// Remove child sources
removeElement(html5.getSources.call(this));
// Set blank video src attribute
// This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error
// Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection
this.media.setAttribute('src', this.config.blankVideo);
// Load the new empty source
// This will cancel existing requests
// See https://github.com/sampotts/plyr/issues/174 // See https://github.com/sampotts/plyr/issues/174
cancelRequests() { this.media.load();
if (!this.isHTML5) {
return;
}
// Remove child sources // Debugging
removeElement(html5.getSources.call(this)); this.debug.log('Cancelled network requests');
},
// Set blank video src attribute
// This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error
// Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection
this.media.setAttribute('src', this.config.blankVideo);
// Load the new empty source
// This will cancel existing requests
// See https://github.com/sampotts/plyr/issues/174
this.media.load();
// Debugging
this.debug.log('Cancelled network requests');
},
}; };
export default html5; export default html5;
+805 -805
View File
File diff suppressed because it is too large Load Diff
+40 -40
View File
@@ -8,54 +8,54 @@ import youtube from './plugins/youtube';
import { createElement, toggleClass, wrap } from './utils/elements'; import { createElement, toggleClass, wrap } from './utils/elements';
const media = { const media = {
// Setup media // Setup media
setup() { setup() {
// If there's no media, bail // If there's no media, bail
if (!this.media) { if (!this.media) {
this.debug.warn('No media element found!'); this.debug.warn('No media element found!');
return; return;
} }
// Add type class // Add type class
toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true); toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true);
// Add provider class // Add provider class
toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true); toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true);
// Add video class for embeds // Add video class for embeds
// This will require changes if audio embeds are added // This will require changes if audio embeds are added
if (this.isEmbed) { if (this.isEmbed) {
toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true); toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);
} }
// Inject the player wrapper // Inject the player wrapper
if (this.isVideo) { if (this.isVideo) {
// Create the wrapper div // Create the wrapper div
this.elements.wrapper = createElement('div', { this.elements.wrapper = createElement('div', {
class: this.config.classNames.video, class: this.config.classNames.video,
}); });
// Wrap the video in a container // Wrap the video in a container
wrap(this.media, this.elements.wrapper); wrap(this.media, this.elements.wrapper);
// Faux poster container // Faux poster container
if (this.isEmbed) { if (this.isEmbed) {
this.elements.poster = createElement('div', { this.elements.poster = createElement('div', {
class: this.config.classNames.poster, class: this.config.classNames.poster,
}); });
this.elements.wrapper.appendChild(this.elements.poster); this.elements.wrapper.appendChild(this.elements.poster);
} }
} }
if (this.isHTML5) { if (this.isHTML5) {
html5.setup.call(this); html5.setup.call(this);
} else if (this.isYouTube) { } else if (this.isYouTube) {
youtube.setup.call(this); youtube.setup.call(this);
} else if (this.isVimeo) { } else if (this.isVimeo) {
vimeo.setup.call(this); vimeo.setup.call(this);
} }
}, },
}; };
export default media; export default media;
+570 -570
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+327 -327
View File
@@ -17,392 +17,392 @@ import { buildUrlParams } from '../utils/urls';
// Parse Vimeo ID from URL // Parse Vimeo ID from URL
function parseId(url) { function parseId(url) {
if (is.empty(url)) { if (is.empty(url)) {
return null; return null;
} }
if (is.number(Number(url))) { if (is.number(Number(url))) {
return url; return url;
} }
const regex = /^.*(vimeo.com\/|video\/)(\d+).*/; const regex = /^.*(vimeo.com\/|video\/)(\d+).*/;
return url.match(regex) ? RegExp.$2 : url; return url.match(regex) ? RegExp.$2 : url;
} }
// Set playback state and trigger change (only on actual change) // Set playback state and trigger change (only on actual change)
function assurePlaybackState(play) { function assurePlaybackState(play) {
if (play && !this.embed.hasPlayed) { if (play && !this.embed.hasPlayed) {
this.embed.hasPlayed = true; this.embed.hasPlayed = true;
} }
if (this.media.paused === play) { if (this.media.paused === play) {
this.media.paused = !play; this.media.paused = !play;
triggerEvent.call(this, this.media, play ? 'play' : 'pause'); triggerEvent.call(this, this.media, play ? 'play' : 'pause');
} }
} }
const vimeo = { const vimeo = {
setup() { setup() {
const player = this; const player = this;
// Add embed class for responsive // Add embed class for responsive
toggleClass(player.elements.wrapper, player.config.classNames.embed, true); toggleClass(player.elements.wrapper, player.config.classNames.embed, true);
// Set speed options from config // Set speed options from config
player.options.speed = player.config.speed.options; player.options.speed = player.config.speed.options;
// Set intial ratio // Set intial ratio
setAspectRatio.call(player); setAspectRatio.call(player);
// Load the SDK if not already // Load the SDK if not already
if (!is.object(window.Vimeo)) { if (!is.object(window.Vimeo)) {
loadScript(player.config.urls.vimeo.sdk) loadScript(player.config.urls.vimeo.sdk)
.then(() => { .then(() => {
vimeo.ready.call(player); vimeo.ready.call(player);
}) })
.catch(error => { .catch(error => {
player.debug.warn('Vimeo SDK (player.js) failed to load', error); player.debug.warn('Vimeo SDK (player.js) failed to load', error);
});
} else {
vimeo.ready.call(player);
}
},
// API Ready
ready() {
const player = this;
const config = player.config.vimeo;
// Get Vimeo params for the iframe
const params = buildUrlParams(
extend(
{},
{
loop: player.config.loop.active,
autoplay: player.autoplay,
muted: player.muted,
gesture: 'media',
playsinline: !this.config.fullscreen.iosNative,
},
config,
),
);
// Get the source URL or ID
let source = player.media.getAttribute('src');
// Get from <div> if needed
if (is.empty(source)) {
source = player.media.getAttribute(player.config.attributes.embed.id);
}
const id = parseId(source);
// Build an iframe
const iframe = createElement('iframe');
const src = format(player.config.urls.vimeo.iframe, id, params);
iframe.setAttribute('src', src);
iframe.setAttribute('allowfullscreen', '');
iframe.setAttribute('allowtransparency', '');
iframe.setAttribute('allow', 'autoplay');
// Set the referrer policy if required
if (!is.empty(config.referrerPolicy)) {
iframe.setAttribute('referrerPolicy', config.referrerPolicy);
}
// Get poster, if already set
const { poster } = player;
// Inject the package
const wrapper = createElement('div', { poster, class: player.config.classNames.embedContainer });
wrapper.appendChild(iframe);
player.media = replaceElement(wrapper, player.media);
// Get poster image
fetch(format(player.config.urls.vimeo.api, id), 'json').then(response => {
if (is.empty(response)) {
return;
}
// Get the URL for thumbnail
const url = new URL(response[0].thumbnail_large);
// Get original image
url.pathname = `${url.pathname.split('_')[0]}.jpg`;
// Set and show poster
ui.setPoster.call(player, url.href).catch(() => {});
}); });
} else {
vimeo.ready.call(player);
}
},
// Setup instance // API Ready
// https://github.com/vimeo/player.js ready() {
player.embed = new window.Vimeo.Player(iframe, { const player = this;
autopause: player.config.autopause, const config = player.config.vimeo;
muted: player.muted,
});
player.media.paused = true; // Get Vimeo params for the iframe
player.media.currentTime = 0; const params = buildUrlParams(
extend(
{},
{
loop: player.config.loop.active,
autoplay: player.autoplay,
muted: player.muted,
gesture: 'media',
playsinline: !this.config.fullscreen.iosNative,
},
config,
),
);
// Disable native text track rendering // Get the source URL or ID
if (player.supported.ui) { let source = player.media.getAttribute('src');
player.embed.disableTextTrack();
}
// Create a faux HTML5 API using the Vimeo API // Get from <div> if needed
player.media.play = () => { if (is.empty(source)) {
assurePlaybackState.call(player, true); source = player.media.getAttribute(player.config.attributes.embed.id);
return player.embed.play(); }
};
player.media.pause = () => { const id = parseId(source);
assurePlaybackState.call(player, false); // Build an iframe
return player.embed.pause(); const iframe = createElement('iframe');
}; const src = format(player.config.urls.vimeo.iframe, id, params);
iframe.setAttribute('src', src);
iframe.setAttribute('allowfullscreen', '');
iframe.setAttribute('allowtransparency', '');
iframe.setAttribute('allow', 'autoplay');
player.media.stop = () => { // Set the referrer policy if required
player.pause(); if (!is.empty(config.referrerPolicy)) {
player.currentTime = 0; iframe.setAttribute('referrerPolicy', config.referrerPolicy);
}; }
// Seeking // Get poster, if already set
let { currentTime } = player.media; const { poster } = player;
Object.defineProperty(player.media, 'currentTime', { // Inject the package
get() { const wrapper = createElement('div', { poster, class: player.config.classNames.embedContainer });
return currentTime; wrapper.appendChild(iframe);
}, player.media = replaceElement(wrapper, player.media);
set(time) {
// Vimeo will automatically play on seek if the video hasn't been played before
// Get current paused state and volume etc // Get poster image
const { embed, media, paused, volume } = player; fetch(format(player.config.urls.vimeo.api, id), 'json').then(response => {
const restorePause = paused && !embed.hasPlayed; if (is.empty(response)) {
return;
}
// Set seeking state and trigger event // Get the URL for thumbnail
media.seeking = true; const url = new URL(response[0].thumbnail_large);
triggerEvent.call(player, media, 'seeking');
// If paused, mute until seek is complete // Get original image
Promise.resolve(restorePause && embed.setVolume(0)) url.pathname = `${url.pathname.split('_')[0]}.jpg`;
// Seek
.then(() => embed.setCurrentTime(time))
// Restore paused
.then(() => restorePause && embed.pause())
// Restore volume
.then(() => restorePause && embed.setVolume(volume))
.catch(() => {
// Do nothing
});
},
});
// Playback speed // Set and show poster
let speed = player.config.speed.selected; ui.setPoster.call(player, url.href).catch(() => {});
Object.defineProperty(player.media, 'playbackRate', { });
get() {
return speed;
},
set(input) {
player.embed
.setPlaybackRate(input)
.then(() => {
speed = input;
triggerEvent.call(player, player.media, 'ratechange');
})
.catch(() => {
// Cannot set Playback Rate, Video is probably not on Pro account
player.options.speed = [1];
});
},
});
// Volume // Setup instance
let { volume } = player.config; // https://github.com/vimeo/player.js
Object.defineProperty(player.media, 'volume', { player.embed = new window.Vimeo.Player(iframe, {
get() { autopause: player.config.autopause,
return volume; muted: player.muted,
}, });
set(input) {
player.embed.setVolume(input).then(() => {
volume = input;
triggerEvent.call(player, player.media, 'volumechange');
});
},
});
// Muted player.media.paused = true;
let { muted } = player.config; player.media.currentTime = 0;
Object.defineProperty(player.media, 'muted', {
get() {
return muted;
},
set(input) {
const toggle = is.boolean(input) ? input : false;
player.embed.setVolume(toggle ? 0 : player.config.volume).then(() => { // Disable native text track rendering
muted = toggle; if (player.supported.ui) {
triggerEvent.call(player, player.media, 'volumechange'); player.embed.disableTextTrack();
}); }
},
});
// Loop // Create a faux HTML5 API using the Vimeo API
let { loop } = player.config; player.media.play = () => {
Object.defineProperty(player.media, 'loop', { assurePlaybackState.call(player, true);
get() { return player.embed.play();
return loop; };
},
set(input) {
const toggle = is.boolean(input) ? input : player.config.loop.active;
player.embed.setLoop(toggle).then(() => { player.media.pause = () => {
loop = toggle; assurePlaybackState.call(player, false);
}); return player.embed.pause();
}, };
});
// Source player.media.stop = () => {
let currentSrc; player.pause();
player.currentTime = 0;
};
// Seeking
let { currentTime } = player.media;
Object.defineProperty(player.media, 'currentTime', {
get() {
return currentTime;
},
set(time) {
// Vimeo will automatically play on seek if the video hasn't been played before
// Get current paused state and volume etc
const { embed, media, paused, volume } = player;
const restorePause = paused && !embed.hasPlayed;
// Set seeking state and trigger event
media.seeking = true;
triggerEvent.call(player, media, 'seeking');
// If paused, mute until seek is complete
Promise.resolve(restorePause && embed.setVolume(0))
// Seek
.then(() => embed.setCurrentTime(time))
// Restore paused
.then(() => restorePause && embed.pause())
// Restore volume
.then(() => restorePause && embed.setVolume(volume))
.catch(() => {
// Do nothing
});
},
});
// Playback speed
let speed = player.config.speed.selected;
Object.defineProperty(player.media, 'playbackRate', {
get() {
return speed;
},
set(input) {
player.embed player.embed
.getVideoUrl() .setPlaybackRate(input)
.then(value => { .then(() => {
currentSrc = value; speed = input;
controls.setDownloadUrl.call(player); triggerEvent.call(player, player.media, 'ratechange');
}) })
.catch(error => { .catch(() => {
this.debug.warn(error); // Cannot set Playback Rate, Video is probably not on Pro account
}); player.options.speed = [1];
});
},
});
Object.defineProperty(player.media, 'currentSrc', { // Volume
get() { let { volume } = player.config;
return currentSrc; Object.defineProperty(player.media, 'volume', {
}, get() {
return volume;
},
set(input) {
player.embed.setVolume(input).then(() => {
volume = input;
triggerEvent.call(player, player.media, 'volumechange');
}); });
},
});
// Ended // Muted
Object.defineProperty(player.media, 'ended', { let { muted } = player.config;
get() { Object.defineProperty(player.media, 'muted', {
return player.currentTime === player.duration; get() {
}, return muted;
},
set(input) {
const toggle = is.boolean(input) ? input : false;
player.embed.setVolume(toggle ? 0 : player.config.volume).then(() => {
muted = toggle;
triggerEvent.call(player, player.media, 'volumechange');
}); });
},
});
// Set aspect ratio based on video size // Loop
Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => { let { loop } = player.config;
const [width, height] = dimensions; Object.defineProperty(player.media, 'loop', {
player.embed.ratio = [width, height]; get() {
setAspectRatio.call(this); return loop;
},
set(input) {
const toggle = is.boolean(input) ? input : player.config.loop.active;
player.embed.setLoop(toggle).then(() => {
loop = toggle;
}); });
},
});
// Set autopause // Source
player.embed.setAutopause(player.config.autopause).then(state => { let currentSrc;
player.config.autopause = state; player.embed
}); .getVideoUrl()
.then(value => {
currentSrc = value;
controls.setDownloadUrl.call(player);
})
.catch(error => {
this.debug.warn(error);
});
// Get title Object.defineProperty(player.media, 'currentSrc', {
player.embed.getVideoTitle().then(title => { get() {
player.config.title = title; return currentSrc;
ui.setTitle.call(this); },
}); });
// Get current time // Ended
player.embed.getCurrentTime().then(value => { Object.defineProperty(player.media, 'ended', {
currentTime = value; get() {
triggerEvent.call(player, player.media, 'timeupdate'); return player.currentTime === player.duration;
}); },
});
// Get duration // Set aspect ratio based on video size
player.embed.getDuration().then(value => { Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => {
player.media.duration = value; const [width, height] = dimensions;
triggerEvent.call(player, player.media, 'durationchange'); player.embed.ratio = [width, height];
}); setAspectRatio.call(this);
});
// Get captions // Set autopause
player.embed.getTextTracks().then(tracks => { player.embed.setAutopause(player.config.autopause).then(state => {
player.media.textTracks = tracks; player.config.autopause = state;
captions.setup.call(player); });
});
player.embed.on('cuechange', ({ cues = [] }) => { // Get title
const strippedCues = cues.map(cue => stripHTML(cue.text)); player.embed.getVideoTitle().then(title => {
captions.updateCues.call(player, strippedCues); player.config.title = title;
}); ui.setTitle.call(this);
});
player.embed.on('loaded', () => { // Get current time
// Assure state and events are updated on autoplay player.embed.getCurrentTime().then(value => {
player.embed.getPaused().then(paused => { currentTime = value;
assurePlaybackState.call(player, !paused); triggerEvent.call(player, player.media, 'timeupdate');
if (!paused) { });
triggerEvent.call(player, player.media, 'playing');
}
});
if (is.element(player.embed.element) && player.supported.ui) { // Get duration
const frame = player.embed.element; player.embed.getDuration().then(value => {
player.media.duration = value;
triggerEvent.call(player, player.media, 'durationchange');
});
// Fix keyboard focus issues // Get captions
// https://github.com/sampotts/plyr/issues/317 player.embed.getTextTracks().then(tracks => {
frame.setAttribute('tabindex', -1); player.media.textTracks = tracks;
} captions.setup.call(player);
}); });
player.embed.on('bufferstart', () => { player.embed.on('cuechange', ({ cues = [] }) => {
triggerEvent.call(player, player.media, 'waiting'); const strippedCues = cues.map(cue => stripHTML(cue.text));
}); captions.updateCues.call(player, strippedCues);
});
player.embed.on('bufferend', () => { player.embed.on('loaded', () => {
triggerEvent.call(player, player.media, 'playing'); // Assure state and events are updated on autoplay
}); player.embed.getPaused().then(paused => {
assurePlaybackState.call(player, !paused);
if (!paused) {
triggerEvent.call(player, player.media, 'playing');
}
});
player.embed.on('play', () => { if (is.element(player.embed.element) && player.supported.ui) {
assurePlaybackState.call(player, true); const frame = player.embed.element;
triggerEvent.call(player, player.media, 'playing');
});
player.embed.on('pause', () => { // Fix keyboard focus issues
assurePlaybackState.call(player, false); // https://github.com/sampotts/plyr/issues/317
}); frame.setAttribute('tabindex', -1);
}
});
player.embed.on('timeupdate', data => { player.embed.on('bufferstart', () => {
player.media.seeking = false; triggerEvent.call(player, player.media, 'waiting');
currentTime = data.seconds; });
triggerEvent.call(player, player.media, 'timeupdate');
});
player.embed.on('progress', data => { player.embed.on('bufferend', () => {
player.media.buffered = data.percent; triggerEvent.call(player, player.media, 'playing');
triggerEvent.call(player, player.media, 'progress'); });
// Check all loaded player.embed.on('play', () => {
if (parseInt(data.percent, 10) === 1) { assurePlaybackState.call(player, true);
triggerEvent.call(player, player.media, 'canplaythrough'); triggerEvent.call(player, player.media, 'playing');
} });
// Get duration as if we do it before load, it gives an incorrect value player.embed.on('pause', () => {
// https://github.com/sampotts/plyr/issues/891 assurePlaybackState.call(player, false);
player.embed.getDuration().then(value => { });
if (value !== player.media.duration) {
player.media.duration = value;
triggerEvent.call(player, player.media, 'durationchange');
}
});
});
player.embed.on('seeked', () => { player.embed.on('timeupdate', data => {
player.media.seeking = false; player.media.seeking = false;
triggerEvent.call(player, player.media, 'seeked'); currentTime = data.seconds;
}); triggerEvent.call(player, player.media, 'timeupdate');
});
player.embed.on('ended', () => { player.embed.on('progress', data => {
player.media.paused = true; player.media.buffered = data.percent;
triggerEvent.call(player, player.media, 'ended'); triggerEvent.call(player, player.media, 'progress');
});
player.embed.on('error', detail => { // Check all loaded
player.media.error = detail; if (parseInt(data.percent, 10) === 1) {
triggerEvent.call(player, player.media, 'error'); triggerEvent.call(player, player.media, 'canplaythrough');
}); }
// Rebuild UI // Get duration as if we do it before load, it gives an incorrect value
setTimeout(() => ui.build.call(player), 0); // https://github.com/sampotts/plyr/issues/891
}, player.embed.getDuration().then(value => {
if (value !== player.media.duration) {
player.media.duration = value;
triggerEvent.call(player, player.media, 'durationchange');
}
});
});
player.embed.on('seeked', () => {
player.media.seeking = false;
triggerEvent.call(player, player.media, 'seeked');
});
player.embed.on('ended', () => {
player.media.paused = true;
triggerEvent.call(player, player.media, 'ended');
});
player.embed.on('error', detail => {
player.media.error = detail;
triggerEvent.call(player, player.media, 'error');
});
// Rebuild UI
setTimeout(() => ui.build.call(player), 0);
},
}; };
export default vimeo; export default vimeo;
+390 -390
View File
@@ -15,426 +15,426 @@ import { setAspectRatio } from '../utils/style';
// Parse YouTube ID from URL // Parse YouTube ID from URL
function parseId(url) { function parseId(url) {
if (is.empty(url)) { if (is.empty(url)) {
return null; return null;
} }
const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/; const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
return url.match(regex) ? RegExp.$2 : url; return url.match(regex) ? RegExp.$2 : url;
} }
// Set playback state and trigger change (only on actual change) // Set playback state and trigger change (only on actual change)
function assurePlaybackState(play) { function assurePlaybackState(play) {
if (play && !this.embed.hasPlayed) { if (play && !this.embed.hasPlayed) {
this.embed.hasPlayed = true; this.embed.hasPlayed = true;
} }
if (this.media.paused === play) { if (this.media.paused === play) {
this.media.paused = !play; this.media.paused = !play;
triggerEvent.call(this, this.media, play ? 'play' : 'pause'); triggerEvent.call(this, this.media, play ? 'play' : 'pause');
} }
} }
function getHost(config) { function getHost(config) {
if (config.noCookie) { if (config.noCookie) {
return 'https://www.youtube-nocookie.com'; return 'https://www.youtube-nocookie.com';
} }
if (window.location.protocol === 'http:') { if (window.location.protocol === 'http:') {
return 'http://www.youtube.com'; return 'http://www.youtube.com';
} }
// Use YouTube's default // Use YouTube's default
return undefined; return undefined;
} }
const youtube = { const youtube = {
setup() { setup() {
// Add embed class for responsive // Add embed class for responsive
toggleClass(this.elements.wrapper, this.config.classNames.embed, true); toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
// Setup API // Setup API
if (is.object(window.YT) && is.function(window.YT.Player)) { if (is.object(window.YT) && is.function(window.YT.Player)) {
youtube.ready.call(this); youtube.ready.call(this);
} else { } else {
// Reference current global callback // Reference current global callback
const callback = window.onYouTubeIframeAPIReady; const callback = window.onYouTubeIframeAPIReady;
// Set callback to process queue // Set callback to process queue
window.onYouTubeIframeAPIReady = () => { window.onYouTubeIframeAPIReady = () => {
// Call global callback if set // Call global callback if set
if (is.function(callback)) { if (is.function(callback)) {
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 youtube.ready.call(this);
getTitle(videoId) { };
const url = format(this.config.urls.youtube.api, videoId);
fetch(url) // Load the SDK
.then(data => { loadScript(this.config.urls.youtube.sdk).catch(error => {
if (is.object(data)) { this.debug.warn('YouTube API failed to load', error);
const { title, height, width } = data; });
}
},
// Set title // Get the media title
this.config.title = title; getTitle(videoId) {
ui.setTitle.call(this); const url = format(this.config.urls.youtube.api, videoId);
// Set aspect ratio fetch(url)
this.embed.ratio = [width, height]; .then(data => {
} if (is.object(data)) {
const { title, height, width } = data;
setAspectRatio.call(this); // Set title
}) this.config.title = title;
.catch(() => { ui.setTitle.call(this);
// Set aspect ratio
setAspectRatio.call(this);
});
},
// API ready // Set aspect ratio
ready() { this.embed.ratio = [width, height];
const player = this; }
// Ignore already setup (race condition)
const currentId = player.media && player.media.getAttribute('id'); setAspectRatio.call(this);
if (!is.empty(currentId) && currentId.startsWith('youtube-')) { })
.catch(() => {
// Set aspect ratio
setAspectRatio.call(this);
});
},
// API ready
ready() {
const player = this;
// Ignore already setup (race condition)
const currentId = player.media && player.media.getAttribute('id');
if (!is.empty(currentId) && currentId.startsWith('youtube-')) {
return;
}
// Get the source URL or ID
let source = player.media.getAttribute('src');
// Get from <div> if needed
if (is.empty(source)) {
source = player.media.getAttribute(this.config.attributes.embed.id);
}
// 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 = 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(src => {
// If the image is padded, use background-size "cover" instead (like youtube does too with their posters)
if (!src.includes('maxres')) {
player.elements.poster.style.backgroundSize = 'cover';
}
})
.catch(() => {});
const config = player.config.youtube;
// Setup instance
// https://developers.google.com/youtube/iframe_api_reference
player.embed = new window.YT.Player(id, {
videoId,
host: getHost(config),
playerVars: extend(
{},
{
autoplay: player.config.autoplay ? 1 : 0, // Autoplay
hl: player.config.hl, // iframe interface language
controls: player.supported.ui ? 0 : 1, // Only show controls if not fully supported
disablekb: 1, // Disable keyboard as we handle it
playsinline: !player.config.fullscreen.iosNative ? 1 : 0, // Allow iOS inline playback
// Captions are flaky on YouTube
cc_load_policy: player.captions.active ? 1 : 0,
cc_lang_pref: player.config.captions.language,
// Tracking for stats
widget_referrer: window ? window.location.href : null,
},
config,
),
events: {
onError(event) {
// YouTube may fire onError twice, so only handle it once
if (!player.media.error) {
const code = event.data;
// Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError
const message =
{
2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.',
5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.',
100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',
101: 'The owner of the requested video does not allow it to be played in embedded players.',
150: 'The owner of the requested video does not allow it to be played in embedded players.',
}[code] || 'An unknown error occured';
player.media.error = { code, message };
triggerEvent.call(player, player.media, 'error');
}
},
onPlaybackRateChange(event) {
// Get the instance
const instance = event.target;
// Get current speed
player.media.playbackRate = instance.getPlaybackRate();
triggerEvent.call(player, player.media, 'ratechange');
},
onReady(event) {
// Bail if onReady has already been called. See issue #1108
if (is.function(player.media.play)) {
return; return;
} }
// Get the instance
const instance = event.target;
// Get the source URL or ID // Get the title
let source = player.media.getAttribute('src'); youtube.getTitle.call(player, videoId);
// Get from <div> if needed // Create a faux HTML5 API using the YouTube API
if (is.empty(source)) { player.media.play = () => {
source = player.media.getAttribute(this.config.attributes.embed.id); assurePlaybackState.call(player, true);
} instance.playVideo();
};
// Replace the <iframe> with a <div> due to YouTube API issues player.media.pause = () => {
const videoId = parseId(source); assurePlaybackState.call(player, false);
const id = generateId(player.provider); instance.pauseVideo();
// 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 player.media.stop = () => {
const posterSrc = s => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`; instance.stopVideo();
};
// Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide) player.media.duration = instance.getDuration();
loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded player.media.paused = true;
.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(src => {
// If the image is padded, use background-size "cover" instead (like youtube does too with their posters)
if (!src.includes('maxres')) {
player.elements.poster.style.backgroundSize = 'cover';
}
})
.catch(() => {});
const config = player.config.youtube; // Seeking
player.media.currentTime = 0;
// Setup instance Object.defineProperty(player.media, 'currentTime', {
// https://developers.google.com/youtube/iframe_api_reference get() {
player.embed = new window.YT.Player(id, { return Number(instance.getCurrentTime());
videoId,
host: getHost(config),
playerVars: extend(
{},
{
autoplay: player.config.autoplay ? 1 : 0, // Autoplay
hl: player.config.hl, // iframe interface language
controls: player.supported.ui ? 0 : 1, // Only show controls if not fully supported
disablekb: 1, // Disable keyboard as we handle it
playsinline: !player.config.fullscreen.iosNative ? 1 : 0, // Allow iOS inline playback
// Captions are flaky on YouTube
cc_load_policy: player.captions.active ? 1 : 0,
cc_lang_pref: player.config.captions.language,
// Tracking for stats
widget_referrer: window ? window.location.href : null,
},
config,
),
events: {
onError(event) {
// YouTube may fire onError twice, so only handle it once
if (!player.media.error) {
const code = event.data;
// Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError
const message =
{
2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.',
5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.',
100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',
101: 'The owner of the requested video does not allow it to be played in embedded players.',
150: 'The owner of the requested video does not allow it to be played in embedded players.',
}[code] || 'An unknown error occured';
player.media.error = { code, message };
triggerEvent.call(player, player.media, 'error');
}
},
onPlaybackRateChange(event) {
// Get the instance
const instance = event.target;
// Get current speed
player.media.playbackRate = instance.getPlaybackRate();
triggerEvent.call(player, player.media, 'ratechange');
},
onReady(event) {
// Bail if onReady has already been called. See issue #1108
if (is.function(player.media.play)) {
return;
}
// Get the instance
const instance = event.target;
// Get the title
youtube.getTitle.call(player, videoId);
// Create a faux HTML5 API using the YouTube API
player.media.play = () => {
assurePlaybackState.call(player, true);
instance.playVideo();
};
player.media.pause = () => {
assurePlaybackState.call(player, false);
instance.pauseVideo();
};
player.media.stop = () => {
instance.stopVideo();
};
player.media.duration = instance.getDuration();
player.media.paused = true;
// Seeking
player.media.currentTime = 0;
Object.defineProperty(player.media, 'currentTime', {
get() {
return Number(instance.getCurrentTime());
},
set(time) {
// If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).
if (player.paused && !player.embed.hasPlayed) {
player.embed.mute();
}
// Set seeking state and trigger event
player.media.seeking = true;
triggerEvent.call(player, player.media, 'seeking');
// Seek after events sent
instance.seekTo(time);
},
});
// Playback speed
Object.defineProperty(player.media, 'playbackRate', {
get() {
return instance.getPlaybackRate();
},
set(input) {
instance.setPlaybackRate(input);
},
});
// Volume
let { volume } = player.config;
Object.defineProperty(player.media, 'volume', {
get() {
return volume;
},
set(input) {
volume = input;
instance.setVolume(volume * 100);
triggerEvent.call(player, player.media, 'volumechange');
},
});
// Muted
let { muted } = player.config;
Object.defineProperty(player.media, 'muted', {
get() {
return muted;
},
set(input) {
const toggle = is.boolean(input) ? input : muted;
muted = toggle;
instance[toggle ? 'mute' : 'unMute']();
triggerEvent.call(player, player.media, 'volumechange');
},
});
// Source
Object.defineProperty(player.media, 'currentSrc', {
get() {
return instance.getVideoUrl();
},
});
// Ended
Object.defineProperty(player.media, 'ended', {
get() {
return player.currentTime === player.duration;
},
});
// Get available speeds
const speeds = instance.getAvailablePlaybackRates();
// Filter based on config
player.options.speed = speeds.filter(s => player.config.speed.options.includes(s));
// Set the tabindex to avoid focus entering iframe
if (player.supported.ui) {
player.media.setAttribute('tabindex', -1);
}
triggerEvent.call(player, player.media, 'timeupdate');
triggerEvent.call(player, player.media, 'durationchange');
// Reset timer
clearInterval(player.timers.buffering);
// Setup buffering
player.timers.buffering = setInterval(() => {
// Get loaded % from YouTube
player.media.buffered = instance.getVideoLoadedFraction();
// Trigger progress only when we actually buffer something
if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {
triggerEvent.call(player, player.media, 'progress');
}
// Set last buffer point
player.media.lastBuffered = player.media.buffered;
// Bail if we're at 100%
if (player.media.buffered === 1) {
clearInterval(player.timers.buffering);
// Trigger event
triggerEvent.call(player, player.media, 'canplaythrough');
}
}, 200);
// Rebuild UI
setTimeout(() => ui.build.call(player), 50);
},
onStateChange(event) {
// Get the instance
const instance = event.target;
// Reset timer
clearInterval(player.timers.playing);
const seeked = player.media.seeking && [1, 2].includes(event.data);
if (seeked) {
// Unset seeking and fire seeked event
player.media.seeking = false;
triggerEvent.call(player, player.media, 'seeked');
}
// Handle events
// -1 Unstarted
// 0 Ended
// 1 Playing
// 2 Paused
// 3 Buffering
// 5 Video cued
switch (event.data) {
case -1:
// Update scrubber
triggerEvent.call(player, player.media, 'timeupdate');
// Get loaded % from YouTube
player.media.buffered = instance.getVideoLoadedFraction();
triggerEvent.call(player, player.media, 'progress');
break;
case 0:
assurePlaybackState.call(player, false);
// YouTube doesn't support loop for a single video, so mimick it.
if (player.media.loop) {
// YouTube needs a call to `stopVideo` before playing again
instance.stopVideo();
instance.playVideo();
} else {
triggerEvent.call(player, player.media, 'ended');
}
break;
case 1:
// Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)
if (!player.config.autoplay && player.media.paused && !player.embed.hasPlayed) {
player.media.pause();
} else {
assurePlaybackState.call(player, true);
triggerEvent.call(player, player.media, 'playing');
// Poll to get playback progress
player.timers.playing = setInterval(() => {
triggerEvent.call(player, player.media, 'timeupdate');
}, 50);
// Check duration again due to YouTube bug
// https://github.com/sampotts/plyr/issues/374
// https://code.google.com/p/gdata-issues/issues/detail?id=8690
if (player.media.duration !== instance.getDuration()) {
player.media.duration = instance.getDuration();
triggerEvent.call(player, player.media, 'durationchange');
}
}
break;
case 2:
// Restore audio (YouTube starts playing on seek if the video hasn't been played yet)
if (!player.muted) {
player.embed.unMute();
}
assurePlaybackState.call(player, false);
break;
case 3:
// Trigger waiting event to add loading classes to container as the video buffers.
triggerEvent.call(player, player.media, 'waiting');
break;
default:
break;
}
triggerEvent.call(player, player.elements.container, 'statechange', false, {
code: event.data,
});
},
}, },
}); set(time) {
}, // If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).
if (player.paused && !player.embed.hasPlayed) {
player.embed.mute();
}
// Set seeking state and trigger event
player.media.seeking = true;
triggerEvent.call(player, player.media, 'seeking');
// Seek after events sent
instance.seekTo(time);
},
});
// Playback speed
Object.defineProperty(player.media, 'playbackRate', {
get() {
return instance.getPlaybackRate();
},
set(input) {
instance.setPlaybackRate(input);
},
});
// Volume
let { volume } = player.config;
Object.defineProperty(player.media, 'volume', {
get() {
return volume;
},
set(input) {
volume = input;
instance.setVolume(volume * 100);
triggerEvent.call(player, player.media, 'volumechange');
},
});
// Muted
let { muted } = player.config;
Object.defineProperty(player.media, 'muted', {
get() {
return muted;
},
set(input) {
const toggle = is.boolean(input) ? input : muted;
muted = toggle;
instance[toggle ? 'mute' : 'unMute']();
triggerEvent.call(player, player.media, 'volumechange');
},
});
// Source
Object.defineProperty(player.media, 'currentSrc', {
get() {
return instance.getVideoUrl();
},
});
// Ended
Object.defineProperty(player.media, 'ended', {
get() {
return player.currentTime === player.duration;
},
});
// Get available speeds
const speeds = instance.getAvailablePlaybackRates();
// Filter based on config
player.options.speed = speeds.filter(s => player.config.speed.options.includes(s));
// Set the tabindex to avoid focus entering iframe
if (player.supported.ui) {
player.media.setAttribute('tabindex', -1);
}
triggerEvent.call(player, player.media, 'timeupdate');
triggerEvent.call(player, player.media, 'durationchange');
// Reset timer
clearInterval(player.timers.buffering);
// Setup buffering
player.timers.buffering = setInterval(() => {
// Get loaded % from YouTube
player.media.buffered = instance.getVideoLoadedFraction();
// Trigger progress only when we actually buffer something
if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {
triggerEvent.call(player, player.media, 'progress');
}
// Set last buffer point
player.media.lastBuffered = player.media.buffered;
// Bail if we're at 100%
if (player.media.buffered === 1) {
clearInterval(player.timers.buffering);
// Trigger event
triggerEvent.call(player, player.media, 'canplaythrough');
}
}, 200);
// Rebuild UI
setTimeout(() => ui.build.call(player), 50);
},
onStateChange(event) {
// Get the instance
const instance = event.target;
// Reset timer
clearInterval(player.timers.playing);
const seeked = player.media.seeking && [1, 2].includes(event.data);
if (seeked) {
// Unset seeking and fire seeked event
player.media.seeking = false;
triggerEvent.call(player, player.media, 'seeked');
}
// Handle events
// -1 Unstarted
// 0 Ended
// 1 Playing
// 2 Paused
// 3 Buffering
// 5 Video cued
switch (event.data) {
case -1:
// Update scrubber
triggerEvent.call(player, player.media, 'timeupdate');
// Get loaded % from YouTube
player.media.buffered = instance.getVideoLoadedFraction();
triggerEvent.call(player, player.media, 'progress');
break;
case 0:
assurePlaybackState.call(player, false);
// YouTube doesn't support loop for a single video, so mimick it.
if (player.media.loop) {
// YouTube needs a call to `stopVideo` before playing again
instance.stopVideo();
instance.playVideo();
} else {
triggerEvent.call(player, player.media, 'ended');
}
break;
case 1:
// Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)
if (!player.config.autoplay && player.media.paused && !player.embed.hasPlayed) {
player.media.pause();
} else {
assurePlaybackState.call(player, true);
triggerEvent.call(player, player.media, 'playing');
// Poll to get playback progress
player.timers.playing = setInterval(() => {
triggerEvent.call(player, player.media, 'timeupdate');
}, 50);
// Check duration again due to YouTube bug
// https://github.com/sampotts/plyr/issues/374
// https://code.google.com/p/gdata-issues/issues/detail?id=8690
if (player.media.duration !== instance.getDuration()) {
player.media.duration = instance.getDuration();
triggerEvent.call(player, player.media, 'durationchange');
}
}
break;
case 2:
// Restore audio (YouTube starts playing on seek if the video hasn't been played yet)
if (!player.muted) {
player.embed.unMute();
}
assurePlaybackState.call(player, false);
break;
case 3:
// Trigger waiting event to add loading classes to container as the video buffers.
triggerEvent.call(player, player.media, 'waiting');
break;
default:
break;
}
triggerEvent.call(player, player.elements.container, 'statechange', false, {
code: event.data,
});
},
},
});
},
}; };
export default youtube; export default youtube;
+493 -493
View File
File diff suppressed because it is too large Load Diff
+1054 -1054
View File
File diff suppressed because it is too large Load Diff
+122 -122
View File
@@ -13,146 +13,146 @@ import is from './utils/is';
import { getDeep } from './utils/objects'; import { getDeep } from './utils/objects';
const source = { const source = {
// Add elements to HTML5 media (source, tracks, etc) // Add elements to HTML5 media (source, tracks, etc)
insertElements(type, attributes) { insertElements(type, attributes) {
if (is.string(attributes)) { if (is.string(attributes)) {
insertElement(type, this.media, { insertElement(type, this.media, {
src: attributes, src: attributes,
}); });
} else if (is.array(attributes)) { } else if (is.array(attributes)) {
attributes.forEach(attribute => { attributes.forEach(attribute => {
insertElement(type, this.media, attribute); insertElement(type, this.media, attribute);
}); });
} }
}, },
// Update source // Update source
// Sources are not checked for support so be careful // Sources are not checked for support so be careful
change(input) { change(input) {
if (!getDeep(input, 'sources.length')) { if (!getDeep(input, 'sources.length')) {
this.debug.warn('Invalid source format'); this.debug.warn('Invalid source format');
return; return;
}
// Cancel current network requests
html5.cancelRequests.call(this);
// Destroy instance and re-setup
this.destroy.call(
this,
() => {
// Reset quality options
this.options.quality = [];
// Remove elements
removeElement(this.media);
this.media = null;
// Reset class name
if (is.element(this.elements.container)) {
this.elements.container.removeAttribute('class');
} }
// Cancel current network requests // Set the type and provider
html5.cancelRequests.call(this); const { sources, type } = input;
const [{ provider = providers.html5, src }] = sources;
const tagName = provider === 'html5' ? type : 'div';
const attributes = provider === 'html5' ? {} : { src };
// Destroy instance and re-setup Object.assign(this, {
this.destroy.call( provider,
this, type,
() => { // Check for support
// Reset quality options supported: support.check(type, provider, this.config.playsinline),
this.options.quality = []; // Create new element
media: createElement(tagName, attributes),
});
// Remove elements // Inject the new element
removeElement(this.media); this.elements.container.appendChild(this.media);
this.media = null;
// Reset class name // Autoplay the new source?
if (is.element(this.elements.container)) { if (is.boolean(input.autoplay)) {
this.elements.container.removeAttribute('class'); this.config.autoplay = input.autoplay;
} }
// Set the type and provider // Set attributes for audio and video
const { sources, type } = input; if (this.isHTML5) {
const [{ provider = providers.html5, src }] = sources; if (this.config.crossorigin) {
const tagName = provider === 'html5' ? type : 'div'; this.media.setAttribute('crossorigin', '');
const attributes = provider === 'html5' ? {} : { src }; }
if (this.config.autoplay) {
this.media.setAttribute('autoplay', '');
}
if (!is.empty(input.poster)) {
this.poster = input.poster;
}
if (this.config.loop.active) {
this.media.setAttribute('loop', '');
}
if (this.config.muted) {
this.media.setAttribute('muted', '');
}
if (this.config.playsinline) {
this.media.setAttribute('playsinline', '');
}
}
Object.assign(this, { // Restore class hook
provider, ui.addStyleHook.call(this);
type,
// Check for support
supported: support.check(type, provider, this.config.playsinline),
// Create new element
media: createElement(tagName, attributes),
});
// Inject the new element // Set new sources for html5
this.elements.container.appendChild(this.media); if (this.isHTML5) {
source.insertElements.call(this, 'source', sources);
}
// Autoplay the new source? // Set video title
if (is.boolean(input.autoplay)) { this.config.title = input.title;
this.config.autoplay = input.autoplay;
}
// Set attributes for audio and video // Set up from scratch
if (this.isHTML5) { media.setup.call(this);
if (this.config.crossorigin) {
this.media.setAttribute('crossorigin', '');
}
if (this.config.autoplay) {
this.media.setAttribute('autoplay', '');
}
if (!is.empty(input.poster)) {
this.poster = input.poster;
}
if (this.config.loop.active) {
this.media.setAttribute('loop', '');
}
if (this.config.muted) {
this.media.setAttribute('muted', '');
}
if (this.config.playsinline) {
this.media.setAttribute('playsinline', '');
}
}
// Restore class hook // HTML5 stuff
ui.addStyleHook.call(this); if (this.isHTML5) {
// Setup captions
if (Object.keys(input).includes('tracks')) {
source.insertElements.call(this, 'track', input.tracks);
}
}
// Set new sources for html5 // If HTML5 or embed but not fully supported, setupInterface and call ready now
if (this.isHTML5) { if (this.isHTML5 || (this.isEmbed && !this.supported.ui)) {
source.insertElements.call(this, 'source', sources); // Setup interface
} ui.build.call(this);
}
// Set video title // Load HTML5 sources
this.config.title = input.title; if (this.isHTML5) {
this.media.load();
}
// Set up from scratch // Update previewThumbnails config & reload plugin
media.setup.call(this); if (!is.empty(input.previewThumbnails)) {
Object.assign(this.config.previewThumbnails, input.previewThumbnails);
// HTML5 stuff // Cleanup previewThumbnails plugin if it was loaded
if (this.isHTML5) { if (this.previewThumbnails && this.previewThumbnails.loaded) {
// Setup captions this.previewThumbnails.destroy();
if (Object.keys(input).includes('tracks')) { this.previewThumbnails = null;
source.insertElements.call(this, 'track', input.tracks); }
}
}
// If HTML5 or embed but not fully supported, setupInterface and call ready now // Create new instance if it is still enabled
if (this.isHTML5 || (this.isEmbed && !this.supported.ui)) { if (this.config.previewThumbnails.enabled) {
// Setup interface this.previewThumbnails = new PreviewThumbnails(this);
ui.build.call(this); }
} }
// Load HTML5 sources // Update the fullscreen support
if (this.isHTML5) { this.fullscreen.update();
this.media.load(); },
} true,
);
// Update previewThumbnails config & reload plugin },
if (!is.empty(input.previewThumbnails)) {
Object.assign(this.config.previewThumbnails, input.previewThumbnails);
// Cleanup previewThumbnails plugin if it was loaded
if (this.previewThumbnails && this.previewThumbnails.loaded) {
this.previewThumbnails.destroy();
this.previewThumbnails = null;
}
// Create new instance if it is still enabled
if (this.config.previewThumbnails.enabled) {
this.previewThumbnails = new PreviewThumbnails(this);
}
}
// Update the fullscreen support
this.fullscreen.update();
},
true,
);
},
}; };
export default source; export default source;
+56 -56
View File
@@ -6,72 +6,72 @@ import is from './utils/is';
import { extend } from './utils/objects'; import { extend } from './utils/objects';
class Storage { class Storage {
constructor(player) { constructor(player) {
this.enabled = player.config.storage.enabled; this.enabled = player.config.storage.enabled;
this.key = player.config.storage.key; this.key = player.config.storage.key;
}
// Check for actual support (see if we can use it)
static get supported() {
try {
if (!('localStorage' in window)) {
return false;
}
const test = '___test';
// Try to use it (it might be disabled, e.g. user is in private mode)
// see: https://github.com/sampotts/plyr/issues/131
window.localStorage.setItem(test, test);
window.localStorage.removeItem(test);
return true;
} catch (e) {
return false;
}
}
get(key) {
if (!Storage.supported || !this.enabled) {
return null;
} }
// Check for actual support (see if we can use it) const store = window.localStorage.getItem(this.key);
static get supported() {
try {
if (!('localStorage' in window)) {
return false;
}
const test = '___test'; if (is.empty(store)) {
return null;
// Try to use it (it might be disabled, e.g. user is in private mode)
// see: https://github.com/sampotts/plyr/issues/131
window.localStorage.setItem(test, test);
window.localStorage.removeItem(test);
return true;
} catch (e) {
return false;
}
} }
get(key) { const json = JSON.parse(store);
if (!Storage.supported || !this.enabled) {
return null;
}
const store = window.localStorage.getItem(this.key); return is.string(key) && key.length ? json[key] : json;
}
if (is.empty(store)) { set(object) {
return null; // Bail if we don't have localStorage support or it's disabled
} if (!Storage.supported || !this.enabled) {
return;
const json = JSON.parse(store);
return is.string(key) && key.length ? json[key] : json;
} }
set(object) { // Can only store objectst
// Bail if we don't have localStorage support or it's disabled if (!is.object(object)) {
if (!Storage.supported || !this.enabled) { return;
return;
}
// Can only store objectst
if (!is.object(object)) {
return;
}
// Get current storage
let storage = this.get();
// Default to empty object
if (is.empty(storage)) {
storage = {};
}
// Update the working copy of the values
extend(storage, object);
// Update storage
window.localStorage.setItem(this.key, JSON.stringify(storage));
} }
// Get current storage
let storage = this.get();
// Default to empty object
if (is.empty(storage)) {
storage = {};
}
// Update the working copy of the values
extend(storage, object);
// Update storage
window.localStorage.setItem(this.key, JSON.stringify(storage));
}
} }
export default Storage; export default Storage;
+82 -82
View File
@@ -9,110 +9,110 @@ import is from './utils/is';
// Default codecs for checking mimetype support // Default codecs for checking mimetype support
const defaultCodecs = { const defaultCodecs = {
'audio/ogg': 'vorbis', 'audio/ogg': 'vorbis',
'audio/wav': '1', 'audio/wav': '1',
'video/webm': 'vp8, vorbis', 'video/webm': 'vp8, vorbis',
'video/mp4': 'avc1.42E01E, mp4a.40.2', 'video/mp4': 'avc1.42E01E, mp4a.40.2',
'video/ogg': 'theora', 'video/ogg': 'theora',
}; };
// Check for feature support // Check for feature support
const support = { const support = {
// Basic support // Basic support
audio: 'canPlayType' in document.createElement('audio'), audio: 'canPlayType' in document.createElement('audio'),
video: 'canPlayType' in document.createElement('video'), video: 'canPlayType' in document.createElement('video'),
// Check for support // Check for support
// Basic functionality vs full UI // Basic functionality vs full UI
check(type, provider, playsinline) { check(type, provider, playsinline) {
const canPlayInline = browser.isIPhone && playsinline && support.playsinline; const canPlayInline = browser.isIPhone && playsinline && support.playsinline;
const api = support[type] || provider !== 'html5'; const api = support[type] || provider !== 'html5';
const ui = api && support.rangeInput && (type !== 'video' || !browser.isIPhone || canPlayInline); const ui = api && support.rangeInput && (type !== 'video' || !browser.isIPhone || canPlayInline);
return { return {
api, api,
ui, ui,
}; };
}, },
// Picture-in-picture support // Picture-in-picture support
// Safari & Chrome only currently // Safari & Chrome only currently
pip: (() => { pip: (() => {
if (browser.isIPhone) { if (browser.isIPhone) {
return false; return false;
} }
// Safari // Safari
// https://developer.apple.com/documentation/webkitjs/adding_picture_in_picture_to_your_safari_media_controls // https://developer.apple.com/documentation/webkitjs/adding_picture_in_picture_to_your_safari_media_controls
if (is.function(createElement('video').webkitSetPresentationMode)) { if (is.function(createElement('video').webkitSetPresentationMode)) {
return true; return true;
} }
// Chrome // Chrome
// https://developers.google.com/web/updates/2018/10/watch-video-using-picture-in-picture // https://developers.google.com/web/updates/2018/10/watch-video-using-picture-in-picture
if (document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture) { if (document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture) {
return true; return true;
} }
return false; return false;
})(), })(),
// Airplay support // Airplay support
// Safari only currently // Safari only currently
airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent), airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent),
// Inline playback support // Inline playback support
// https://webkit.org/blog/6784/new-video-policies-for-ios/ // https://webkit.org/blog/6784/new-video-policies-for-ios/
playsinline: 'playsInline' in document.createElement('video'), playsinline: 'playsInline' in document.createElement('video'),
// Check for mime type support against a player instance // Check for mime type support against a player instance
// Credits: http://diveintohtml5.info/everything.html // Credits: http://diveintohtml5.info/everything.html
// Related: http://www.leanbackplayer.com/test/h5mt.html // Related: http://www.leanbackplayer.com/test/h5mt.html
mime(input) { mime(input) {
if (is.empty(input)) { if (is.empty(input)) {
return false; return false;
} }
const [mediaType] = input.split('/'); const [mediaType] = input.split('/');
let type = input; let type = input;
// Verify we're using HTML5 and there's no media type mismatch // Verify we're using HTML5 and there's no media type mismatch
if (!this.isHTML5 || mediaType !== this.type) { if (!this.isHTML5 || mediaType !== this.type) {
return false; return false;
} }
// Add codec if required // Add codec if required
if (Object.keys(defaultCodecs).includes(type)) { if (Object.keys(defaultCodecs).includes(type)) {
type += `; codecs="${defaultCodecs[input]}"`; type += `; codecs="${defaultCodecs[input]}"`;
} }
try { try {
return Boolean(type && this.media.canPlayType(type).replace(/no/, '')); return Boolean(type && this.media.canPlayType(type).replace(/no/, ''));
} catch (e) { } catch (e) {
return false; return false;
} }
}, },
// Check for textTracks support // Check for textTracks support
textTracks: 'textTracks' in document.createElement('video'), textTracks: 'textTracks' in document.createElement('video'),
// <input type="range"> Sliders // <input type="range"> Sliders
rangeInput: (() => { rangeInput: (() => {
const range = document.createElement('input'); const range = document.createElement('input');
range.type = 'range'; range.type = 'range';
return range.type === 'range'; return range.type === 'range';
})(), })(),
// Touch // Touch
// NOTE: Remember a device can be mouse + touch enabled so we check on first touch event // NOTE: Remember a device can be mouse + touch enabled so we check on first touch event
touch: 'ontouchstart' in document.documentElement, touch: 'ontouchstart' in document.documentElement,
// Detect transitions support // Detect transitions support
transitions: transitionEndEvent !== false, transitions: transitionEndEvent !== false,
// Reduced motion iOS & MacOS setting // Reduced motion iOS & MacOS setting
// https://webkit.org/blog/7551/responsive-design-for-motion/ // https://webkit.org/blog/7551/responsive-design-for-motion/
reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches, reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches,
}; };
export default support; export default support;
+208 -213
View File
@@ -13,267 +13,262 @@ import is from './utils/is';
import loadImage from './utils/load-image'; import loadImage from './utils/load-image';
const ui = { const ui = {
addStyleHook() { addStyleHook() {
toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true); toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);
toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui); toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);
}, },
// Toggle native HTML5 media controls // Toggle native HTML5 media controls
toggleNativeControls(toggle = false) { toggleNativeControls(toggle = false) {
if (toggle && this.isHTML5) { if (toggle && this.isHTML5) {
this.media.setAttribute('controls', ''); this.media.setAttribute('controls', '');
} else { } else {
this.media.removeAttribute('controls'); this.media.removeAttribute('controls');
} }
}, },
// Setup the UI // Setup the UI
build() { build() {
// Re-attach media element listeners // Re-attach media element listeners
// TODO: Use event bubbling? // TODO: Use event bubbling?
this.listeners.media(); this.listeners.media();
// Don't setup interface if no support // Don't setup interface if no support
if (!this.supported.ui) { if (!this.supported.ui) {
this.debug.warn(`Basic support only for ${this.provider} ${this.type}`); this.debug.warn(`Basic support only for ${this.provider} ${this.type}`);
// Restore native controls // Restore native controls
ui.toggleNativeControls.call(this, true); ui.toggleNativeControls.call(this, true);
// Bail // Bail
return; return;
} }
// Inject custom controls if not present // Inject custom controls if not present
if (!is.element(this.elements.controls)) { if (!is.element(this.elements.controls)) {
// Inject custom controls // Inject custom controls
controls.inject.call(this); controls.inject.call(this);
// Re-attach control listeners // Re-attach control listeners
this.listeners.controls(); this.listeners.controls();
} }
// Remove native controls // Remove native controls
ui.toggleNativeControls.call(this); ui.toggleNativeControls.call(this);
// Setup captions for HTML5 // Setup captions for HTML5
if (this.isHTML5) { if (this.isHTML5) {
captions.setup.call(this); captions.setup.call(this);
} }
// Reset volume // Reset volume
this.volume = null; this.volume = null;
// Reset mute state // Reset mute state
this.muted = null; this.muted = null;
// Reset loop state // Reset loop state
this.loop = null; this.loop = null;
// Reset quality setting // Reset quality setting
this.quality = null; this.quality = null;
// Reset speed // Reset speed
this.speed = null; this.speed = null;
// Reset volume display // Reset volume display
controls.updateVolume.call(this); controls.updateVolume.call(this);
// Reset time display // Reset time display
controls.timeUpdate.call(this); controls.timeUpdate.call(this);
// Update the UI // Update the UI
ui.checkPlaying.call(this); ui.checkPlaying.call(this);
// Check for picture-in-picture support // Check for picture-in-picture support
toggleClass( toggleClass(
this.elements.container, this.elements.container,
this.config.classNames.pip.supported, this.config.classNames.pip.supported,
support.pip && this.isHTML5 && this.isVideo, support.pip && this.isHTML5 && this.isVideo,
); );
// Check for airplay support // Check for airplay support
toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5); toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
// Add iOS class // Add iOS class
toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos); toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
// Add touch class // Add touch class
toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch); toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
// Ready for API calls // Ready for API calls
this.ready = true; this.ready = true;
// Ready event at end of execution stack // Ready event at end of execution stack
setTimeout(() => { setTimeout(() => {
triggerEvent.call(this, this.media, 'ready'); triggerEvent.call(this, this.media, 'ready');
}, 0); }, 0);
// Set the title // Set the title
ui.setTitle.call(this); ui.setTitle.call(this);
// Assure the poster image is set, if the property was added before the element was created // Assure the poster image is set, if the property was added before the element was created
if (this.poster) { if (this.poster) {
ui.setPoster.call(this, this.poster, false).catch(() => {}); ui.setPoster.call(this, this.poster, false).catch(() => {});
} }
// Manually set the duration if user has overridden it. // Manually set the duration if user has overridden it.
// The event listeners for it doesn't get called if preload is disabled (#701) // The event listeners for it doesn't get called if preload is disabled (#701)
if (this.config.duration) { if (this.config.duration) {
controls.durationUpdate.call(this); controls.durationUpdate.call(this);
} }
}, },
// Setup aria attribute for play and iframe title // Setup aria attribute for play and iframe title
setTitle() { setTitle() {
// Find the current text // Find the current text
let label = i18n.get('play', this.config); let label = i18n.get('play', this.config);
// If there's a media title set, use that for the label // If there's a media title set, use that for the label
if (is.string(this.config.title) && !is.empty(this.config.title)) { if (is.string(this.config.title) && !is.empty(this.config.title)) {
label += `, ${this.config.title}`; label += `, ${this.config.title}`;
} }
// If there's a play button, set label // If there's a play button, set label
Array.from(this.elements.buttons.play || []).forEach(button => { Array.from(this.elements.buttons.play || []).forEach(button => {
button.setAttribute('aria-label', label); button.setAttribute('aria-label', label);
}); });
// Set iframe title // Set iframe title
// https://github.com/sampotts/plyr/issues/124 // https://github.com/sampotts/plyr/issues/124
if (this.isEmbed) { if (this.isEmbed) {
const iframe = getElement.call(this, 'iframe'); const iframe = getElement.call(this, 'iframe');
if (!is.element(iframe)) { if (!is.element(iframe)) {
return; return;
} }
// Default to media type // Default to media type
const title = !is.empty(this.config.title) ? this.config.title : 'video'; const title = !is.empty(this.config.title) ? this.config.title : 'video';
const format = i18n.get('frameTitle', this.config); const format = i18n.get('frameTitle', this.config);
iframe.setAttribute('title', format.replace('{title}', title)); iframe.setAttribute('title', format.replace('{title}', title));
} }
}, },
// Toggle poster // Toggle poster
togglePoster(enable) { togglePoster(enable) {
toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable); toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);
}, },
// Set the poster image (async) // Set the poster image (async)
// Used internally for the poster setter, with the passive option forced to false // Used internally for the poster setter, with the passive option forced to false
setPoster(poster, passive = true) { setPoster(poster, passive = true) {
// Don't override if call is passive // Don't override if call is passive
if (passive && this.poster) { if (passive && this.poster) {
return Promise.reject(new Error('Poster already set')); return Promise.reject(new Error('Poster already set'));
} }
// Set property synchronously to respect the call order // Set property synchronously to respect the call order
this.media.setAttribute('poster', poster); this.media.setAttribute('poster', poster);
// HTML5 uses native poster attribute // HTML5 uses native poster attribute
if (this.isHTML5) { if (this.isHTML5) {
return Promise.resolve(poster); return Promise.resolve(poster);
} }
// Wait until ui is ready // Wait until ui is ready
return ( return (
ready ready
.call(this) .call(this)
// Load image // Load image
.then(() => loadImage(poster)) .then(() => loadImage(poster))
.catch(err => { .catch(err => {
// Hide poster on error unless it's been set by another call // Hide poster on error unless it's been set by another call
if (poster === this.poster) { if (poster === this.poster) {
ui.togglePoster.call(this, false); ui.togglePoster.call(this, false);
} }
// Rethrow // Rethrow
throw err; throw err;
}) })
.then(() => { .then(() => {
// Prevent race conditions // Prevent race conditions
if (poster !== this.poster) { if (poster !== this.poster) {
throw new Error('setPoster cancelled by later call to setPoster'); throw new Error('setPoster cancelled by later call to setPoster');
} }
}) })
.then(() => { .then(() => {
Object.assign(this.elements.poster.style, { Object.assign(this.elements.poster.style, {
backgroundImage: `url('${poster}')`, backgroundImage: `url('${poster}')`,
// Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube) // Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube)
backgroundSize: '', backgroundSize: '',
}); });
ui.togglePoster.call(this, true); ui.togglePoster.call(this, true);
return poster; return poster;
}) })
); );
}, },
// Check playing state // Check playing state
checkPlaying(event) { checkPlaying(event) {
// Class hooks // Class hooks
toggleClass(this.elements.container, this.config.classNames.playing, this.playing); toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
toggleClass(this.elements.container, this.config.classNames.paused, this.paused); toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped); toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);
// Set state // Set state
Array.from(this.elements.buttons.play || []).forEach(target => { Array.from(this.elements.buttons.play || []).forEach(target => {
Object.assign(target, { pressed: this.playing }); Object.assign(target, { pressed: this.playing });
target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config)); target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config));
}); });
// Only update controls on non timeupdate events // Only update controls on non timeupdate events
if (is.event(event) && event.type === 'timeupdate') { if (is.event(event) && event.type === 'timeupdate') {
return; return;
} }
// Toggle controls // Toggle controls
ui.toggleControls.call(this);
},
// Check if media is loading
checkLoading(event) {
this.loading = ['stalled', 'waiting'].includes(event.type);
// Clear timer
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);
// Update controls visibility
ui.toggleControls.call(this); ui.toggleControls.call(this);
}, },
this.loading ? 250 : 0,
);
},
// Check if media is loading // Toggle controls based on state and `force` argument
checkLoading(event) { toggleControls(force) {
this.loading = ['stalled', 'waiting'].includes(event.type); const { controls: controlsElement } = this.elements;
// Clear timer if (controlsElement && this.config.hideControls) {
clearTimeout(this.timers.loading); // 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();
// Timer to prevent flicker when seeking // Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide
this.timers.loading = setTimeout( this.toggleControls(
() => { Boolean(
// Update progress bar loading class state force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek,
toggleClass(this.elements.container, this.config.classNames.loading, this.loading); ),
);
// Update controls visibility }
ui.toggleControls.call(this); },
},
this.loading ? 250 : 0,
);
},
// Toggle controls based on state and `force` argument
toggleControls(force) {
const { controls: controlsElement } = this.elements;
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();
// Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide
this.toggleControls(
Boolean(
force ||
this.loading ||
this.paused ||
controlsElement.pressed ||
controlsElement.hover ||
recentTouchSeek,
),
);
}
},
}; };
export default ui; export default ui;
+21 -21
View File
@@ -5,34 +5,34 @@
import is from './is'; import is from './is';
export const transitionEndEvent = (() => { export const transitionEndEvent = (() => {
const element = document.createElement('span'); const element = document.createElement('span');
const events = { const events = {
WebkitTransition: 'webkitTransitionEnd', WebkitTransition: 'webkitTransitionEnd',
MozTransition: 'transitionend', MozTransition: 'transitionend',
OTransition: 'oTransitionEnd otransitionend', OTransition: 'oTransitionEnd otransitionend',
transition: 'transitionend', transition: 'transitionend',
}; };
const type = Object.keys(events).find(event => element.style[event] !== undefined); const type = Object.keys(events).find(event => element.style[event] !== undefined);
return is.string(type) ? events[type] : false; return is.string(type) ? events[type] : false;
})(); })();
// Force repaint of element // Force repaint of element
export function repaint(element, delay) { export function repaint(element, delay) {
setTimeout(() => { setTimeout(() => {
try { try {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
element.hidden = true; element.hidden = true;
// eslint-disable-next-line no-unused-expressions // eslint-disable-next-line no-unused-expressions
element.offsetHeight; element.offsetHeight;
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
element.hidden = false; element.hidden = false;
} catch (e) { } catch (e) {
// Do nothing // Do nothing
} }
}, delay); }, delay);
} }
+8 -8
View File
@@ -6,18 +6,18 @@ import is from './is';
// Remove duplicates in an array // Remove duplicates in an array
export function dedupe(array) { export function dedupe(array) {
if (!is.array(array)) { if (!is.array(array)) {
return array; return array;
} }
return array.filter((item, index) => array.indexOf(item) === index); return array.filter((item, index) => array.indexOf(item) === index);
} }
// Get the closest value in an array // Get the closest value in an array
export function closest(array, value) { export function closest(array, value) {
if (!is.array(array) || !array.length) { if (!is.array(array) || !array.length) {
return null; return null;
} }
return array.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev)); return array.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev));
} }
+5 -5
View File
@@ -4,11 +4,11 @@
// ========================================================================== // ==========================================================================
const browser = { const browser = {
isIE: /* @cc_on!@ */ false || !!document.documentMode, isIE: /* @cc_on!@ */ false || !!document.documentMode,
isEdge: window.navigator.userAgent.includes('Edge'), isEdge: window.navigator.userAgent.includes('Edge'),
isWebkit: 'WebkitAppearance' in document.documentElement.style && !/Edge/.test(navigator.userAgent), isWebkit: 'WebkitAppearance' in document.documentElement.style && !/Edge/.test(navigator.userAgent),
isIPhone: /(iPhone|iPod)/gi.test(navigator.platform), isIPhone: /(iPhone|iPod)/gi.test(navigator.platform),
isIos: /(iPad|iPhone|iPod)/gi.test(navigator.platform), isIos: /(iPad|iPhone|iPod)/gi.test(navigator.platform),
}; };
export default browser; export default browser;
+161 -161
View File
@@ -7,257 +7,257 @@ import { extend } from './objects';
// Wrap an element // Wrap an element
export function wrap(elements, wrapper) { export function wrap(elements, wrapper) {
// Convert `elements` to an array, if necessary. // Convert `elements` to an array, if necessary.
const targets = elements.length ? elements : [elements]; const targets = elements.length ? elements : [elements];
// Loops backwards to prevent having to clone the wrapper on the // Loops backwards to prevent having to clone the wrapper on the
// first element (see `child` below). // first element (see `child` below).
Array.from(targets) Array.from(targets)
.reverse() .reverse()
.forEach((element, index) => { .forEach((element, index) => {
const child = index > 0 ? wrapper.cloneNode(true) : wrapper; const child = index > 0 ? wrapper.cloneNode(true) : wrapper;
// Cache the current parent and sibling. // Cache the current parent and sibling.
const parent = element.parentNode; const parent = element.parentNode;
const sibling = element.nextSibling; const sibling = element.nextSibling;
// Wrap the element (is automatically removed from its current // Wrap the element (is automatically removed from its current
// parent). // parent).
child.appendChild(element); child.appendChild(element);
// If the element had a sibling, insert the wrapper before // If the element had a sibling, insert the wrapper before
// the sibling to maintain the HTML structure; otherwise, just // the sibling to maintain the HTML structure; otherwise, just
// append it to the parent. // append it to the parent.
if (sibling) { if (sibling) {
parent.insertBefore(child, sibling); parent.insertBefore(child, sibling);
} else { } else {
parent.appendChild(child); parent.appendChild(child);
} }
}); });
} }
// Set attributes // Set attributes
export function setAttributes(element, attributes) { export function setAttributes(element, attributes) {
if (!is.element(element) || is.empty(attributes)) { if (!is.element(element) || is.empty(attributes)) {
return; return;
} }
// Assume null and undefined attributes should be left out, // Assume null and undefined attributes should be left out,
// Setting them would otherwise convert them to "null" and "undefined" // Setting them would otherwise convert them to "null" and "undefined"
Object.entries(attributes) Object.entries(attributes)
.filter(([, value]) => !is.nullOrUndefined(value)) .filter(([, value]) => !is.nullOrUndefined(value))
.forEach(([key, value]) => element.setAttribute(key, value)); .forEach(([key, value]) => element.setAttribute(key, value));
} }
// Create a DocumentFragment // Create a DocumentFragment
export function createElement(type, attributes, text) { export function createElement(type, attributes, text) {
// Create a new <element> // Create a new <element>
const element = document.createElement(type); const element = document.createElement(type);
// Set all passed attributes // Set all passed attributes
if (is.object(attributes)) { if (is.object(attributes)) {
setAttributes(element, attributes); setAttributes(element, attributes);
} }
// Add text node // Add text node
if (is.string(text)) { if (is.string(text)) {
element.innerText = text; element.innerText = text;
} }
// Return built element // Return built element
return element; return element;
} }
// Inaert an element after another // Inaert an element after another
export function insertAfter(element, target) { export function insertAfter(element, target) {
if (!is.element(element) || !is.element(target)) { if (!is.element(element) || !is.element(target)) {
return; return;
} }
target.parentNode.insertBefore(element, target.nextSibling); target.parentNode.insertBefore(element, target.nextSibling);
} }
// Insert a DocumentFragment // Insert a DocumentFragment
export function insertElement(type, parent, attributes, text) { export function insertElement(type, parent, attributes, text) {
if (!is.element(parent)) { if (!is.element(parent)) {
return; return;
} }
parent.appendChild(createElement(type, attributes, text)); parent.appendChild(createElement(type, attributes, text));
} }
// Remove element(s) // Remove element(s)
export function removeElement(element) { export function removeElement(element) {
if (is.nodeList(element) || is.array(element)) { if (is.nodeList(element) || is.array(element)) {
Array.from(element).forEach(removeElement); Array.from(element).forEach(removeElement);
return; return;
} }
if (!is.element(element) || !is.element(element.parentNode)) { if (!is.element(element) || !is.element(element.parentNode)) {
return; return;
} }
element.parentNode.removeChild(element); element.parentNode.removeChild(element);
} }
// Remove all child elements // Remove all child elements
export function emptyElement(element) { export function emptyElement(element) {
if (!is.element(element)) { if (!is.element(element)) {
return; return;
} }
let { length } = element.childNodes; let { length } = element.childNodes;
while (length > 0) { while (length > 0) {
element.removeChild(element.lastChild); element.removeChild(element.lastChild);
length -= 1; length -= 1;
} }
} }
// Replace element // Replace element
export function replaceElement(newChild, oldChild) { export function replaceElement(newChild, oldChild) {
if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) { if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) {
return null; return null;
} }
oldChild.parentNode.replaceChild(newChild, oldChild); oldChild.parentNode.replaceChild(newChild, oldChild);
return newChild; return newChild;
} }
// Get an attribute object from a string selector // Get an attribute object from a string selector
export function getAttributesFromSelector(sel, existingAttributes) { export function getAttributesFromSelector(sel, existingAttributes) {
// For example: // For example:
// '.test' to { class: 'test' } // '.test' to { class: 'test' }
// '#test' to { id: 'test' } // '#test' to { id: 'test' }
// '[data-test="test"]' to { 'data-test': 'test' } // '[data-test="test"]' to { 'data-test': 'test' }
if (!is.string(sel) || is.empty(sel)) { if (!is.string(sel) || is.empty(sel)) {
return {}; return {};
} }
const attributes = {}; const attributes = {};
const existing = extend({}, existingAttributes); const existing = extend({}, existingAttributes);
sel.split(',').forEach(s => { sel.split(',').forEach(s => {
// Remove whitespace // Remove whitespace
const selector = s.trim(); const selector = s.trim();
const className = selector.replace('.', ''); const className = selector.replace('.', '');
const stripped = selector.replace(/[[\]]/g, ''); const stripped = selector.replace(/[[\]]/g, '');
// Get the parts and value // Get the parts and value
const parts = stripped.split('='); const parts = stripped.split('=');
const [key] = parts; const [key] = parts;
const value = parts.length > 1 ? parts[1].replace(/["']/g, '') : ''; const value = parts.length > 1 ? parts[1].replace(/["']/g, '') : '';
// Get the first character // Get the first character
const start = selector.charAt(0); const start = selector.charAt(0);
switch (start) { switch (start) {
case '.': case '.':
// Add to existing classname // Add to existing classname
if (is.string(existing.class)) { if (is.string(existing.class)) {
attributes.class = `${existing.class} ${className}`; attributes.class = `${existing.class} ${className}`;
} else { } else {
attributes.class = className; attributes.class = className;
}
break;
case '#':
// ID selector
attributes.id = selector.replace('#', '');
break;
case '[':
// Attribute selector
attributes[key] = value;
break;
default:
break;
} }
}); break;
return extend(existing, attributes); case '#':
// ID selector
attributes.id = selector.replace('#', '');
break;
case '[':
// Attribute selector
attributes[key] = value;
break;
default:
break;
}
});
return extend(existing, attributes);
} }
// Toggle hidden // Toggle hidden
export function toggleHidden(element, hidden) { export function toggleHidden(element, hidden) {
if (!is.element(element)) { if (!is.element(element)) {
return; return;
} }
let hide = hidden; let hide = hidden;
if (!is.boolean(hide)) { if (!is.boolean(hide)) {
hide = !element.hidden; hide = !element.hidden;
} }
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
element.hidden = hide; element.hidden = hide;
} }
// Mirror Element.classList.toggle, with IE compatibility for "force" argument // Mirror Element.classList.toggle, with IE compatibility for "force" argument
export function toggleClass(element, className, force) { export function toggleClass(element, className, force) {
if (is.nodeList(element)) { if (is.nodeList(element)) {
return Array.from(element).map(e => toggleClass(e, className, force)); return Array.from(element).map(e => toggleClass(e, className, force));
}
if (is.element(element)) {
let method = 'toggle';
if (typeof force !== 'undefined') {
method = force ? 'add' : 'remove';
} }
if (is.element(element)) { element.classList[method](className);
let method = 'toggle'; return element.classList.contains(className);
if (typeof force !== 'undefined') { }
method = force ? 'add' : 'remove';
}
element.classList[method](className); return false;
return element.classList.contains(className);
}
return false;
} }
// Has class name // Has class name
export function hasClass(element, className) { export function hasClass(element, className) {
return is.element(element) && element.classList.contains(className); return is.element(element) && element.classList.contains(className);
} }
// Element matches selector // Element matches selector
export function matches(element, selector) { export function matches(element, selector) {
const { prototype } = Element; const { prototype } = Element;
function match() { function match() {
return Array.from(document.querySelectorAll(selector)).includes(this); return Array.from(document.querySelectorAll(selector)).includes(this);
} }
const method = const method =
prototype.matches || prototype.matches ||
prototype.webkitMatchesSelector || prototype.webkitMatchesSelector ||
prototype.mozMatchesSelector || prototype.mozMatchesSelector ||
prototype.msMatchesSelector || prototype.msMatchesSelector ||
match; match;
return method.call(element, selector); return method.call(element, selector);
} }
// Find all elements // Find all elements
export function getElements(selector) { export function getElements(selector) {
return this.elements.container.querySelectorAll(selector); return this.elements.container.querySelectorAll(selector);
} }
// Find a single element // Find a single element
export function getElement(selector) { export function getElement(selector) {
return this.elements.container.querySelector(selector); return this.elements.container.querySelector(selector);
} }
// Set focus and tab focus class // Set focus and tab focus class
export function setFocus(element = null, tabFocus = false) { export function setFocus(element = null, tabFocus = false) {
if (!is.element(element)) { if (!is.element(element)) {
return; return;
} }
// Set regular focus // Set regular focus
element.focus({ preventScroll: true }); element.focus({ preventScroll: true });
// If we want to mimic keyboard focus via tab // If we want to mimic keyboard focus via tab
if (tabFocus) { if (tabFocus) {
toggleClass(element, this.config.classNames.tabFocus); toggleClass(element, this.config.classNames.tabFocus);
} }
} }
+71 -71
View File
@@ -8,110 +8,110 @@ import is from './is';
// https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md // https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md
// https://www.youtube.com/watch?v=NPM6172J22g // https://www.youtube.com/watch?v=NPM6172J22g
const supportsPassiveListeners = (() => { const supportsPassiveListeners = (() => {
// Test via a getter in the options object to see if the passive property is accessed // Test via a getter in the options object to see if the passive property is accessed
let supported = false; let supported = false;
try { try {
const options = Object.defineProperty({}, 'passive', { const options = Object.defineProperty({}, 'passive', {
get() { get() {
supported = true; supported = true;
return null; return null;
}, },
}); });
window.addEventListener('test', null, options); window.addEventListener('test', null, options);
window.removeEventListener('test', null, options); window.removeEventListener('test', null, options);
} catch (e) { } catch (e) {
// Do nothing // Do nothing
} }
return supported; return supported;
})(); })();
// Toggle event listener // Toggle event listener
export function toggleListener(element, event, callback, toggle = false, passive = true, capture = false) { export function toggleListener(element, event, callback, toggle = false, passive = true, capture = false) {
// Bail if no element, event, or callback // Bail if no element, event, or callback
if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) { if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) {
return; return;
}
// 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;
// If passive events listeners are supported
if (supportsPassiveListeners) {
options = {
// Whether the listener can be passive (i.e. default never prevented)
passive,
// Whether the listener is a capturing listener or not
capture,
};
}
// If a single node is passed, bind the event listener
events.forEach(type => {
if (this && this.eventListeners && toggle) {
// Cache event listener
this.eventListeners.push({ element, type, callback, options });
} }
// Allow multiple events element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);
const events = event.split(' '); });
// Build options
// Default to just the capture boolean for browsers with no passive listener support
let options = capture;
// If passive events listeners are supported
if (supportsPassiveListeners) {
options = {
// Whether the listener can be passive (i.e. default never prevented)
passive,
// Whether the listener is a capturing listener or not
capture,
};
}
// If a single node is passed, bind the event listener
events.forEach(type => {
if (this && this.eventListeners && toggle) {
// Cache event listener
this.eventListeners.push({ element, type, callback, options });
}
element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);
});
} }
// Bind event handler // Bind event handler
export function on(element, events = '', callback, passive = true, capture = false) { export function on(element, events = '', callback, passive = true, capture = false) {
toggleListener.call(this, element, events, callback, true, passive, capture); toggleListener.call(this, element, events, callback, true, passive, capture);
} }
// Unbind event handler // Unbind event handler
export function off(element, events = '', callback, passive = true, capture = false) { export function off(element, events = '', callback, passive = true, capture = false) {
toggleListener.call(this, element, events, callback, false, passive, capture); toggleListener.call(this, element, events, callback, false, passive, capture);
} }
// Bind once-only event handler // Bind once-only event handler
export function once(element, events = '', callback, passive = true, capture = false) { export function once(element, events = '', callback, passive = true, capture = false) {
const onceCallback = (...args) => { const onceCallback = (...args) => {
off(element, events, onceCallback, passive, capture); off(element, events, onceCallback, passive, capture);
callback.apply(this, args); callback.apply(this, args);
}; };
toggleListener.call(this, element, events, onceCallback, true, passive, capture); toggleListener.call(this, element, events, onceCallback, true, passive, capture);
} }
// Trigger event // Trigger event
export function triggerEvent(element, type = '', bubbles = false, detail = {}) { export function triggerEvent(element, type = '', bubbles = false, detail = {}) {
// Bail if no element // Bail if no element
if (!is.element(element) || is.empty(type)) { if (!is.element(element) || is.empty(type)) {
return; return;
} }
// Create and dispatch the event // Create and dispatch the event
const event = new CustomEvent(type, { const event = new CustomEvent(type, {
bubbles, bubbles,
detail: { ...detail, plyr: this }, detail: { ...detail, plyr: this },
}); });
// Dispatch the event // Dispatch the event
element.dispatchEvent(event); element.dispatchEvent(event);
} }
// Unbind all cached event listeners // Unbind all cached event listeners
export function unbindListeners() { export function unbindListeners() {
if (this && this.eventListeners) { if (this && this.eventListeners) {
this.eventListeners.forEach(item => { this.eventListeners.forEach(item => {
const { element, type, callback, options } = item; const { element, type, callback, options } = item;
element.removeEventListener(type, callback, options); element.removeEventListener(type, callback, options);
}); });
this.eventListeners = []; this.eventListeners = [];
} }
} }
// Run method when / if player is ready // Run method when / if player is ready
export function ready() { export function ready() {
return new Promise(resolve => return new Promise(resolve =>
this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve), this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve),
).then(() => {}); ).then(() => {});
} }
+32 -32
View File
@@ -4,39 +4,39 @@
// ========================================================================== // ==========================================================================
export default function fetch(url, responseType = 'text') { export default function fetch(url, responseType = 'text') {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
const request = new XMLHttpRequest(); const request = new XMLHttpRequest();
// Check for CORS support // Check for CORS support
if (!('withCredentials' in request)) { if (!('withCredentials' in request)) {
return; return;
} }
request.addEventListener('load', () => { request.addEventListener('load', () => {
if (responseType === 'text') { if (responseType === 'text') {
try { try {
resolve(JSON.parse(request.responseText)); resolve(JSON.parse(request.responseText));
} catch (e) { } catch (e) {
resolve(request.responseText); resolve(request.responseText);
} }
} else { } else {
resolve(request.response); resolve(request.response);
}
});
request.addEventListener('error', () => {
throw new Error(request.status);
});
request.open('GET', url, true);
// Set the required response type
request.responseType = responseType;
request.send();
} catch (e) {
reject(e);
} }
}); });
request.addEventListener('error', () => {
throw new Error(request.status);
});
request.open('GET', url, true);
// Set the required response type
request.responseType = responseType;
request.send();
} catch (e) {
reject(e);
}
});
} }
+25 -25
View File
@@ -8,40 +8,40 @@ import { replaceAll } from './strings';
// Skip i18n for abbreviations and brand names // Skip i18n for abbreviations and brand names
const resources = { const resources = {
pip: 'PIP', pip: 'PIP',
airplay: 'AirPlay', airplay: 'AirPlay',
html5: 'HTML5', html5: 'HTML5',
vimeo: 'Vimeo', vimeo: 'Vimeo',
youtube: 'YouTube', youtube: 'YouTube',
}; };
const i18n = { const i18n = {
get(key = '', config = {}) { get(key = '', config = {}) {
if (is.empty(key) || is.empty(config)) { if (is.empty(key) || is.empty(config)) {
return ''; return '';
} }
let string = getDeep(config.i18n, key); let string = getDeep(config.i18n, key);
if (is.empty(string)) { if (is.empty(string)) {
if (Object.keys(resources).includes(key)) { if (Object.keys(resources).includes(key)) {
return resources[key]; return resources[key];
} }
return ''; return '';
} }
const replace = { const replace = {
'{seektime}': config.seekTime, '{seektime}': config.seekTime,
'{title}': config.title, '{title}': config.title,
}; };
Object.entries(replace).forEach(([k, v]) => { Object.entries(replace).forEach(([k, v]) => {
string = replaceAll(string, k, v); string = replaceAll(string, k, v);
}); });
return string; return string;
}, },
}; };
export default i18n; export default i18n;
+39 -39
View File
@@ -22,51 +22,51 @@ const isTrack = input => instanceOf(input, TextTrack) || (!isNullOrUndefined(inp
const isPromise = input => instanceOf(input, Promise) && isFunction(input.then); const isPromise = input => instanceOf(input, Promise) && isFunction(input.then);
const isEmpty = input => const isEmpty = input =>
isNullOrUndefined(input) || isNullOrUndefined(input) ||
((isString(input) || isArray(input) || isNodeList(input)) && !input.length) || ((isString(input) || isArray(input) || isNodeList(input)) && !input.length) ||
(isObject(input) && !Object.keys(input).length); (isObject(input) && !Object.keys(input).length);
const isUrl = input => { const isUrl = input => {
// Accept a URL object // Accept a URL object
if (instanceOf(input, window.URL)) { if (instanceOf(input, window.URL)) {
return true; return true;
} }
// Must be string from here // Must be string from here
if (!isString(input)) { if (!isString(input)) {
return false; return false;
} }
// Add the protocol if required // Add the protocol if required
let string = input; let string = input;
if (!input.startsWith('http://') || !input.startsWith('https://')) { if (!input.startsWith('http://') || !input.startsWith('https://')) {
string = `http://${input}`; string = `http://${input}`;
} }
try { try {
return !isEmpty(new URL(string).hostname); return !isEmpty(new URL(string).hostname);
} catch (e) { } catch (e) {
return false; return false;
} }
}; };
export default { export default {
nullOrUndefined: isNullOrUndefined, nullOrUndefined: isNullOrUndefined,
object: isObject, object: isObject,
number: isNumber, number: isNumber,
string: isString, string: isString,
boolean: isBoolean, boolean: isBoolean,
function: isFunction, function: isFunction,
array: isArray, array: isArray,
weakMap: isWeakMap, weakMap: isWeakMap,
nodeList: isNodeList, nodeList: isNodeList,
element: isElement, element: isElement,
textNode: isTextNode, textNode: isTextNode,
event: isEvent, event: isEvent,
keyboardEvent: isKeyboardEvent, keyboardEvent: isKeyboardEvent,
cue: isCue, cue: isCue,
track: isTrack, track: isTrack,
promise: isPromise, promise: isPromise,
url: isUrl, url: isUrl,
empty: isEmpty, empty: isEmpty,
}; };
+9 -9
View File
@@ -5,15 +5,15 @@
// ========================================================================== // ==========================================================================
export default function loadImage(src, minWidth = 1) { export default function loadImage(src, minWidth = 1) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const image = new Image(); const image = new Image();
const handler = () => { const handler = () => {
delete image.onload; delete image.onload;
delete image.onerror; delete image.onerror;
(image.naturalWidth >= minWidth ? resolve : reject)(image); (image.naturalWidth >= minWidth ? resolve : reject)(image);
}; };
Object.assign(image, { onload: handler, onerror: handler, src }); Object.assign(image, { onload: handler, onerror: handler, src });
}); });
} }
+5 -5
View File
@@ -5,10 +5,10 @@
import loadjs from 'loadjs'; import loadjs from 'loadjs';
export default function loadScript(url) { export default function loadScript(url) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
loadjs(url, { loadjs(url, {
success: resolve, success: resolve,
error: reject, error: reject,
});
}); });
});
} }
+55 -55
View File
@@ -8,68 +8,68 @@ import is from './is';
// Load an external SVG sprite // Load an external SVG sprite
export default function loadSprite(url, id) { export default function loadSprite(url, id) {
if (!is.string(url)) { if (!is.string(url)) {
return; return;
}
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
if (hasId && exists()) {
return;
} }
const prefix = 'cache'; // Inject the SVG to the body
const hasId = is.string(id); document.body.insertAdjacentElement('afterbegin', container);
let isCached = false; };
const exists = () => document.getElementById(id) !== null;
const update = (container, data) => { // Only load once if ID set
// eslint-disable-next-line no-param-reassign if (!hasId || !exists()) {
container.innerHTML = data; const useStorage = Storage.supported;
// Create container
const container = document.createElement('div');
container.setAttribute('hidden', '');
// Check again incase of race condition if (hasId) {
if (hasId && exists()) { container.setAttribute('id', id);
return; }
// Check in cache
if (useStorage) {
const cached = window.localStorage.getItem(`${prefix}-${id}`);
isCached = cached !== null;
if (isCached) {
const data = JSON.parse(cached);
update(container, data.content);
}
}
// Get the sprite
fetch(url)
.then(result => {
if (is.empty(result)) {
return;
} }
// Inject the SVG to the body
document.body.insertAdjacentElement('afterbegin', container);
};
// Only load once if ID set
if (!hasId || !exists()) {
const useStorage = Storage.supported;
// Create container
const container = document.createElement('div');
container.setAttribute('hidden', '');
if (hasId) {
container.setAttribute('id', id);
}
// Check in cache
if (useStorage) { if (useStorage) {
const cached = window.localStorage.getItem(`${prefix}-${id}`); window.localStorage.setItem(
isCached = cached !== null; `${prefix}-${id}`,
JSON.stringify({
if (isCached) { content: result,
const data = JSON.parse(cached); }),
update(container, data.content); );
}
} }
// Get the sprite update(container, result);
fetch(url) })
.then(result => { .catch(() => {});
if (is.empty(result)) { }
return;
}
if (useStorage) {
window.localStorage.setItem(
`${prefix}-${id}`,
JSON.stringify({
content: result,
}),
);
}
update(container, result);
})
.catch(() => {});
}
} }
+1 -1
View File
@@ -11,7 +11,7 @@
* @type Number * @type Number
*/ */
export function clamp(input = 0, min = 0, max = 255) { export function clamp(input = 0, min = 0, max = 255) {
return Math.min(Math.max(input, min), max); return Math.min(Math.max(input, min), max);
} }
export default { clamp }; export default { clamp };
+23 -23
View File
@@ -6,37 +6,37 @@ import is from './is';
// Clone nested objects // Clone nested objects
export function cloneDeep(object) { export function cloneDeep(object) {
return JSON.parse(JSON.stringify(object)); return JSON.parse(JSON.stringify(object));
} }
// Get a nested value in an object // Get a nested value in an object
export function getDeep(object, path) { export function getDeep(object, path) {
return path.split('.').reduce((obj, key) => obj && obj[key], object); return path.split('.').reduce((obj, key) => obj && obj[key], object);
} }
// Deep extend destination object with N more objects // Deep extend destination object with N more objects
export function extend(target = {}, ...sources) { export function extend(target = {}, ...sources) {
if (!sources.length) { if (!sources.length) {
return target; return target;
}
const source = sources.shift();
if (!is.object(source)) {
return target;
}
Object.keys(source).forEach(key => {
if (is.object(source[key])) {
if (!Object.keys(target).includes(key)) {
Object.assign(target, { [key]: {} });
}
extend(target[key], source[key]);
} else {
Object.assign(target, { [key]: source[key] });
} }
});
const source = sources.shift(); return extend(target, ...sources);
if (!is.object(source)) {
return target;
}
Object.keys(source).forEach(key => {
if (is.object(source[key])) {
if (!Object.keys(target).includes(key)) {
Object.assign(target, { [key]: {} });
}
extend(target[key], source[key]);
} else {
Object.assign(target, { [key]: source[key] });
}
});
return extend(target, ...sources);
} }
+3 -3
View File
@@ -6,9 +6,9 @@ import is from './is';
* @param {Object} value An object that may or may not be `Promise`-like. * @param {Object} value An object that may or may not be `Promise`-like.
*/ */
export function silencePromise(value) { export function silencePromise(value) {
if (is.promise(value)) { if (is.promise(value)) {
value.then(null, () => {}); value.then(null, () => {});
} }
} }
export default { silencePromise }; export default { silencePromise };
+35 -40
View File
@@ -6,80 +6,75 @@ import is from './is';
// Generate a random ID // Generate a random ID
export function generateId(prefix) { export function generateId(prefix) {
return `${prefix}-${Math.floor(Math.random() * 10000)}`; return `${prefix}-${Math.floor(Math.random() * 10000)}`;
} }
// Format string // Format string
export function format(input, ...args) { export function format(input, ...args) {
if (is.empty(input)) { if (is.empty(input)) {
return input; return input;
} }
return input.toString().replace(/{(\d+)}/g, (match, i) => args[i].toString()); return input.toString().replace(/{(\d+)}/g, (match, i) => args[i].toString());
} }
// Get percentage // Get percentage
export function getPercentage(current, max) { export function getPercentage(current, max) {
if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) { if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {
return 0; return 0;
} }
return ((current / max) * 100).toFixed(2); return ((current / max) * 100).toFixed(2);
} }
// Replace all occurances of a string in a string // Replace all occurances of a string in a string
export function replaceAll(input = '', find = '', replace = '') { export const replaceAll = (input = '', find = '', replace = '') =>
return input.replace( input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString());
new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'),
replace.toString(),
);
}
// Convert to title case // Convert to title case
export function toTitleCase(input = '') { export const toTitleCase = (input = '') =>
return input.toString().replace(/\w\S*/g, text => text.charAt(0).toUpperCase() + text.substr(1).toLowerCase()); input.toString().replace(/\w\S*/g, text => text.charAt(0).toUpperCase() + text.substr(1).toLowerCase());
}
// Convert string to pascalCase // Convert string to pascalCase
export function toPascalCase(input = '') { export function toPascalCase(input = '') {
let string = input.toString(); let string = input.toString();
// Convert kebab case // Convert kebab case
string = replaceAll(string, '-', ' '); string = replaceAll(string, '-', ' ');
// Convert snake case // Convert snake case
string = replaceAll(string, '_', ' '); string = replaceAll(string, '_', ' ');
// Convert to title case // Convert to title case
string = toTitleCase(string); string = toTitleCase(string);
// Convert to pascal case // Convert to pascal case
return replaceAll(string, ' ', ''); return replaceAll(string, ' ', '');
} }
// Convert string to pascalCase // Convert string to pascalCase
export function toCamelCase(input = '') { export function toCamelCase(input = '') {
let string = input.toString(); let string = input.toString();
// Convert to pascal case // Convert to pascal case
string = toPascalCase(string); string = toPascalCase(string);
// Convert first character to lowercase // Convert first character to lowercase
return string.charAt(0).toLowerCase() + string.slice(1); return string.charAt(0).toLowerCase() + string.slice(1);
} }
// Remove HTML from a string // Remove HTML from a string
export function stripHTML(source) { export function stripHTML(source) {
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
const element = document.createElement('div'); const element = document.createElement('div');
fragment.appendChild(element); fragment.appendChild(element);
element.innerHTML = source; element.innerHTML = source;
return fragment.firstChild.innerText; return fragment.firstChild.innerText;
} }
// Like outerHTML, but also works for DocumentFragment // Like outerHTML, but also works for DocumentFragment
export function getHTML(element) { export function getHTML(element) {
const wrapper = document.createElement('div'); const wrapper = document.createElement('div');
wrapper.appendChild(element); wrapper.appendChild(element);
return wrapper.innerHTML; return wrapper.innerHTML;
} }
+46 -46
View File
@@ -5,74 +5,74 @@
import is from './is'; import is from './is';
export function validateRatio(input) { export function validateRatio(input) {
if (!is.array(input) && (!is.string(input) || !input.includes(':'))) { if (!is.array(input) && (!is.string(input) || !input.includes(':'))) {
return false; return false;
} }
const ratio = is.array(input) ? input : input.split(':'); const ratio = is.array(input) ? input : input.split(':');
return ratio.map(Number).every(is.number); return ratio.map(Number).every(is.number);
} }
export function reduceAspectRatio(ratio) { export function reduceAspectRatio(ratio) {
if (!is.array(ratio) || !ratio.every(is.number)) { if (!is.array(ratio) || !ratio.every(is.number)) {
return null; return null;
} }
const [width, height] = ratio; const [width, height] = ratio;
const getDivider = (w, h) => (h === 0 ? w : getDivider(h, w % h)); const getDivider = (w, h) => (h === 0 ? w : getDivider(h, w % h));
const divider = getDivider(width, height); const divider = getDivider(width, height);
return [width / divider, height / divider]; return [width / divider, height / divider];
} }
export function getAspectRatio(input) { export function getAspectRatio(input) {
const parse = ratio => (validateRatio(ratio) ? ratio.split(':').map(Number) : null); const parse = ratio => (validateRatio(ratio) ? ratio.split(':').map(Number) : null);
// Try provided ratio // Try provided ratio
let ratio = parse(input); let ratio = parse(input);
// Get from config // Get from config
if (ratio === null) { if (ratio === null) {
ratio = parse(this.config.ratio); ratio = parse(this.config.ratio);
} }
// Get from embed // Get from embed
if (ratio === null && !is.empty(this.embed) && is.array(this.embed.ratio)) { if (ratio === null && !is.empty(this.embed) && is.array(this.embed.ratio)) {
({ ratio } = this.embed); ({ ratio } = this.embed);
} }
// Get from HTML5 video // Get from HTML5 video
if (ratio === null && this.isHTML5) { if (ratio === null && this.isHTML5) {
const { videoWidth, videoHeight } = this.media; const { videoWidth, videoHeight } = this.media;
ratio = reduceAspectRatio([videoWidth, videoHeight]); ratio = reduceAspectRatio([videoWidth, videoHeight]);
} }
return ratio; return ratio;
} }
// Set aspect ratio for responsive container // Set aspect ratio for responsive container
export function setAspectRatio(input) { export function setAspectRatio(input) {
if (!this.isVideo) { if (!this.isVideo) {
return {}; return {};
} }
const { wrapper } = this.elements; const { wrapper } = this.elements;
const ratio = getAspectRatio.call(this, input); const ratio = getAspectRatio.call(this, input);
const [w, h] = is.array(ratio) ? ratio : [0, 0]; const [w, h] = is.array(ratio) ? ratio : [0, 0];
const padding = (100 / w) * h; const padding = (100 / w) * h;
wrapper.style.paddingBottom = `${padding}%`; wrapper.style.paddingBottom = `${padding}%`;
// For Vimeo we have an extra <div> to hide the standard controls and UI // For Vimeo we have an extra <div> to hide the standard controls and UI
if (this.isVimeo && this.supported.ui) { if (this.isVimeo && this.supported.ui) {
const height = 240; const height = 240;
const offset = (height - padding) / (height / 50); const offset = (height - padding) / (height / 50);
this.media.style.transform = `translateY(-${offset}%)`; this.media.style.transform = `translateY(-${offset}%)`;
} else if (this.isHTML5) { } else if (this.isHTML5) {
wrapper.classList.toggle(this.config.classNames.videoFixedRatio, ratio !== null); wrapper.classList.toggle(this.config.classNames.videoFixedRatio, ratio !== null);
} }
return { padding, ratio }; return { padding, ratio };
} }
export default { setAspectRatio }; export default { setAspectRatio };
+18 -18
View File
@@ -11,25 +11,25 @@ export const getSeconds = value => Math.trunc(value % 60, 10);
// Format time to UI friendly string // Format time to UI friendly string
export function formatTime(time = 0, displayHours = false, inverted = false) { export function formatTime(time = 0, displayHours = false, inverted = false) {
// Bail if the value isn't a number // Bail if the value isn't a number
if (!is.number(time)) { if (!is.number(time)) {
return formatTime(undefined, displayHours, inverted); return formatTime(undefined, displayHours, inverted);
} }
// Format time component to add leading zero // Format time component to add leading zero
const format = value => `0${value}`.slice(-2); const format = value => `0${value}`.slice(-2);
// Breakdown to hours, mins, secs // Breakdown to hours, mins, secs
let hours = getHours(time); let hours = getHours(time);
const mins = getMinutes(time); const mins = getMinutes(time);
const secs = getSeconds(time); const secs = getSeconds(time);
// Do we need to display hours? // Do we need to display hours?
if (displayHours || hours > 0) { if (displayHours || hours > 0) {
hours = `${hours}:`; hours = `${hours}:`;
} else { } else {
hours = ''; hours = '';
} }
// Render // Render
return `${inverted && time > 0 ? '-' : ''}${hours}${format(mins)}:${format(secs)}`; return `${inverted && time > 0 ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;
} }
+18 -18
View File
@@ -10,30 +10,30 @@ import is from './is';
* @param {Boolean} safe - failsafe parsing * @param {Boolean} safe - failsafe parsing
*/ */
export function parseUrl(input, safe = true) { export function parseUrl(input, safe = true) {
let url = input; let url = input;
if (safe) { if (safe) {
const parser = document.createElement('a'); const parser = document.createElement('a');
parser.href = url; parser.href = url;
url = parser.href; url = parser.href;
} }
try { try {
return new URL(url); return new URL(url);
} catch (e) { } catch (e) {
return null; return null;
} }
} }
// Convert object to URLSearchParams // Convert object to URLSearchParams
export function buildUrlParams(input) { export function buildUrlParams(input) {
const params = new URLSearchParams(); const params = new URLSearchParams();
if (is.object(input)) { if (is.object(input)) {
Object.entries(input).forEach(([key, value]) => { Object.entries(input).forEach(([key, value]) => {
params.set(key, value); params.set(key, value);
}); });
} }
return params; return params;
} }
+47 -47
View File
@@ -4,66 +4,66 @@
// Base // Base
.plyr { .plyr {
@include plyr-font-smoothing($plyr-font-smoothing); @include plyr-font-smoothing($plyr-font-smoothing);
align-items: center; align-items: center;
direction: ltr; direction: ltr;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
font-family: var(--plyr-font-family); font-family: $plyr-font-family;
font-variant-numeric: tabular-nums; // Force monosace-esque number widths font-variant-numeric: tabular-nums; // Force monosace-esque number widths
font-weight: var(--plyr-font-weight-regular); font-weight: $plyr-font-weight-regular;
height: 100%;
line-height: $plyr-line-height;
max-width: 100%;
min-width: 200px;
position: relative;
text-shadow: none;
transition: box-shadow 0.3s ease;
z-index: 0; // Force any border radius
// Media elements
video,
audio,
iframe {
display: block;
height: 100%; height: 100%;
line-height: var(--plyr-line-height); width: 100%;
max-width: 100%; }
min-width: 200px;
position: relative;
text-shadow: none;
transition: box-shadow 0.3s ease;
z-index: 0; // Force any border radius
// Media elements button {
video, font: inherit;
audio, line-height: inherit;
iframe { width: auto;
display: block; }
height: 100%;
width: 100%;
}
button { // Ignore focus
font: inherit; &:focus {
line-height: inherit; outline: 0;
width: auto; }
}
// Ignore focus
&:focus {
outline: 0;
}
} }
// border-box everything // border-box everything
// http://paulirish.com/2012/box-sizing-border-box-ftw/ // http://paulirish.com/2012/box-sizing-border-box-ftw/
@if $plyr-border-box { @if $plyr-border-box {
.plyr--full-ui { .plyr--full-ui {
box-sizing: border-box; box-sizing: border-box;
*, *,
*::after, *::after,
*::before { *::before {
box-sizing: inherit; box-sizing: inherit;
}
} }
}
} }
// Fix 300ms delay // Fix 300ms delay
@if $plyr-touch-action { @if $plyr-touch-action {
.plyr--full-ui { .plyr--full-ui {
a, a,
button, button,
input, input,
label { label {
touch-action: manipulation; touch-action: manipulation;
}
} }
}
} }
+6 -6
View File
@@ -3,10 +3,10 @@
// -------------------------------------------------------------- // --------------------------------------------------------------
.plyr__badge { .plyr__badge {
background: var(--plyr-badge-background, $plyr-badge-background); background: $plyr-badge-background;
border-radius: 2px; border-radius: $plyr-badge-border-radius;
color: var(--plyr-badge-color, $plyr-badge-color); color: $plyr-badge-color;
font-size: var(--plyr-font-size-badge, $plyr-font-size-badge); font-size: $plyr-font-size-badge;
line-height: 1; line-height: 1;
padding: 3px 4px; padding: 3px 4px;
} }
+38 -40
View File
@@ -4,57 +4,55 @@
// Hide default captions // Hide default captions
.plyr--full-ui ::-webkit-media-text-track-container { .plyr--full-ui ::-webkit-media-text-track-container {
display: none; display: none;
} }
.plyr__captions { .plyr__captions {
animation: plyr-fade-in 0.3s ease; animation: plyr-fade-in 0.3s ease;
bottom: 0; bottom: 0;
display: none;
font-size: $plyr-font-size-captions-small;
left: 0;
padding: $plyr-control-spacing;
position: absolute;
text-align: center;
transition: transform 0.4s ease-in-out;
width: 100%;
.plyr__caption {
background: $plyr-captions-background;
border-radius: 2px;
box-decoration-break: clone;
color: $plyr-captions-text-color;
line-height: 185%;
padding: 0.2em 0.5em;
white-space: pre-wrap;
// Firefox adds a <div> when using getCueAsHTML()
div {
display: inline;
}
}
span:empty {
display: none; display: none;
font-size: $plyr-font-size-captions-small; }
left: 0;
padding: $plyr-control-spacing;
position: absolute;
text-align: center;
transition: transform 0.4s ease-in-out;
width: 100%;
.plyr__caption { @media (min-width: $plyr-bp-sm) {
background: $plyr-captions-background; font-size: $plyr-font-size-captions-base;
background: var(--plyr-captions-background, $plyr-captions-background); padding: calc(#{$plyr-control-spacing} * 2);
border-radius: 2px; }
box-decoration-break: clone;
color: $plyr-captions-text-color;
color: var(--plyr-captions-text-color, $plyr-captions-text-color);
line-height: 185%;
padding: 0.2em 0.5em;
white-space: pre-wrap;
// Firefox adds a <div> when using getCueAsHTML() @media (min-width: $plyr-bp-md) {
div { font-size: $plyr-font-size-captions-medium;
display: inline; }
}
}
span:empty {
display: none;
}
@media (min-width: $plyr-bp-sm) {
font-size: $plyr-font-size-captions-base;
padding: ($plyr-control-spacing * 2);
}
@media (min-width: $plyr-bp-md) {
font-size: $plyr-font-size-captions-medium;
}
} }
.plyr--captions-active .plyr__captions { .plyr--captions-active .plyr__captions {
display: block; display: block;
} }
// If the lower controls are shown and not empty // If the lower controls are shown and not empty
.plyr:not(.plyr--hide-controls) .plyr__controls:not(:empty) ~ .plyr__captions { .plyr:not(.plyr--hide-controls) .plyr__controls:not(:empty) ~ .plyr__captions {
transform: translateY(-($plyr-control-spacing * 4)); transform: translateY(calc(#{$plyr-control-spacing} * -4));
} }
+31 -32
View File
@@ -3,45 +3,44 @@
// -------------------------------------------------------------- // --------------------------------------------------------------
.plyr__control { .plyr__control {
background: transparent; background: transparent;
border: 0; border: 0;
border-radius: $plyr-control-radius; border-radius: $plyr-control-radius;
border-radius: var(--plyr-control-radius, $plyr-control-radius); color: inherit;
color: inherit; cursor: pointer;
cursor: pointer; flex-shrink: 0;
flex-shrink: 0; overflow: visible; // IE11
overflow: visible; // IE11 padding: $plyr-control-padding;
padding: var(--plyr-control-padding, $plyr-control-padding); position: relative;
position: relative; transition: all 0.3s ease;
transition: all 0.3s ease;
svg { svg {
display: block; display: block;
fill: currentColor; fill: currentColor;
height: var(--plyr-control-icon-size, $plyr-control-icon-size); height: $plyr-control-icon-size;
pointer-events: none; pointer-events: none;
width: var(--plyr-control-icon-size, $plyr-control-icon-size); width: $plyr-control-icon-size;
} }
// Default focus // Default focus
&:focus { &:focus {
outline: 0; outline: 0;
} }
// Tab focus // Tab focus
&.plyr__tab-focus { &.plyr__tab-focus {
@include plyr-tab-focus(); @include plyr-tab-focus();
} }
} }
// Remove any link styling // Remove any link styling
a.plyr__control { a.plyr__control {
text-decoration: none; text-decoration: none;
&::after, &::after,
&::before { &::before {
display: none; display: none;
} }
} }
// Change icons on state change // Change icons on state change
@@ -49,5 +48,5 @@ a.plyr__control {
.plyr__control.plyr__control--pressed .icon--not-pressed, .plyr__control.plyr__control--pressed .icon--not-pressed,
.plyr__control:not(.plyr__control--pressed) .label--pressed, .plyr__control:not(.plyr__control--pressed) .label--pressed,
.plyr__control.plyr__control--pressed .label--not-pressed { .plyr__control.plyr__control--pressed .label--not-pressed {
display: none; display: none;
} }
+35 -35
View File
@@ -4,49 +4,49 @@
// Hide native controls // Hide native controls
.plyr--full-ui ::-webkit-media-controls { .plyr--full-ui ::-webkit-media-controls {
display: none; display: none;
} }
// Playback controls // Playback controls
.plyr__controls { .plyr__controls {
align-items: center; align-items: center;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
text-align: center; text-align: center;
.plyr__progress__container { .plyr__progress__container {
flex: 1; flex: 1;
min-width: 0; // Fix for Edge issue where content would overflow min-width: 0; // Fix for Edge issue where content would overflow
}
// Spacing
.plyr__controls__item {
margin-left: calc(#{$plyr-control-spacing} / 4);
&:first-child {
margin-left: 0;
margin-right: auto;
} }
// Spacing &.plyr__progress__container {
.plyr__controls__item { padding-left: calc(#{$plyr-control-spacing} / 4);
margin-left: ($plyr-control-spacing / 4);
&:first-child {
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;
}
} }
// Hide empty controls &.plyr__time {
&:empty { padding: 0 calc(#{$plyr-control-spacing} / 2);
display: none;
} }
&.plyr__progress__container:first-child,
&.plyr__time:first-child,
&.plyr__time + .plyr__time {
padding-left: 0;
}
}
// Hide empty controls
&:empty {
display: none;
}
} }
// Some options are hidden by default // Some options are hidden by default
@@ -54,11 +54,11 @@
.plyr [data-plyr='pip'], .plyr [data-plyr='pip'],
.plyr [data-plyr='airplay'], .plyr [data-plyr='airplay'],
.plyr [data-plyr='fullscreen'] { .plyr [data-plyr='fullscreen'] {
display: none; display: none;
} }
.plyr--captions-enabled [data-plyr='captions'], .plyr--captions-enabled [data-plyr='captions'],
.plyr--pip-supported [data-plyr='pip'], .plyr--pip-supported [data-plyr='pip'],
.plyr--airplay-supported [data-plyr='airplay'], .plyr--airplay-supported [data-plyr='airplay'],
.plyr--fullscreen-enabled [data-plyr='fullscreen'] { .plyr--fullscreen-enabled [data-plyr='fullscreen'] {
display: inline-block; display: inline-block;
} }
+181 -179
View File
@@ -3,198 +3,200 @@
// -------------------------------------------------------------- // --------------------------------------------------------------
.plyr__menu { .plyr__menu {
display: flex; // Edge fix display: flex; // Edge fix
position: relative; position: relative;
// Animate the icon // Animate the icon
.plyr__control svg { .plyr__control svg {
transition: transform 0.3s ease; transition: transform 0.3s ease;
} }
.plyr__control[aria-expanded='true'] { .plyr__control[aria-expanded='true'] {
svg { svg {
transform: rotate(90deg); transform: rotate(90deg);
}
// Hide tooltip
.plyr__tooltip {
display: none;
}
} }
// The actual menu container // Hide tooltip
&__container { .plyr__tooltip {
animation: plyr-popup 0.2s ease; display: none;
background: $plyr-menu-background; }
border-radius: 4px; }
bottom: 100%;
box-shadow: $plyr-menu-shadow; // The actual menu container
color: $plyr-menu-color; &__container {
font-size: $plyr-font-size-base; animation: plyr-popup 0.2s ease;
margin-bottom: 10px; background: $plyr-menu-background;
border-radius: 4px;
bottom: 100%;
box-shadow: $plyr-menu-shadow;
color: $plyr-menu-color;
font-size: $plyr-font-size-base;
margin-bottom: 10px;
position: absolute;
right: -3px;
text-align: left;
white-space: nowrap;
z-index: 3;
> div {
overflow: hidden;
transition: height 0.35s cubic-bezier(0.4, 0, 0.2, 1), width 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
// Arrow
&::after {
border: 4px solid transparent;
border-top-color: $plyr-menu-background;
content: '';
height: 0;
position: absolute;
right: 15px;
top: 100%;
width: 0;
}
[role='menu'] {
padding: $plyr-control-padding;
}
[role='menuitem'],
[role='menuitemradio'] {
margin-top: 2px;
&:first-child {
margin-top: 0;
}
}
// Options
.plyr__control {
align-items: center;
color: $plyr-menu-color;
display: flex;
font-size: $plyr-font-size-menu;
padding-bottom: calc(#{$plyr-control-padding} / 1.5);
padding-left: calc(#{$plyr-control-padding} * 1.5);
padding-right: calc(#{$plyr-control-padding} * 1.5);
padding-top: calc(#{$plyr-control-padding} / 1.5);
user-select: none;
width: 100%;
> span {
align-items: inherit;
display: flex;
width: 100%;
}
&::after {
border: 4px solid transparent;
content: '';
position: absolute; position: absolute;
right: -3px; top: 50%;
text-align: left; transform: translateY(-50%);
white-space: nowrap; }
z-index: 3;
> div { &--forward {
overflow: hidden; padding-right: calc(#{$plyr-control-padding} * 4);
transition: height 0.35s cubic-bezier(0.4, 0, 0.2, 1), width 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
// Arrow
&::after { &::after {
border: 4px solid transparent; border-left-color: $plyr-menu-arrow-color;
border-top-color: $plyr-menu-background; right: 5px;
content: '';
height: 0;
position: absolute;
right: 15px;
top: 100%;
width: 0;
} }
[role='menu'] { &.plyr__tab-focus::after,
padding: $plyr-control-padding; &:hover::after {
border-left-color: currentColor;
}
}
&--back {
font-weight: $plyr-font-weight-regular;
margin: $plyr-control-padding;
margin-bottom: calc(#{$plyr-control-padding} / 2);
padding-left: calc(#{$plyr-control-padding} * 4);
position: relative;
width: calc(100% - (#{$plyr-control-padding} * 2));
&::after {
border-right-color: $plyr-menu-arrow-color;
left: $plyr-control-padding;
} }
[role='menuitem'], &::before {
[role='menuitemradio'] { background: $plyr-menu-border-color;
margin-top: 2px; box-shadow: 0 1px 0 $plyr-menu-border-shadow-color;
content: '';
&:first-child { height: 1px;
margin-top: 0; left: 0;
} margin-top: calc(#{$plyr-control-padding} / 2);
overflow: hidden;
position: absolute;
right: 0;
top: 100%;
} }
// Options &.plyr__tab-focus::after,
.plyr__control { &:hover::after {
align-items: center; border-right-color: currentColor;
color: $plyr-menu-color;
display: flex;
font-size: $plyr-font-size-menu;
padding: ceil($plyr-control-padding / 2) ceil($plyr-control-padding * 1.5);
user-select: none;
width: 100%;
> span {
align-items: inherit;
display: flex;
width: 100%;
}
&::after {
border: 4px solid transparent;
content: '';
position: absolute;
top: 50%;
transform: translateY(-50%);
}
&--forward {
padding-right: ceil($plyr-control-padding * 4);
&::after {
border-left-color: rgba($plyr-menu-color, 0.8);
right: 5px;
}
&.plyr__tab-focus::after,
&:hover::after {
border-left-color: currentColor;
}
}
&--back {
$horizontal-padding: ($plyr-control-padding * 2);
font-weight: $plyr-font-weight-regular;
margin: $plyr-control-padding;
margin-bottom: floor($plyr-control-padding / 2);
padding-left: ceil($plyr-control-padding * 4);
position: relative;
width: calc(100% - #{$horizontal-padding});
&::after {
border-right-color: rgba($plyr-menu-color, 0.8);
left: $plyr-control-padding;
}
&::before {
background: $plyr-menu-border-color;
box-shadow: 0 1px 0 $plyr-menu-border-shadow-color;
content: '';
height: 1px;
left: 0;
margin-top: ceil($plyr-control-padding / 2);
overflow: hidden;
position: absolute;
right: 0;
top: 100%;
}
&.plyr__tab-focus::after,
&:hover::after {
border-right-color: currentColor;
}
}
}
.plyr__control[role='menuitemradio'] {
padding-left: $plyr-control-padding;
&::before,
&::after {
border-radius: 100%;
}
&::before {
background: rgba(#000, 0.1);
content: '';
display: block;
flex-shrink: 0;
height: 16px;
margin-right: $plyr-control-spacing;
transition: all 0.3s ease;
width: 16px;
}
&::after {
background: #fff;
border: 0;
height: 6px;
left: 12px;
opacity: 0;
top: 50%;
transform: translateY(-50%) scale(0);
transition: transform 0.3s ease, opacity 0.3s ease;
width: 6px;
}
&[aria-checked='true'] {
&::before {
background: var(--plyr-color-main, $plyr-color-main);
}
&::after {
opacity: 1;
transform: translateY(-50%) scale(1);
}
}
&.plyr__tab-focus::before,
&:hover::before {
background: rgba(#000, 0.1);
}
}
// Option value
.plyr__menu__value {
align-items: center;
display: flex;
margin-left: auto;
margin-right: -($plyr-control-padding - 2);
overflow: hidden;
padding-left: ceil($plyr-control-padding * 3.5);
pointer-events: none;
} }
}
} }
.plyr__control[role='menuitemradio'] {
padding-left: $plyr-control-padding;
&::before,
&::after {
border-radius: 100%;
}
&::before {
background: rgba(#000, 0.1);
content: '';
display: block;
flex-shrink: 0;
height: 16px;
margin-right: $plyr-control-spacing;
transition: all 0.3s ease;
width: 16px;
}
&::after {
background: #fff;
border: 0;
height: 6px;
left: 12px;
opacity: 0;
top: 50%;
transform: translateY(-50%) scale(0);
transition: transform 0.3s ease, opacity 0.3s ease;
width: 6px;
}
&[aria-checked='true'] {
&::before {
background: $plyr-control-toggle-checked-background;
}
&::after {
opacity: 1;
transform: translateY(-50%) scale(1);
}
}
&.plyr__tab-focus::before,
&:hover::before {
background: rgba($plyr-color-gray-900, 0.1);
}
}
// Option value
.plyr__menu__value {
align-items: center;
display: flex;
margin-left: auto;
margin-right: calc((#{$plyr-control-padding} - 2) * -1);
overflow: hidden;
padding-left: calc(#{$plyr-control-padding} * 3.5);
pointer-events: none;
}
}
} }
+13 -13
View File
@@ -3,20 +3,20 @@
// -------------------------------------------------------------- // --------------------------------------------------------------
.plyr__poster { .plyr__poster {
background-color: #000; background-color: #000;
background-position: 50% 50%; background-position: 50% 50%;
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: contain; background-size: contain;
height: 100%; height: 100%;
left: 0; left: 0;
opacity: 0; opacity: 0;
position: absolute; position: absolute;
top: 0; top: 0;
transition: opacity 0.2s ease; transition: opacity 0.2s ease;
width: 100%; width: 100%;
z-index: 1; z-index: 1;
} }
.plyr--stopped.plyr__poster-enabled .plyr__poster { .plyr--stopped.plyr__poster-enabled .plyr__poster {
opacity: 1; opacity: 1;
} }
+66 -66
View File
@@ -6,89 +6,89 @@
$plyr-progress-offset: $plyr-range-thumb-height; $plyr-progress-offset: $plyr-range-thumb-height;
.plyr__progress { .plyr__progress {
left: $plyr-progress-offset / 2; left: calc(#{$plyr-progress-offset} * 0.5);
margin-right: $plyr-progress-offset; margin-right: $plyr-progress-offset;
position: relative;
input[type='range'],
&__buffer {
margin-left: calc(#{$plyr-progress-offset} * -0.5);
margin-right: calc(#{$plyr-progress-offset} * -0.5);
width: calc(100% + #{$plyr-progress-offset});
}
input[type='range'] {
position: relative; position: relative;
z-index: 2;
}
input[type='range'], // Seek tooltip to show time
&__buffer { .plyr__tooltip {
margin-left: -($plyr-progress-offset / 2); font-size: $plyr-font-size-time;
margin-right: -($plyr-progress-offset / 2); left: 0;
width: calc(100% + #{$plyr-progress-offset}); }
}
input[type='range'] {
position: relative;
z-index: 2;
}
// Seek tooltip to show time
.plyr__tooltip {
font-size: $plyr-font-size-time;
left: 0;
}
} }
.plyr__progress__buffer { .plyr__progress__buffer {
-webkit-appearance: none; /* stylelint-disable-line */ -webkit-appearance: none; /* stylelint-disable-line */
background: transparent;
border: 0;
border-radius: 100px;
height: $plyr-range-track-height;
left: 0;
margin-top: calc((#{$plyr-range-track-height} / 2) * -1);
padding: 0;
position: absolute;
top: 50%;
&::-webkit-progress-bar {
background: transparent; background: transparent;
border: 0; }
&::-webkit-progress-value {
background: currentColor;
border-radius: 100px; border-radius: 100px;
height: $plyr-range-track-height; min-width: $plyr-range-track-height;
left: 0; transition: width 0.2s ease;
margin-top: -($plyr-range-track-height / 2); }
padding: 0;
position: absolute;
top: 50%;
&::-webkit-progress-bar { // Mozilla
background: transparent; &::-moz-progress-bar {
} background: currentColor;
border-radius: 100px;
min-width: $plyr-range-track-height;
transition: width 0.2s ease;
}
&::-webkit-progress-value { // Microsoft
background: currentColor; &::-ms-fill {
border-radius: 100px; border-radius: 100px;
min-width: $plyr-range-track-height; transition: width 0.2s ease;
transition: width 0.2s ease; }
}
// Mozilla
&::-moz-progress-bar {
background: currentColor;
border-radius: 100px;
min-width: $plyr-range-track-height;
transition: width 0.2s ease;
}
// Microsoft
&::-ms-fill {
border-radius: 100px;
transition: width 0.2s ease;
}
} }
// Loading state // Loading state
.plyr--loading .plyr__progress__buffer { .plyr--loading .plyr__progress__buffer {
animation: plyr-progress 1s linear infinite; animation: plyr-progress 1s linear infinite;
background-image: linear-gradient( background-image: linear-gradient(
-45deg, -45deg,
var(--plyr-progress-loading-background, $plyr-progress-loading-background) 25%, $plyr-progress-loading-background 25%,
transparent 25%, transparent 25%,
transparent 50%, transparent 50%,
var(--plyr-progress-loading-background, $plyr-progress-loading-background) 50%, $plyr-progress-loading-background 50%,
var(--plyr-progress-loading-background, $plyr-progress-loading-background) 75%, $plyr-progress-loading-background 75%,
transparent 75%, transparent 75%,
transparent transparent
); );
background-repeat: repeat-x; background-repeat: repeat-x;
background-size: $plyr-progress-loading-size $plyr-progress-loading-size; background-size: $plyr-progress-loading-size $plyr-progress-loading-size;
color: transparent; color: transparent;
} }
.plyr--video.plyr--loading .plyr__progress__buffer { .plyr--video.plyr--loading .plyr__progress__buffer {
background-color: $plyr-video-progress-buffered-background; background-color: $plyr-video-progress-buffered-background;
} }
.plyr--audio.plyr--loading .plyr__progress__buffer { .plyr--audio.plyr--loading .plyr__progress__buffer {
background-color: $plyr-audio-progress-buffered-background; background-color: $plyr-audio-progress-buffered-background;
} }
+77 -77
View File
@@ -3,92 +3,92 @@
// -------------------------------------------------------------- // --------------------------------------------------------------
.plyr--full-ui input[type='range'] { .plyr--full-ui input[type='range'] {
// WebKit // WebKit
-webkit-appearance: none; /* stylelint-disable-line */
background: transparent;
border: 0;
border-radius: calc(#{$plyr-range-thumb-height} * 2);
// color is used in JS to populate lower fill for WebKit
color: $plyr-range-fill-background;
display: block;
height: $plyr-range-max-height;
margin: 0;
padding: 0;
transition: box-shadow 0.3s ease;
width: 100%;
&::-webkit-slider-runnable-track {
@include plyr-range-track();
background-image: linear-gradient(to right, currentColor var(--value, 0%), transparent var(--value, 0%));
}
&::-webkit-slider-thumb {
@include plyr-range-thumb();
-webkit-appearance: none; /* stylelint-disable-line */ -webkit-appearance: none; /* stylelint-disable-line */
background: transparent; margin-top: calc(((#{$plyr-range-thumb-height} - #{$plyr-range-track-height}) / 2) * -1);
}
// Mozilla
&::-moz-range-track {
@include plyr-range-track();
}
&::-moz-range-thumb {
@include plyr-range-thumb();
}
&::-moz-range-progress {
background: currentColor;
border-radius: calc(#{$plyr-range-track-height} / 2);
height: $plyr-range-track-height;
}
// Microsoft
&::-ms-track {
@include plyr-range-track();
color: transparent;
}
&::-ms-fill-upper {
@include plyr-range-track();
}
&::-ms-fill-lower {
@include plyr-range-track();
background: currentColor;
}
&::-ms-thumb {
@include plyr-range-thumb();
// For some reason, Edge uses the -webkit margin above
margin-top: 0;
}
&::-ms-tooltip {
display: none;
}
// Focus styles
&:focus {
outline: 0;
}
&::-moz-focus-outer {
border: 0; border: 0;
border-radius: ($plyr-range-thumb-height * 2); }
// color is used in JS to populate lower fill for WebKit
color: $plyr-range-fill-background;
display: block;
height: $plyr-range-max-height;
margin: 0;
padding: 0;
transition: box-shadow 0.3s ease;
width: 100%;
&.plyr__tab-focus {
&::-webkit-slider-runnable-track { &::-webkit-slider-runnable-track {
@include plyr-range-track(); @include plyr-tab-focus();
background-image: linear-gradient(to right, currentColor var(--value, 0%), transparent var(--value, 0%));
} }
&::-webkit-slider-thumb {
@include plyr-range-thumb();
-webkit-appearance: none; /* stylelint-disable-line */
margin-top: -(($plyr-range-thumb-height - $plyr-range-track-height) / 2);
}
// Mozilla
&::-moz-range-track { &::-moz-range-track {
@include plyr-range-track(); @include plyr-tab-focus();
} }
&::-moz-range-thumb {
@include plyr-range-thumb();
}
&::-moz-range-progress {
background: currentColor;
border-radius: ($plyr-range-track-height / 2);
height: $plyr-range-track-height;
}
// Microsoft
&::-ms-track { &::-ms-track {
@include plyr-range-track(); @include plyr-tab-focus();
color: transparent;
}
&::-ms-fill-upper {
@include plyr-range-track();
}
&::-ms-fill-lower {
@include plyr-range-track();
background: currentColor;
}
&::-ms-thumb {
@include plyr-range-thumb();
// For some reason, Edge uses the -webkit margin above
margin-top: 0;
}
&::-ms-tooltip {
display: none;
}
// Focus styles
&:focus {
outline: 0;
}
&::-moz-focus-outer {
border: 0;
}
&.plyr__tab-focus {
&::-webkit-slider-runnable-track {
@include plyr-tab-focus();
}
&::-moz-range-track {
@include plyr-tab-focus();
}
&::-ms-track {
@include plyr-tab-focus();
}
} }
}
} }
+9 -9
View File
@@ -3,18 +3,18 @@
// -------------------------------------------------------------- // --------------------------------------------------------------
.plyr__time { .plyr__time {
font-size: $plyr-font-size-time; font-size: $plyr-font-size-time;
} }
// Media duration hidden on small screens // Media duration hidden on small screens
.plyr__time + .plyr__time { .plyr__time + .plyr__time {
// Add a slash in before // Add a slash in before
&::before { &::before {
content: '\2044'; content: '\2044';
margin-right: $plyr-control-spacing; margin-right: $plyr-control-spacing;
} }
@media (max-width: $plyr-bp-sm-max) { @media (max-width: calc(#{$plyr-bp-md} - 1)) {
display: none; display: none;
} }
} }
+56 -56
View File
@@ -3,86 +3,86 @@
// -------------------------------------------------------------- // --------------------------------------------------------------
.plyr__tooltip { .plyr__tooltip {
background: $plyr-tooltip-background; background: $plyr-tooltip-background;
border-radius: $plyr-tooltip-radius; border-radius: $plyr-tooltip-radius;
bottom: 100%; bottom: 100%;
box-shadow: $plyr-tooltip-shadow; box-shadow: $plyr-tooltip-shadow;
color: $plyr-tooltip-color; color: $plyr-tooltip-color;
font-size: $plyr-font-size-small; font-size: $plyr-font-size-small;
font-weight: $plyr-font-weight-regular; font-weight: $plyr-font-weight-regular;
left: 50%; left: 50%;
line-height: 1.3; line-height: 1.3;
margin-bottom: ($plyr-tooltip-padding * 2); margin-bottom: calc(#{$plyr-tooltip-padding} * 2);
opacity: 0; opacity: 0;
padding: $plyr-tooltip-padding ($plyr-tooltip-padding * 1.5); padding: $plyr-tooltip-padding calc(#{$plyr-tooltip-padding} * 1.5);
pointer-events: none; pointer-events: none;
position: absolute; position: absolute;
transform: translate(-50%, 10px) scale(0.8); transform: translate(-50%, 10px) scale(0.8);
transform-origin: 50% 100%; transform-origin: 50% 100%;
transition: transform 0.2s 0.1s ease, opacity 0.2s 0.1s ease; transition: transform 0.2s 0.1s ease, opacity 0.2s 0.1s ease;
white-space: nowrap; white-space: nowrap;
z-index: 2; z-index: 2;
// The background triangle // The background triangle
&::before { &::before {
border-left: $plyr-tooltip-arrow-size solid transparent; border-left: $plyr-tooltip-arrow-size solid transparent;
border-right: $plyr-tooltip-arrow-size solid transparent; border-right: $plyr-tooltip-arrow-size solid transparent;
border-top: $plyr-tooltip-arrow-size solid $plyr-tooltip-background; border-top: $plyr-tooltip-arrow-size solid $plyr-tooltip-background;
bottom: -$plyr-tooltip-arrow-size; bottom: calc(#{$plyr-tooltip-arrow-size} * -1);
content: ''; content: '';
height: 0; height: 0;
left: 50%; left: 50%;
position: absolute; position: absolute;
transform: translateX(-50%); transform: translateX(-50%);
width: 0; width: 0;
z-index: 2; z-index: 2;
} }
} }
// Displaying // Displaying
.plyr .plyr__control:hover .plyr__tooltip, .plyr .plyr__control:hover .plyr__tooltip,
.plyr .plyr__control.plyr__tab-focus .plyr__tooltip, .plyr .plyr__control.plyr__tab-focus .plyr__tooltip,
.plyr__tooltip--visible { .plyr__tooltip--visible {
opacity: 1; opacity: 1;
transform: translate(-50%, 0) scale(1); transform: translate(-50%, 0) scale(1);
} }
.plyr .plyr__control:hover .plyr__tooltip { .plyr .plyr__control:hover .plyr__tooltip {
z-index: 3; z-index: 3;
} }
// First tooltip // First tooltip
.plyr__controls > .plyr__control:first-child .plyr__tooltip, .plyr__controls > .plyr__control:first-child .plyr__tooltip,
.plyr__controls > .plyr__control:first-child + .plyr__control .plyr__tooltip { .plyr__controls > .plyr__control:first-child + .plyr__control .plyr__tooltip {
left: 0; left: 0;
transform: translate(0, 10px) scale(0.8); transform: translate(0, 10px) scale(0.8);
transform-origin: 0 100%; transform-origin: 0 100%;
&::before { &::before {
left: ($plyr-control-icon-size / 2) + $plyr-control-padding; left: calc((#{$plyr-control-icon-size} / 2) + #{$plyr-control-padding});
} }
} }
// Last tooltip // Last tooltip
.plyr__controls > .plyr__control:last-child .plyr__tooltip { .plyr__controls > .plyr__control:last-child .plyr__tooltip {
left: auto; left: auto;
right: 0; right: 0;
transform: translate(0, 10px) scale(0.8); transform: translate(0, 10px) scale(0.8);
transform-origin: 100% 100%; transform-origin: 100% 100%;
&::before { &::before {
left: auto; left: auto;
right: ($plyr-control-icon-size / 2) + $plyr-control-padding; right: calc((#{$plyr-control-icon-size} / 2) + #{$plyr-control-padding});
transform: translateX(50%); transform: translateX(50%);
} }
} }
.plyr__controls > .plyr__control:first-child, .plyr__controls > .plyr__control:first-child,
.plyr__controls > .plyr__control:first-child + .plyr__control, .plyr__controls > .plyr__control:first-child + .plyr__control,
.plyr__controls > .plyr__control:last-child { .plyr__controls > .plyr__control:last-child {
&:hover .plyr__tooltip, &:hover .plyr__tooltip,
&.plyr__tab-focus .plyr__tooltip, &.plyr__tab-focus .plyr__tooltip,
.plyr__tooltip--visible { .plyr__tooltip--visible {
transform: translate(0, 0) scale(1); transform: translate(0, 0) scale(1);
} }
} }
+14 -14
View File
@@ -3,23 +3,23 @@
// -------------------------------------------------------------- // --------------------------------------------------------------
.plyr__volume { .plyr__volume {
align-items: center; align-items: center;
display: flex; display: flex;
max-width: 110px; max-width: 110px;
min-width: 80px; min-width: 80px;
position: relative; position: relative;
width: 20%; width: 20%;
input[type='range'] { input[type='range'] {
margin-left: ($plyr-control-spacing / 2); margin-left: calc(#{$plyr-control-spacing} / 2);
margin-right: ($plyr-control-spacing / 2); margin-right: calc(#{$plyr-control-spacing} / 2);
position: relative; position: relative;
z-index: 2; z-index: 2;
} }
} }
// Auto size on iOS as there's no slider // Auto size on iOS as there's no slider
.plyr--is-ios .plyr__volume { .plyr--is-ios .plyr__volume {
min-width: 0; min-width: 0;
width: auto; width: auto;
} }
+17 -17
View File
@@ -3,29 +3,29 @@
// -------------------------------------------------------------- // --------------------------------------------------------------
@keyframes plyr-progress { @keyframes plyr-progress {
to { to {
background-position: $plyr-progress-loading-size 0; background-position: $plyr-progress-loading-size 0;
} }
} }
@keyframes plyr-popup { @keyframes plyr-popup {
0% { 0% {
opacity: 0.5; opacity: 0.5;
transform: translateY(10px); transform: translateY(10px);
} }
to { to {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
} }
} }
@keyframes plyr-fade-in { @keyframes plyr-fade-in {
from { from {
opacity: 0; opacity: 0;
} }
to { to {
opacity: 1; opacity: 1;
} }
} }
+56 -56
View File
@@ -11,12 +11,12 @@ $css-vars-use-native: false !default;
// Assigns a variable to the global map // Assigns a variable to the global map
/// ///
@function css-var-assign($varName: null, $varValue: null) { @function css-var-assign($varName: null, $varValue: null) {
@return map-merge( @return map-merge(
$css-vars, $css-vars,
( (
$varName: $varValue, $varName: $varValue,
) )
); );
} }
/// ///
@@ -30,33 +30,33 @@ $css-vars-use-native: false !default;
// background: var(--main-background, green); // background: var(--main-background, green);
/// ///
@function var($args...) { @function var($args...) {
// CHECK PARAMS // CHECK PARAMS
@if (length($args) ==0) { @if (length($args) ==0) {
@error 'Variable name is expected to be passed to the var() function'; @error 'Variable name is expected to be passed to the var() function';
} }
@if (str-length(nth($args, 1)) < 2 or str-slice(nth($args, 1), 0, 2) != '--') { @if (str-length(nth($args, 1)) < 2 or str-slice(nth($args, 1), 0, 2) != '--') {
@error "Variable name is expected to start from '--'"; @error "Variable name is expected to start from '--'";
}
// PROCESS
$var-name: nth($args, 1);
$var-value: map-get($css-vars, $var-name);
@if ($css-vars-use-native) {
// CSS variables
// Native CSS: don't process function in case of native
@return unquote('var(' + $args + ')');
} @else {
@if ($var-value == null) {
// variable is not provided so far
@if (length($args) == 2) {
$var-value: nth($args, 2);
}
} }
// PROCESS // Sass: return value from the map
$var-name: nth($args, 1); @return $var-value;
$var-value: map-get($css-vars, $var-name); }
@if ($css-vars-use-native) {
// CSS variables
// Native CSS: don't process function in case of native
@return unquote('var(' + $args + ')');
} @else {
@if ($var-value == null) {
// variable is not provided so far
@if (length($args) == 2) {
$var-value: nth($args, 2);
}
}
// Sass: return value from the map
@return $var-value;
}
} }
/// ///
@@ -69,32 +69,32 @@ $css-vars-use-native: false !default;
// )); // ));
/// ///
@mixin css-vars($var-map: null) { @mixin css-vars($var-map: null) {
// CHECK PARAMS // CHECK PARAMS
@if ($var-map == null) { @if ($var-map == null) {
@error 'Map of variables is expected, instead got: null'; @error 'Map of variables is expected, instead got: null';
} }
@if (type_of($var-map) != map) { @if (type_of($var-map) != map) {
@error 'Map of variables is expected, instead got another type passed: #{type_of($var, ap)}'; @error 'Map of variables is expected, instead got another type passed: #{type_of($var, ap)}';
} }
// PROCESS // PROCESS
@if ($css-vars-use-native) { @if ($css-vars-use-native) {
// CSS variables // CSS variables
// Native CSS: assign CSS custom properties to the global scope // Native CSS: assign CSS custom properties to the global scope
@at-root :root { @at-root :root {
@each $var-name, $var-value in $var-map { @each $var-name, $var-value in $var-map {
@if (type_of($var-value) == string) { @if (type_of($var-value) == string) {
#{$var-name}: $var-value; // to prevent quotes interpolation #{$var-name}: $var-value; // to prevent quotes interpolation
} @else { } @else {
#{$var-name}: #{$var-value}; #{$var-name}: #{$var-value};
}
}
}
} @else {
// Sass or debug
// merge variables and values to the global map (provides no output)
@each $var-name, $var-value in $var-map {
$css-vars: css-var-assign($varName, $varValue) !global; // store in global variable
} }
}
} }
} @else {
// Sass or debug
// merge variables and values to the global map (provides no output)
@each $var-name, $var-value in $var-map {
$css-vars: css-var-assign($varName, $varValue) !global; // store in global variable
}
}
} }

Some files were not shown because too many files have changed in this diff Show More