Merge pull request #1770 from sampotts/css-variables

Allow customization via CSS Custom Properties
This commit is contained in:
Sam Potts
2020-04-24 00:22:39 +10:00
committed by GitHub
130 changed files with 18590 additions and 15457 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
+7465 -4563
View File
File diff suppressed because it is too large Load Diff
+17 -2
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
+243 -253
View File
@@ -1,278 +1,268 @@
<!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>
<div id="container">
<video
controls
crossorigin
playsinline
data-poster="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg"
id="player"
>
<!-- Video files -->
<source
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4"
type="video/mp4"
size="576"
/>
<source
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4"
type="video/mp4"
size="720"
/>
<source
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4"
type="video/mp4"
size="1080"
/>
<main> <!-- Caption files -->
<div id="container"> <track
<video kind="captions"
controls label="English"
crossorigin srclang="en"
playsinline src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt"
poster="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg" default
id="player" />
> <track
<!-- Video files --> kind="captions"
<source label="Français"
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" srclang="fr"
type="video/mp4" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt"
size="576" />
/>
<source
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4"
type="video/mp4"
size="720"
/>
<source
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4"
type="video/mp4"
size="1080"
/>
<!-- Caption files --> <!-- Fallback for browsers that don't support the <video> element -->
<track <a href="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" download>Download</a>
kind="captions" </video>
label="English"
srclang="en"
src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt"
default
/>
<track
kind="captions"
label="Français"
srclang="fr"
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 -->
<a href="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" download
>Download</a
>
</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>
+1 -1
View File
@@ -5,9 +5,9 @@
"homepage": "https://plyr.io", "homepage": "https://plyr.io",
"author": "Sam Potts <sam@potts.es>", "author": "Sam Potts <sam@potts.es>",
"dependencies": { "dependencies": {
"@sentry/browser": "^5.15.4",
"core-js": "^3.6.4", "core-js": "^3.6.4",
"custom-event-polyfill": "^1.0.7", "custom-event-polyfill": "^1.0.7",
"raven-js": "^3.27.2",
"shr-buttons": "2.0.3", "shr-buttons": "2.0.3",
"url-polyfill": "^1.1.8" "url-polyfill": "^1.1.8"
} }
+125 -136
View File
@@ -8,7 +8,7 @@ import './tab-focus';
import 'custom-event-polyfill'; import 'custom-event-polyfill';
import 'url-polyfill'; import 'url-polyfill';
import Raven from 'raven-js'; import * as Sentry from '@sentry/browser';
import Shr from 'shr-buttons'; import Shr from 'shr-buttons';
import Plyr from '../../../src/js/plyr'; import Plyr from '../../../src/js/plyr';
@@ -16,145 +16,134 @@ import sources from './sources';
import toggleClass from './toggle-class'; import toggleClass from './toggle-class';
(() => { (() => {
const { host } = window.location; const production = 'plyr.io';
const env = {
prod: host === 'plyr.io',
dev: host === 'dev.plyr.io',
};
document.addEventListener('DOMContentLoaded', () => { // Sentry for demo site (https://plyr.io) only
Raven.context(() => { if (window.location.host === production) {
const selector = '#player'; Sentry.init({
dsn: 'https://d4ad9866ad834437a4754e23937071e4@sentry.io/305555',
whitelistUrls: [production].map(d => new RegExp(`https://(([a-z0-9])+(.))*${d}`)),
});
}
// Setup share buttons document.addEventListener('DOMContentLoaded', () => {
Shr.setup('.js-shr', { const selector = '#player';
count: {
className: 'button__count',
},
wrapper: {
className: 'button--with-count',
},
});
// Setup the player // Setup share buttons
const player = new Plyr(selector, { Shr.setup('.js-shr', {
debug: true, count: {
title: 'View From A Blue Moon', className: 'button__count',
iconUrl: 'dist/demo.svg', },
keyboard: { wrapper: {
global: true, className: 'button--with-count',
}, },
tooltips: {
controls: true,
},
captions: {
active: true,
},
ads: {
enabled: env.prod || env.dev,
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 hasCurrentType = !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;
}
// 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 (hasCurrentType) {
currentType = 'video';
}
// Replace current history state
if (historySupport && types.includes(currentType)) {
window.history.replaceState({ type: currentType }, '', hasCurrentType ? '' : `#${currentType}`);
}
// If it's not video, load the source
if (currentType !== 'video') {
setSource(currentType, true);
}
render(currentType);
});
}); });
// Raven / Sentry // Setup the player
// For demo site (https://plyr.io) only const player = new Plyr(selector, {
if (env.prod) { debug: true,
Raven.config('https://d4ad9866ad834437a4754e23937071e4@sentry.io/305555').install(); 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;
} }
// 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);
});
})(); })();
+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;
+3
View File
@@ -3,6 +3,9 @@
// ========================================================================== // ==========================================================================
@charset 'UTF-8'; @charset 'UTF-8';
@import '../../../../src/sass/lib/css-vars';
$css-vars-use-native: true;
// Settings // Settings
@import '../settings/breakpoints'; @import '../settings/breakpoints';
@import '../settings/colors'; @import '../settings/colors';
+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-5; 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;
} }
+12 -12
View File
@@ -3,22 +3,22 @@
// ========================================================================== // ==========================================================================
// Grayscale // Grayscale
$color-gray-9: hsl(210, 15%, 16%); $color-gray-900: hsl(210, 15%, 16%);
$color-gray-8: lighten($color-gray-9, 9%); $color-gray-800: lighten($color-gray-900, 9%);
$color-gray-7: lighten($color-gray-8, 9%); $color-gray-700: lighten($color-gray-800, 9%);
$color-gray-6: lighten($color-gray-7, 9%); $color-gray-600: lighten($color-gray-700, 9%);
$color-gray-5: lighten($color-gray-6, 9%); $color-gray-500: lighten($color-gray-600, 9%);
$color-gray-4: lighten($color-gray-5, 9%); $color-gray-400: lighten($color-gray-500, 9%);
$color-gray-3: lighten($color-gray-4, 9%); $color-gray-300: lighten($color-gray-400, 9%);
$color-gray-2: lighten($color-gray-3, 9%); $color-gray-200: lighten($color-gray-300, 9%);
$color-gray-1: lighten($color-gray-2, 9%); $color-gray-100: lighten($color-gray-200, 9%);
$color-gray-0: lighten($color-gray-1, 9%); $color-gray-50: lighten($color-gray-100, 9%);
// Branding // Branding
$color-brand-primary: hsl(198, 100%, 50%); $color-brand-primary: hsl(198, 100%, 50%);
// Text // Text
$color-text: $color-gray-7; $color-text: $color-gray-700;
$color-headings: $color-brand-primary; $color-headings: $color-brand-primary;
// Brands // Brands
@@ -36,7 +36,7 @@ $color-button-background: $color-brand-primary;
$color-button-text: #fff; $color-button-text: #fff;
$color-button-background-hover: hsl(198, 100%, 55%); $color-button-background-hover: hsl(198, 100%, 55%);
$color-button-count-background: #fff; $color-button-count-background: #fff;
$color-button-count-text: $color-gray-6; $color-button-count-text: $color-gray-600;
// Focus // Focus
$tab-focus-default-color: #fff; $tab-focus-default-color: #fff;
+14 -21
View File
@@ -2,24 +2,17 @@
// Plyr Settings // Plyr Settings
// ========================================================================== // ==========================================================================
// Font @include css-vars(
$plyr-font-family: inherit; (
--plyr-color-main: $color-brand-primary,
// 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; --plyr-font-size-menu: var(--plyr-font-size-base),
--plyr-font-weight-regular: 500,
// Other --plyr-font-weight-bold: 600,
$plyr-font-smoothing: true; --plyr-font-size-captions-medium: 18px,
--plyr-font-size-captions-large: 21px,
// Colors )
$plyr-color-main: $color-brand-primary; );
// Captions
$plyr-font-size-captions-base: $plyr-font-size-base;
$plyr-font-size-captions-small: $plyr-font-size-small;
$plyr-font-size-captions-medium: 18px;
$plyr-font-size-captions-large: 21px;
$plyr-font-size-menu: $plyr-font-size-base;
+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;
} }
+65 -13
View File
@@ -2,27 +2,79 @@
# yarn lockfile v1 # yarn lockfile v1
core-js@^3.1.4: "@sentry/browser@^5.15.4":
version "3.1.4" version "5.15.4"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.1.4.tgz#3a2837fc48e582e1ae25907afcd6cf03b0cc7a07" resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.15.4.tgz#5a7e7bad088556665ed8e69bceb0e18784e4f6c7"
integrity sha512-YNZN8lt82XIMLnLirj9MhKDFZHalwzzrL9YLt6eb0T5D0EDl4IQ90IGkua8mHbnxNrkj1d8hbdizMc0Qmg1WnQ== integrity sha512-l/auT1HtZM3KxjCGQHYO/K51ygnlcuOrM+7Ga8gUUbU9ZXDYw6jRi0+Af9aqXKmdDw1naNxr7OCSy6NBrLWVZw==
dependencies:
"@sentry/core" "5.15.4"
"@sentry/types" "5.15.4"
"@sentry/utils" "5.15.4"
tslib "^1.9.3"
"@sentry/core@5.15.4":
version "5.15.4"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.15.4.tgz#08b617e093a636168be5aebad141d1f744217085"
integrity sha512-9KP4NM4SqfV5NixpvAymC7Nvp36Zj4dU2fowmxiq7OIbzTxGXDhwuN/t0Uh8xiqlkpkQqSECZ1OjSFXrBldetQ==
dependencies:
"@sentry/hub" "5.15.4"
"@sentry/minimal" "5.15.4"
"@sentry/types" "5.15.4"
"@sentry/utils" "5.15.4"
tslib "^1.9.3"
"@sentry/hub@5.15.4":
version "5.15.4"
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.15.4.tgz#cb64473725a60eec63b0be58ed1143eaaf894bee"
integrity sha512-1XJ1SVqadkbUT4zLS0TVIVl99si7oHizLmghR8LMFl5wOkGEgehHSoOydQkIAX2C7sJmaF5TZ47ORBHgkqclUg==
dependencies:
"@sentry/types" "5.15.4"
"@sentry/utils" "5.15.4"
tslib "^1.9.3"
"@sentry/minimal@5.15.4":
version "5.15.4"
resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.15.4.tgz#113f01fefb86b7830994c3dfa7ad4889ba7b2003"
integrity sha512-GL4GZ3drS9ge+wmxkHBAMEwulaE7DMvAEfKQPDAjg2p3MfcCMhAYfuY4jJByAC9rg9OwBGGehz7UmhWMFjE0tw==
dependencies:
"@sentry/hub" "5.15.4"
"@sentry/types" "5.15.4"
tslib "^1.9.3"
"@sentry/types@5.15.4":
version "5.15.4"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.15.4.tgz#37f30e35b06e8e12ad1101f1beec3e9b88ca1aab"
integrity sha512-quPHPpeAuwID48HLPmqBiyXE3xEiZLZ5D3CEbU3c3YuvvAg8qmfOOTI6z4Z3Eedi7flvYpnx3n7N3dXIEz30Eg==
"@sentry/utils@5.15.4":
version "5.15.4"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.15.4.tgz#02865ab3c9b745656cea0ab183767ec26c96f6e6"
integrity sha512-lO8SLBjrUDGADl0LOkd55R5oL510d/1SaI08/IBHZCxCUwI4TiYo5EPECq8mrj3XGfgCyq9osw33bymRlIDuSQ==
dependencies:
"@sentry/types" "5.15.4"
tslib "^1.9.3"
core-js@^3.6.4:
version "3.6.4"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.4.tgz#440a83536b458114b9cb2ac1580ba377dc470647"
integrity sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw==
custom-event-polyfill@^1.0.7: custom-event-polyfill@^1.0.7:
version "1.0.7" version "1.0.7"
resolved "https://registry.yarnpkg.com/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz#9bc993ddda937c1a30ccd335614c6c58c4f87aee" resolved "https://registry.yarnpkg.com/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz#9bc993ddda937c1a30ccd335614c6c58c4f87aee"
integrity sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w== integrity sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==
raven-js@^3.27.2:
version "3.27.2"
resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.27.2.tgz#6c33df952026cd73820aa999122b7b7737a66775"
integrity sha512-mFWQcXnhRFEQe5HeFroPaEghlnqy7F5E2J3Fsab189ondqUzcjwSVi7el7F36cr6PvQYXoZ1P2F5CSF2/azeMQ==
shr-buttons@2.0.3: shr-buttons@2.0.3:
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/shr-buttons/-/shr-buttons-2.0.3.tgz#2ffd021fc3d789e1510ce2736b938bd09ea1da5a" resolved "https://registry.yarnpkg.com/shr-buttons/-/shr-buttons-2.0.3.tgz#2ffd021fc3d789e1510ce2736b938bd09ea1da5a"
integrity sha512-sPAgHiw4uaIt9TnxTfyZEedDChcldSVtnBHE44cpe/mSC7rqm4IEKZRLYqnVlTcGM+FSDNBPUNpSf50Q2ntd+w== integrity sha512-sPAgHiw4uaIt9TnxTfyZEedDChcldSVtnBHE44cpe/mSC7rqm4IEKZRLYqnVlTcGM+FSDNBPUNpSf50Q2ntd+w==
url-polyfill@^1.1.5: tslib@^1.9.3:
version "1.1.5" version "1.11.1"
resolved "https://registry.yarnpkg.com/url-polyfill/-/url-polyfill-1.1.5.tgz#bec79b72b5407dba6d8cced2e32e4ab273aa9fb1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35"
integrity sha512-9XjIJ6nwrU+nGd8t90Ze0Zs7t8A+SU0gqsqPttj6j3zAVe5q0HFcuv37nDBdVSPpi4aTHTfbUF/i+ZVD+o2EbA== integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==
url-polyfill@^1.1.8:
version "1.1.8"
resolved "https://registry.yarnpkg.com/url-polyfill/-/url-polyfill-1.1.8.tgz#21eb58ad61192f52b77dcac8ab5293ae7bc67060"
integrity sha512-Ey61F4FEqhcu1vHSOMmjl0Vd/RPRLEjMj402qszD/dhMBrVfoUsnIj8KSZo2yj+eIlxJGKFdnm6ES+7UzMgZ3Q==
+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",
"prettier": "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",
"aws-sdk": "^2.648.0", "autoprefixer": "^9.7.6",
"@babel/core": "^7.9.0", "aws-sdk": "^2.658.0",
"@babel/preset-env": "^7.9.0", "@babel/core": "^7.9.0",
"babel-eslint": "^10.1.0", "@babel/preset-env": "^7.9.5",
"browser-sync": "^2.26.7", "babel-eslint": "^10.1.0",
"del": "^5.1.0", "browser-sync": "^2.26.7",
"eslint": "^6.8.0", "del": "^5.1.0",
"eslint-config-airbnb-base": "^14.1.0", "eslint": "^6.8.0",
"eslint-config-prettier": "^6.10.1", "eslint-config-airbnb-base": "^14.1.0",
"eslint-plugin-import": "^2.20.1", "eslint-config-prettier": "^6.10.1",
"eslint-plugin-simple-import-sort": "^5.0.2", "eslint-plugin-import": "^2.20.2",
"fancy-log": "^1.3.3", "eslint-plugin-simple-import-sort": "^5.0.2",
"fastly-purge": "^1.0.1", "fancy-log": "^1.3.3",
"git-branch": "^2.0.1", "fastly-purge": "^1.0.1",
"gulp": "^4.0.2", "git-branch": "^2.0.1",
"gulp-autoprefixer": "^7.0.1", "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-clean-css": "^4.3.0", "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-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.6.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.3.2",
"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.17.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.5",
"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"
} }
} }
+240 -143
View File
@@ -8,26 +8,26 @@ Plyr is a simple, lightweight, accessible and customizable HTML5, YouTube and Vi
# Features # Features
- 📼 **HTML Video & Audio, YouTube & Vimeo** - support for the major formats - 📼 **HTML Video & Audio, YouTube & Vimeo** - support for the major formats
- 💪 **Accessible** - full support for VTT captions and screen readers - 💪 **Accessible** - full support for VTT captions and screen readers
- 🔧 **[Customizable](#html)** - make the player look how you want with the markup you want - 🔧 **[Customizable](#html)** - make the player look how you want with the markup you want
- 😎 **Clean HTML** - uses the _right_ elements. `<input type="range">` for volume and `<progress>` for progress and well, `<button>`s for buttons. There's no - 😎 **Clean HTML** - uses the _right_ elements. `<input type="range">` for volume and `<progress>` for progress and well, `<button>`s for buttons. There's no
`<span>` or `<a href="#">` button hacks `<span>` or `<a href="#">` button hacks
- 📱 **Responsive** - works with any screen size - 📱 **Responsive** - works with any screen size
- 💵 **[Monetization](#ads)** - make money from your videos - 💵 **[Monetization](#ads)** - make money from your videos
- 📹 **[Streaming](#demos)** - support for hls.js, Shaka and dash.js streaming playback - 📹 **[Streaming](#demos)** - support for hls.js, Shaka and dash.js streaming playback
- 🎛 **[API](#api)** - toggle playback, volume, seeking, and more through a standardized API - 🎛 **[API](#api)** - toggle playback, volume, seeking, and more through a standardized API
- 🎤 **[Events](#events)** - no messing around with Vimeo and YouTube APIs, all events are standardized across formats - 🎤 **[Events](#events)** - no messing around with Vimeo and YouTube APIs, all events are standardized across formats
- 🔎 **[Fullscreen](#fullscreen)** - supports native fullscreen with fallback to "full window" modes - 🔎 **[Fullscreen](#fullscreen)** - supports native fullscreen with fallback to "full window" modes
- ⌨️ **[Shortcuts](#shortcuts)** - supports keyboard shortcuts - ⌨️ **[Shortcuts](#shortcuts)** - supports keyboard shortcuts
- 🖥 **Picture-in-Picture** - supports picture-in-picture mode - 🖥 **Picture-in-Picture** - supports picture-in-picture mode
- 📱 **Playsinline** - supports the `playsinline` attribute - 📱 **Playsinline** - supports the `playsinline` attribute
- 🏎 **Speed controls** - adjust speed on the fly - 🏎 **Speed controls** - adjust speed on the fly
- 📖 **Multiple captions** - support for multiple caption tracks - 📖 **Multiple captions** - support for multiple caption tracks
- 🌎 **i18n support** - support for internationalization of controls - 🌎 **i18n support** - support for internationalization of controls
- 👌 **[Preview thumbnails](#preview-thumbnails)** - support for displaying preview thumbnails - 👌 **[Preview thumbnails](#preview-thumbnails)** - support for displaying preview thumbnails
- 🤟 **No frameworks** - written in "vanilla" ES6 JavaScript, no jQuery required - 🤟 **No frameworks** - written in "vanilla" ES6 JavaScript, no jQuery required
- 💁‍♀️ **SASS** - to include in your build processes - 💁‍♀️ **SASS** - to include in your build processes
### Demos ### Demos
@@ -42,21 +42,23 @@ Plyr extends upon the standard [HTML5 media element](https://developer.mozilla.o
### HTML5 Video ### HTML5 Video
```html ```html
<video poster="/path/to/poster.jpg" id="player" playsinline controls> <video id="player" playsinline controls data-poster="/path/to/poster.jpg">
<source src="/path/to/video.mp4" type="video/mp4" /> <source src="/path/to/video.mp4" type="video/mp4" />
<source src="/path/to/video.webm" type="video/webm" /> <source src="/path/to/video.webm" type="video/webm" />
<!-- Captions are optional --> <!-- Captions are optional -->
<track kind="captions" label="English captions" src="/path/to/captions.vtt" srclang="en" default /> <track kind="captions" label="English captions" src="/path/to/captions.vtt" srclang="en" default />
</video> </video>
``` ```
**Note**: The poster image should be specified using `data-poster`. This is to prevent it [being downloaded twice](https://github.com/sampotts/plyr/issues/1531). If you're sure the image will be cached, you can still use the `poster` attribute for true progressive enhancement.
### HTML5 Audio ### HTML5 Audio
```html ```html
<audio id="player" controls> <audio id="player" controls>
<source src="/path/to/audio.mp3" type="audio/mp3" /> <source src="/path/to/audio.mp3" type="audio/mp3" />
<source src="/path/to/audio.ogg" type="audio/ogg" /> <source src="/path/to/audio.ogg" type="audio/ogg" />
</audio> </audio>
``` ```
@@ -68,12 +70,12 @@ We recommend [progressive enhancement](https://www.smashingmagazine.com/2009/04/
```html ```html
<div class="plyr__video-embed" id="player"> <div class="plyr__video-embed" id="player">
<iframe <iframe
src="https://www.youtube.com/embed/bTqVqk7FSmY?origin=https://plyr.io&amp;iv_load_policy=3&amp;modestbranding=1&amp;playsinline=1&amp;showinfo=0&amp;rel=0&amp;enablejsapi=1" src="https://www.youtube.com/embed/bTqVqk7FSmY?origin=https://plyr.io&amp;iv_load_policy=3&amp;modestbranding=1&amp;playsinline=1&amp;showinfo=0&amp;rel=0&amp;enablejsapi=1"
allowfullscreen allowfullscreen
allowtransparency allowtransparency
allow="autoplay" allow="autoplay"
></iframe> ></iframe>
</div> </div>
``` ```
@@ -93,12 +95,12 @@ Much the same as YouTube above.
```html ```html
<div class="plyr__video-embed" id="player"> <div class="plyr__video-embed" id="player">
<iframe <iframe
src="https://player.vimeo.com/video/76979871?loop=false&amp;byline=false&amp;portrait=false&amp;title=false&amp;speed=true&amp;transparent=0&amp;gesture=media" src="https://player.vimeo.com/video/76979871?loop=false&amp;byline=false&amp;portrait=false&amp;title=false&amp;speed=true&amp;transparent=0&amp;gesture=media"
allowfullscreen allowfullscreen
allowtransparency allowtransparency
allow="autoplay" allow="autoplay"
></iframe> ></iframe>
</div> </div>
``` ```
@@ -123,7 +125,7 @@ Alternatively you can include the `plyr.js` script before the closing `</body>`
```html ```html
<script src="path/to/plyr.js"></script> <script src="path/to/plyr.js"></script>
<script> <script>
const player = new Plyr('#player'); const player = new Plyr('#player');
</script> </script>
``` ```
@@ -164,9 +166,9 @@ reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.5.10
Plyr has partnered up with [vi.ai](https://vi.ai/publisher-video-monetization/?aid=plyrio) to offer monetization options for your videos. Getting setup is easy: Plyr has partnered up with [vi.ai](https://vi.ai/publisher-video-monetization/?aid=plyrio) to offer monetization options for your videos. Getting setup is easy:
- [Sign up for a vi.ai account](https://vi.ai/publisher-video-monetization/?aid=plyrio) - [Sign up for a vi.ai account](https://vi.ai/publisher-video-monetization/?aid=plyrio)
- Grab your publisher ID from the code snippet - Grab your publisher ID from the code snippet
- Enable ads in the [config options](#options) and enter your publisher ID - Enable ads in the [config options](#options) and enter your publisher ID
Any questions regarding the ads can be sent straight to vi.ai and any issues with rendering raised through GitHub issues. Any questions regarding the ads can be sent straight to vi.ai and any issues with rendering raised through GitHub issues.
@@ -174,7 +176,102 @@ If you do not wish to use Vi, you can set your own `ads.tagUrl` [option](#option
# Advanced # Advanced
## SASS ## Customizing the CSS
If you want to change any design tokens used for the rendering of the player, you can do so using [CSS Custom Properties](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties).
Here's a list of the properties and what they are used for:
| Name | Description | Default / Fallback |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `--plyr-color-main` | The primary UI color. | ![#f03c15](https://placehold.it/15/00b3ff/000000?text=+) `#00b3ff` |
| `--plyr-tab-focus-color` | The color used for the dotted outline when an element is `:focus-visible` (equivalent) keyboard focus. | `--plyr-color-main` |
| `--plyr-badge-background` | The background color for badges in the menu. | ![#4a5464](https://placehold.it/15/4a5464/000000?text=+) `#4a5464` |
| `--plyr-badge-text-color` | The text color for badges. | ![#ffffff](https://placehold.it/15/ffffff/000000?text=+) `#ffffff` |
| `--plyr-badge-border-radius` | The border radius used for badges. | `2px` |
| `--plyr-tab-focus-color` | The color used to highlight tab (keyboard) focus. | `--plyr-color-main` |
| `--plyr-captions-background` | The color for the background of captions. | `rgba(0, 0, 0, 0.8)` |
| `--plyr-captions-text-color` | The color used for the captions text. | ![#ffffff](https://placehold.it/15/ffffff/000000?text=+) `#ffffff` |
| `--plyr-control-icon-size` | The size of the icons used in the controls. | `18px` |
| `--plyr-control-spacing` | The space between controls (sometimes used in a multiple - e.g. `10px / 2 = 5px`). | `10px` |
| `--plyr-control-padding` | The padding inside controls. | `--plyr-control-spacing * 0.7` (`7px`) |
| `--plyr-control-radius` | The border radius used on controls. | `3px` |
| `--plyr-control-toggle-checked-background` | The background color used for checked menu items. | `--plyr-color-main` |
| `--plyr-video-controls-background` | The background for the video controls. | `linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.75))` |
| `--plyr-video-control-color` | The text/icon color for video controls. | ![#ffffff](https://placehold.it/15/ffffff/000000?text=+) `#ffffff` |
| `--plyr-video-control-color-hover` | The text/icon color used when video controls are `:hover`, `:focus` and `:focus-visible` (equivalent). | ![#ffffff](https://placehold.it/15/ffffff/000000?text=+) `#ffffff` |
| `--plyr-video-control-background-hover` | The background color used when video controls are `:hover`, `:focus` and `:focus-visible` (equivalent). | `--plyr-color-main` |
| `--plyr-audio-controls-background` | The background for the audio controls. | ![#ffffff](https://placehold.it/15/ffffff/000000?text=+) `#ffffff` |
| `--plyr-audio-control-color` | The text/icon color for audio controls. | ![#4a5464](https://placehold.it/15/4a5464/000000?text=+) `#4a5464` |
| `--plyr-audio-control-color-hover` | The text/icon color used when audio controls are `:hover`, `:focus` and `:focus-visible` (equivalent). | ![#ffffff](https://placehold.it/15/ffffff/000000?text=+) `#ffffff` |
| `--plyr-audio-control-background-hover` | The background color used when video controls are `:hover`, `:focus` and `:focus-visible` (equivalent). | `--plyr-color-main` |
| `--plyr-menu-background` | The background color for menus. | `rgba(255, 255, 255, 0.9)` |
| `--plyr-menu-color` | The text/icon color for menu items. | ![#4a5464](https://placehold.it/15/4a5464/000000?text=+) `#4a5464` |
| `--plyr-menu-shadow` | The shadow used on menus. | `0 1px 2px rgba(0, 0, 0, 0.15)` |
| `--plyr-menu-radius` | The border radius on the menu. | `4px` |
| `--plyr-menu-arrow-size` | The size of the arrow on the bottom of the menu. | `6px` |
| `--plyr-menu-item-arrow-color` | The color of the arrows in the menu. | ![#728197](https://placehold.it/15/728197/000000?text=+) `#728197` |
| `--plyr-menu-item-arrow-size` | The size of the arrows in the menu. | `4px` |
| `--plyr-menu-border-color` | The border color for the bottom of the back button in the top of the sub menu pages. | ![#dcdfe5](https://placehold.it/15/dcdfe5/000000?text=+) `#dcdfe5` |
| `--plyr-menu-border-shadow-color` | The shadow below the border of the back button in the top of the sub menu pages. | ![#ffffff](https://placehold.it/15/ffffff/000000?text=+) `#ffffff` |
| `--plyr-progress-loading-size` | The size of the stripes in the loading state in the scrubber. | `25px` |
| `--plyr-progress-loading-background` | The background color on the loading state in the scrubber. | `rgba(35, 40, 47, 0.6)` |
| `--plyr-video-progress-buffered-background` | The fill color for the buffer indication in the scrubber for video. | `rgba(255, 255, 255, 0.25)` |
| `--plyr-audio-progress-buffered-background` | The fill color for the buffer indication in the scrubber for audio. | `rgba(193, 200, 209, 0.6)` |
| `--plyr-range-thumb-height` | The height of the scrubber handle/thumb. | `13px` |
| `--plyr-range-thumb-background` | The background of the scrubber handle/thumb. | ![#ffffff](https://placehold.it/15/ffffff/000000?text=+) `#ffffff` |
| `--plyr-range-thumb-shadow` | The shadow of the scrubber handle/thumb. | `0 1px 1px rgba(215, 26, 18, 0.15), 0 0 0 1px rgba(215, 26, 18, 0.2)` |
| `--plyr-range-thumb-active-shadow-width` | The width of the shadow when the scrubber handle/thumb is `:active` (pressed). | `3px` |
| `--plyr-range-track-height` | The height of the scrubber/progress track. | `5px` |
| `--plyr-range-fill-background` | The fill color of the scrubber/progress. | `--plyr-color-main` |
| `--plyr-video-range-track-background` | The background of the scrubber/progress. | `--plyr-video-progress-buffered-background` |
| `--plyr-video-range-thumb-active-shadow-color` | The color of the shadow when the video scrubber handle/thumb is `:active` (pressed). | `rgba(255, 255, 255, 0.5)` |
| `--plyr-audio-range-track-background` | The background of the scrubber/progress. | `--plyr-video-progress-buffered-background` |
| `--plyr-audio-range-thumb-active-shadow-color` | The color of the shadow when the audio scrubber handle/thumb is `:active` (pressed). | `rgba(215, 26, 18, 0.1)` |
| `--plyr-tooltip-background` | The background color for tooltips. | `rgba(255, 255, 255, 0.9)` |
| `--plyr-tooltip-color` | The text color for tooltips. | ![#4a5464](https://placehold.it/15/4a5464/000000?text=+) `#4a5464` |
| `--plyr-tooltip-padding` | The padding for tooltips. | `calc(var(--plyr-control-spacing) / 2))` |
| `--plyr-tooltip-arrow-size` | The size of the arrow under tooltips. | `4px` |
| `--plyr-tooltip-radius` | The border radius on tooltips. | `3px` |
| `--plyr-tooltip-shadow` | The shadow on tooltips. | `0 1px 2px rgba(0, 0, 0, 0.15)` |
| `--plyr-font-family` | The font family used in the player. | |
| `--plyr-font-size-base` | The base font size. Mainly used for captions. | `15px` |
| `--plyr-font-size-small` | The smaller font size. Mainly used for captions. | `13px` |
| `--plyr-font-size-large` | The larger font size. Mainly used for captions. | `18px` |
| `--plyr-font-size-xlarge` | The even larger font size. Mainly used for captions. | `21px` |
| `--plyr-font-size-time` | The font size for the time. | `--plyr-font-size-small` |
| `--plyr-font-size-menu` | The font size used in the menu. | `--plyr-font-size-small` |
| `--plyr-font-size-badge` | The font size used for badges. | `9px` |
| `--plyr-font-weight-regular` | The regular font weight. | `400` |
| `--plyr-font-weight-bold` | The bold font weight. | `600` |
| `--plyr-line-height` | The line height used within the player. | `1.7` |
| `--plyr-font-smoothing` | Whether to enable font antialiasing within the player. | `false` |
You can set them in your CSS for all players:
```css
:root {
--plyr-color-main: #1ac266;
}
```
...or for a specific class name:
```css
.player {
--plyr-color-main: #1ac266;
}
```
...or in your HTML:
```html
<video class="player" style="--plyr-color-main: #1ac266;">
...
</vieo>
```
### SASS
You can use `plyr.scss` file included in `/src/sass` as part of your build and change variables to suit your design. The SASS requires you to You can use `plyr.scss` file included in `/src/sass` as part of your build and change variables to suit your design. The SASS requires you to
use [autoprefixer](https://www.npmjs.com/package/gulp-autoprefixer) (you should be already!) as all declarations use the W3C definitions. use [autoprefixer](https://www.npmjs.com/package/gulp-autoprefixer) (you should be already!) as all declarations use the W3C definitions.
@@ -215,9 +312,9 @@ WebVTT captions are supported. To add a caption track, check the HTML example ab
You can specify a range of arguments for the constructor to use: You can specify a range of arguments for the constructor to use:
- A [CSS string selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors) - A [CSS string selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors)
- A [`HTMLElement`](https://developer.mozilla.org/en/docs/Web/API/HTMLElement) - A [`HTMLElement`](https://developer.mozilla.org/en/docs/Web/API/HTMLElement)
- A [jQuery](https://jquery.com) object - A [jQuery](https://jquery.com) object
_Note_: If a `NodeList`, `Array`, or jQuery object are passed, the first element will be used for setup. To setup multiple players, see [multiple players](#multiple-players) below. _Note_: If a `NodeList`, `Array`, or jQuery object are passed, the first element will be used for setup. To setup multiple players, see [multiple players](#multiple-players) below.
@@ -263,7 +360,7 @@ The second argument for the constructor is the [options](#options) object:
```javascript ```javascript
const player = new Plyr('#player', { const player = new Plyr('#player', {
title: 'Example Title', title: 'Example Title',
}); });
``` ```
@@ -318,9 +415,9 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
1. Vimeo only 1. Vimeo only
2. Autoplay is generally not recommended as it is seen as a negative user experience. It is also disabled in many browsers. Before raising issues, do your homework. More info can be found here: 2. Autoplay is generally not recommended as it is seen as a negative user experience. It is also disabled in many browsers. Before raising issues, do your homework. More info can be found here:
- https://webkit.org/blog/6784/new-video-policies-for-ios/ - https://webkit.org/blog/6784/new-video-policies-for-ios/
- https://developers.google.com/web/updates/2017/09/autoplay-policy-changes - https://developers.google.com/web/updates/2017/09/autoplay-policy-changes
- https://hacks.mozilla.org/2019/02/firefox-66-to-block-automatically-playing-audible-video-and-audio/ - https://hacks.mozilla.org/2019/02/firefox-66-to-block-automatically-playing-audible-video-and-audio/
# API # API
@@ -332,7 +429,7 @@ The easiest way to access the Plyr object is to set the return value from your c
```javascript ```javascript
const player = new Plyr('#player', { const player = new Plyr('#player', {
/* options */ /* options */
}); });
``` ```
@@ -340,7 +437,7 @@ You can also access the object through any events:
```javascript ```javascript
element.addEventListener('ready', event => { element.addEventListener('ready', event => {
const player = event.detail.plyr; const player = event.detail.plyr;
}); });
``` ```
@@ -434,39 +531,39 @@ Video example:
```javascript ```javascript
player.source = { player.source = {
type: 'video', type: 'video',
title: 'Example title', title: 'Example title',
sources: [ sources: [
{ {
src: '/path/to/movie.mp4', src: '/path/to/movie.mp4',
type: 'video/mp4', type: 'video/mp4',
size: 720, size: 720,
},
{
src: '/path/to/movie.webm',
type: 'video/webm',
size: 1080,
},
],
poster: '/path/to/poster.jpg',
previewThumbnails: {
src: '/path/to/thumbnails.vtt',
}, },
tracks: [ {
{ src: '/path/to/movie.webm',
kind: 'captions', type: 'video/webm',
label: 'English', size: 1080,
srclang: 'en', },
src: '/path/to/captions.en.vtt', ],
default: true, poster: '/path/to/poster.jpg',
}, previewThumbnails: {
{ src: '/path/to/thumbnails.vtt',
kind: 'captions', },
label: 'French', tracks: [
srclang: 'fr', {
src: '/path/to/captions.fr.vtt', kind: 'captions',
}, label: 'English',
], srclang: 'en',
src: '/path/to/captions.en.vtt',
default: true,
},
{
kind: 'captions',
label: 'French',
srclang: 'fr',
src: '/path/to/captions.fr.vtt',
},
],
}; };
``` ```
@@ -474,18 +571,18 @@ Audio example:
```javascript ```javascript
player.source = { player.source = {
type: 'audio', type: 'audio',
title: 'Example title', title: 'Example title',
sources: [ sources: [
{ {
src: '/path/to/audio.mp3', src: '/path/to/audio.mp3',
type: 'audio/mp3', type: 'audio/mp3',
}, },
{ {
src: '/path/to/audio.ogg', src: '/path/to/audio.ogg',
type: 'audio/ogg', type: 'audio/ogg',
}, },
], ],
}; };
``` ```
@@ -493,13 +590,13 @@ YouTube example:
```javascript ```javascript
player.source = { player.source = {
type: 'video', type: 'video',
sources: [ sources: [
{ {
src: 'bTqVqk7FSmY', src: 'bTqVqk7FSmY',
provider: 'youtube', provider: 'youtube',
}, },
], ],
}; };
``` ```
@@ -509,13 +606,13 @@ Vimeo example
```javascript ```javascript
player.source = { player.source = {
type: 'video', type: 'video',
sources: [ sources: [
{ {
src: '143418951', src: '143418951',
provider: 'vimeo', provider: 'vimeo',
}, },
], ],
}; };
``` ```
@@ -540,7 +637,7 @@ property. Here's an example:
```javascript ```javascript
player.on('ready', event => { player.on('ready', event => {
const instance = event.detail.plyr; const instance = event.detail.plyr;
}); });
``` ```
@@ -599,8 +696,8 @@ YouTube and Vimeo are currently supported and function much like a HTML5 video.
to access the API's directly. You can do so via the `embed` property of your player object - e.g. `player.embed`. You can then use the relevant methods from the to access the API's directly. You can do so via the `embed` property of your player object - e.g. `player.embed`. You can then use the relevant methods from the
third party APIs. More info on the respective API's here: third party APIs. More info on the respective API's here:
- [YouTube iframe API Reference](https://developers.google.com/youtube/iframe_api_reference) - [YouTube iframe API Reference](https://developers.google.com/youtube/iframe_api_reference)
- [Vimeo player.js Reference](https://github.com/vimeo/player.js) - [Vimeo player.js Reference](https://github.com/vimeo/player.js)
_Note_: Not all API methods may work 100%. Your mileage may vary. It's better to use the Plyr API where possible. _Note_: Not all API methods may work 100%. Your mileage may vary. It's better to use the Plyr API where possible.
@@ -666,9 +763,9 @@ const supported = Plyr.supported('video', 'html5', true);
The arguments are: The arguments are:
- Media type (`audio` or `video`) - Media type (`audio` or `video`)
- Provider (`html5`, `youtube` or `vimeo`) - Provider (`html5`, `youtube` or `vimeo`)
- Whether the player has the `playsinline` attribute (only applicable to iOS 10+) - Whether the player has the `playsinline` attribute (only applicable to iOS 10+)
## Disable support programmatically ## Disable support programmatically
@@ -676,7 +773,7 @@ The `enabled` option can be used to disable certain User Agents. For example, if
```javascript ```javascript
{ {
enabled: /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent); enabled: /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
} }
``` ```
@@ -709,42 +806,42 @@ Plyr is developed by [@sam_potts](https://twitter.com/sam_potts) / [sampotts.me]
Plyr costs money to run, not only my time. I donate my time for free as I enjoy building Plyr but unfortunately have to pay for domains, hosting, and more. Any help with costs is appreciated... Plyr costs money to run, not only my time. I donate my time for free as I enjoy building Plyr but unfortunately have to pay for domains, hosting, and more. Any help with costs is appreciated...
- [Donate via Patreon](https://www.patreon.com/plyr) - [Donate via Patreon](https://www.patreon.com/plyr)
- [Donate via PayPal](https://www.paypal.me/pottsy/20usd) - [Donate via PayPal](https://www.paypal.me/pottsy/20usd)
# Mentions # Mentions
- [ProductHunt](https://www.producthunt.com/tech/plyr) - [ProductHunt](https://www.producthunt.com/tech/plyr)
- [The Changelog](http://thechangelog.com/plyr-simple-html5-media-player-custom-controls-webvtt-captions/) - [The Changelog](http://thechangelog.com/plyr-simple-html5-media-player-custom-controls-webvtt-captions/)
- [HTML5 Weekly #177](http://html5weekly.com/issues/177) - [HTML5 Weekly #177](http://html5weekly.com/issues/177)
- [Responsive Design #149](http://us4.campaign-archive2.com/?u=559bc631fe5294fc66f5f7f89&id=451a61490f) - [Responsive Design #149](http://us4.campaign-archive2.com/?u=559bc631fe5294fc66f5f7f89&id=451a61490f)
- [Web Design Weekly #174](https://web-design-weekly.com/2015/02/24/web-design-weekly-174/) - [Web Design Weekly #174](https://web-design-weekly.com/2015/02/24/web-design-weekly-174/)
- [Front End Focus #177](https://frontendfoc.us/issues/177) - [Front End Focus #177](https://frontendfoc.us/issues/177)
- [Hacker News](https://news.ycombinator.com/item?id=9136774) - [Hacker News](https://news.ycombinator.com/item?id=9136774)
- [Web Platform Daily](http://webplatformdaily.org/releases/2015-03-04) - [Web Platform Daily](http://webplatformdaily.org/releases/2015-03-04)
- [LayerVault Designer News](https://news.layervault.com/stories/45394-plyr--a-simple-html5-media-player) - [LayerVault Designer News](https://news.layervault.com/stories/45394-plyr--a-simple-html5-media-player)
- [The Treehouse Show #131](https://teamtreehouse.com/library/episode-131-origami-react-responsive-hero-images) - [The Treehouse Show #131](https://teamtreehouse.com/library/episode-131-origami-react-responsive-hero-images)
- [noupe.com](http://www.noupe.com/design/html5-plyr-is-a-responsive-and-accessible-video-player-94389.html) - [noupe.com](http://www.noupe.com/design/html5-plyr-is-a-responsive-and-accessible-video-player-94389.html)
# Used by # Used by
- [Selz.com](https://selz.com) - [Selz.com](https://selz.com)
- [Peugeot.fr](http://www.peugeot.fr/marque-et-technologie/technologies/peugeot-i-cockpit.html) - [Peugeot.fr](http://www.peugeot.fr/marque-et-technologie/technologies/peugeot-i-cockpit.html)
- [Peugeot.de](http://www.peugeot.de/modelle/modellberater/208-3-turer/fotos-videos.html) - [Peugeot.de](http://www.peugeot.de/modelle/modellberater/208-3-turer/fotos-videos.html)
- [TomTom.com](http://prioritydriving.tomtom.com/) - [TomTom.com](http://prioritydriving.tomtom.com/)
- [DIGBMX](http://digbmx.com/) - [DIGBMX](http://digbmx.com/)
- [Grime Archive](https://grimearchive.com/) - [Grime Archive](https://grimearchive.com/)
- [koel - A personal music streaming server that works.](http://koel.phanan.net/) - [koel - A personal music streaming server that works.](http://koel.phanan.net/)
- [Oscar Radio](http://oscar-radio.xyz/) - [Oscar Radio](http://oscar-radio.xyz/)
- [Sparkk TV](https://www.sparkktv.com/) - [Sparkk TV](https://www.sparkktv.com/)
- [@halfhalftravel](https://www.halfhalftravel.com/) - [@halfhalftravel](https://www.halfhalftravel.com/)
If you want to be added to the list, open a pull request. It'd be awesome to see how you're using Plyr 😎 If you want to be added to the list, open a pull request. It'd be awesome to see how you're using Plyr 😎
# Useful links and credits # Useful links and credits
- [PayPal's Accessible HTML5 Video Player (which Plyr was originally ported from)](https://github.com/paypal/accessible-html5-video-player) - [PayPal's Accessible HTML5 Video Player (which Plyr was originally ported from)](https://github.com/paypal/accessible-html5-video-player)
- [An awesome guide for Plyr in Japanese!](http://syncer.jp/how-to-use-plyr-io) by [@arayutw](https://twitter.com/arayutw) - [An awesome guide for Plyr in Japanese!](http://syncer.jp/how-to-use-plyr-io) by [@arayutw](https://twitter.com/arayutw)
# Thanks # Thanks
+326 -326
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,383 +23,383 @@ 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
// Note: mode='hidden' forces a track to download. To ensure every track
// isn't downloaded at once, only 'showing' tracks should be reassigned
// eslint-disable-next-line no-param-reassign
if (track.mode === 'showing') {
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
// Note: mode='hidden' forces a track to download. To ensure every track const activeClass = this.config.classNames.captions.active;
// isn't downloaded at once, only 'showing' tracks should be reassigned // Get the next state
// eslint-disable-next-line no-param-reassign // If the method is called without parameter, toggle based on current value
if (track.mode === 'showing') { const active = is.nullOrUndefined(input) ? !toggled : input;
track.mode = 'hidden';
}
// 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');
}
// Wait for the call stack to clear before setting mode='hidden'
// on the active track - forcing the browser to download it
setTimeout(() => {
if (active && this.captions.toggled) {
this.captions.currentTrackNode.mode = 'hidden';
}
});
},
// 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 // Wait for the call stack to clear before setting mode='hidden'
if (this.isVimeo) { // on the active track - forcing the browser to download it
this.embed.enableTextTrack(language); setTimeout(() => {
} if (active && this.captions.toggled) {
this.captions.currentTrackNode.mode = 'hidden';
}
});
},
// Trigger event // Set captions by track index
triggerEvent.call(this, this.media, 'languagechange'); // Used internally for the currentTrack setter with the passive option forced to false
} set(index, passive = true) {
const tracks = captions.getTracks.call(this);
// Show captions // Disable captions if setting to -1
captions.toggle.call(this, true, passive); if (index === -1) {
captions.toggle.call(this, false, passive);
return;
}
if (this.isHTML5 && this.isVideo) { if (!is.number(index)) {
// If we change the active track while a cue is already displayed we need to update it this.debug.warn('Invalid caption argument', index);
captions.updateCues.call(this); return;
} }
},
// Set captions by language if (!(index in tracks)) {
// Used internally for the language setter with the passive option forced to false this.debug.warn('Track not found', index);
setLanguage(input, passive = true) { return;
if (!is.string(input)) { }
this.debug.warn('Invalid language argument', input);
return; if (this.captions.currentTrack !== index) {
} this.captions.currentTrack = index;
// Normalize const track = tracks[index];
const language = input.toLowerCase(); const { language } = track || {};
// Store reference to node for invalidation on remove
this.captions.currentTrackNode = track;
// 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;
+425 -425
View File
@@ -3,440 +3,440 @@
// ========================================================================== // ==========================================================================
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)
// Selector for the fullscreen container so contextual / non-player content can remain visible in fullscreen mode
// Non-ancestors of the player element will be ignored
// container: null, // defaults to the player element
},
// 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)
// Selector for the fullscreen container so contextual / non-player content can remain visible in fullscreen mode
// Non-ancestors of the player element will be ignored
// container: null, // defaults to the player element
},
// 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,
// Whether the owner of the video has a Pro or Business account
// (which allows us to properly hide controls without CSS hacks, etc)
premium: false,
// Custom settings from Plyr
referrerPolicy: null, // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy
},
// YouTube plugin
youtube: {
noCookie: true, // 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
+259 -262
View File
@@ -11,288 +11,285 @@ 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';
// Get the fullscreen element // Get the fullscreen element
// Checks container is an ancestor, defaults to null // Checks container is an ancestor, defaults to null
this.player.elements.fullscreen = player.config.fullscreen.container this.player.elements.fullscreen =
&& closest(this.player.elements.container, player.config.fullscreen.container); player.config.fullscreen.container && closest(this.player.elements.container, player.config.fullscreen.container);
// 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.fullscreen || 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.fullscreen || 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;
+792 -790
View File
File diff suppressed because it is too large Load Diff
+38 -40
View File
@@ -8,54 +8,52 @@ 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 // Poster image container
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
+334 -325
View File
@@ -10,396 +10,405 @@ import { triggerEvent } from '../utils/events';
import fetch from '../utils/fetch'; import fetch from '../utils/fetch';
import is from '../utils/is'; import is from '../utils/is';
import loadScript from '../utils/load-script'; import loadScript from '../utils/load-script';
import { extend } from '../utils/objects';
import { format, stripHTML } from '../utils/strings'; import { format, stripHTML } from '../utils/strings';
import { setAspectRatio } from '../utils/style'; import { setAspectRatio } from '../utils/style';
import { buildUrlParams } from '../utils/urls'; 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, const { premium, referrerPolicy, ...frameParams } = config;
});
player.media.paused = true; // If the owner has a pro or premium account then we can hide controls etc
player.media.currentTime = 0; if (premium) {
Object.assign(frameParams, {
controls: false,
sidedock: false,
});
}
// Disable native text track rendering // Get Vimeo params for the iframe
if (player.supported.ui) { const params = buildUrlParams({
player.embed.disableTextTrack(); loop: player.config.loop.active,
} autoplay: player.autoplay,
muted: player.muted,
gesture: 'media',
playsinline: !this.config.fullscreen.iosNative,
...frameParams,
});
// Create a faux HTML5 API using the Vimeo API // Get the source URL or ID
player.media.play = () => { let source = player.media.getAttribute('src');
assurePlaybackState.call(player, true);
return player.embed.play();
};
player.media.pause = () => { // Get from <div> if needed
assurePlaybackState.call(player, false); if (is.empty(source)) {
return player.embed.pause(); source = player.media.getAttribute(player.config.attributes.embed.id);
}; }
player.media.stop = () => { const id = parseId(source);
player.pause(); // Build an iframe
player.currentTime = 0; const iframe = createElement('iframe');
}; const src = format(player.config.urls.vimeo.iframe, id, params);
iframe.setAttribute('src', src);
iframe.setAttribute('allowfullscreen', '');
iframe.setAttribute('allow', 'autoplay,fullscreen,picture-in-picture');
// Seeking // Set the referrer policy if required
let { currentTime } = player.media; if (!is.empty(referrerPolicy)) {
Object.defineProperty(player.media, 'currentTime', { iframe.setAttribute('referrerPolicy', referrerPolicy);
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 // Inject the package
const { embed, media, paused, volume } = player; const { poster } = player;
const restorePause = paused && !embed.hasPlayed; if (premium) {
iframe.setAttribute('data-poster', poster);
player.media = replaceElement(iframe, player.media);
} else {
const wrapper = createElement('div', { class: player.config.classNames.embedContainer, 'data-poster': poster });
wrapper.appendChild(iframe);
player.media = replaceElement(wrapper, player.media);
}
// Set seeking state and trigger event // Get poster image
media.seeking = true; fetch(format(player.config.urls.vimeo.api, id), 'json').then(response => {
triggerEvent.call(player, media, 'seeking'); if (is.empty(response)) {
return;
}
// If paused, mute until seek is complete // Get the URL for thumbnail
Promise.resolve(restorePause && embed.setVolume(0)) const url = new URL(response[0].thumbnail_large);
// Seek
.then(() => embed.setCurrentTime(time))
// Restore paused
.then(() => restorePause && embed.pause())
// Restore volume
.then(() => restorePause && embed.setVolume(volume))
.catch(() => {
// Do nothing
});
},
});
// Playback speed // Get original image
let speed = player.config.speed.selected; url.pathname = `${url.pathname.split('_')[0]}.jpg`;
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 // Set and show poster
let { volume } = player.config; ui.setPoster.call(player, url.href).catch(() => {});
Object.defineProperty(player.media, 'volume', { });
get() {
return volume;
},
set(input) {
player.embed.setVolume(input).then(() => {
volume = input;
triggerEvent.call(player, player.media, 'volumechange');
});
},
});
// Muted // Setup instance
let { muted } = player.config; // https://github.com/vimeo/player.js
Object.defineProperty(player.media, 'muted', { player.embed = new window.Vimeo.Player(iframe, {
get() { autopause: player.config.autopause,
return muted; muted: player.muted,
}, });
set(input) {
const toggle = is.boolean(input) ? input : false;
player.embed.setVolume(toggle ? 0 : player.config.volume).then(() => { player.media.paused = true;
muted = toggle; player.media.currentTime = 0;
triggerEvent.call(player, player.media, 'volumechange');
});
},
});
// Loop // Disable native text track rendering
let { loop } = player.config; if (player.supported.ui) {
Object.defineProperty(player.media, 'loop', { player.embed.disableTextTrack();
get() { }
return loop;
},
set(input) {
const toggle = is.boolean(input) ? input : player.config.loop.active;
player.embed.setLoop(toggle).then(() => { // Create a faux HTML5 API using the Vimeo API
loop = toggle; player.media.play = () => {
}); assurePlaybackState.call(player, true);
}, return player.embed.play();
}); };
// Source player.media.pause = () => {
let currentSrc; assurePlaybackState.call(player, false);
return player.embed.pause();
};
player.media.stop = () => {
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, 'data-poster': 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
+1058 -1055
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;
+220 -210
View File
@@ -13,267 +13,277 @@ 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('data-poster', poster);
// HTML5 uses native poster attribute // Wait until ui is ready
if (this.isHTML5) { return (
return Promise.resolve(poster); ready
} .call(this)
// Load image
.then(() => loadImage(poster))
.catch(err => {
// Hide poster on error unless it's been set by another call
if (poster === this.poster) {
ui.togglePoster.call(this, false);
}
// Rethrow
throw err;
})
.then(() => {
// Prevent race conditions
if (poster !== this.poster) {
throw new Error('setPoster cancelled by later call to setPoster');
}
})
.then(() => {
Object.assign(this.elements.poster.style, {
backgroundImage: `url('${poster}')`,
// Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube)
backgroundSize: '',
});
// Wait until ui is ready ui.togglePoster.call(this, true);
return (
ready
.call(this)
// Load image
.then(() => loadImage(poster))
.catch(err => {
// Hide poster on error unless it's been set by another call
if (poster === this.poster) {
ui.togglePoster.call(this, false);
}
// Rethrow
throw err;
})
.then(() => {
// Prevent race conditions
if (poster !== this.poster) {
throw new Error('setPoster cancelled by later call to setPoster');
}
})
.then(() => {
Object.assign(this.elements.poster.style, {
backgroundImage: `url('${poster}')`,
// Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube)
backgroundSize: '',
});
ui.togglePoster.call(this, true); return poster;
})
);
},
return poster; // Check playing state
}) checkPlaying(event) {
); // Class hooks
}, 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.stopped, this.stopped);
// Check playing state // Set state
checkPlaying(event) { Array.from(this.elements.buttons.play || []).forEach(target => {
// Class hooks Object.assign(target, { pressed: this.playing });
toggleClass(this.elements.container, this.config.classNames.playing, this.playing); target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config));
toggleClass(this.elements.container, this.config.classNames.paused, this.paused); });
toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);
// Set state // Only update controls on non timeupdate events
Array.from(this.elements.buttons.play || []).forEach(target => { if (is.event(event) && event.type === 'timeupdate') {
Object.assign(target, { pressed: this.playing }); return;
target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config)); }
});
// Only update controls on non timeupdate events // Toggle controls
if (is.event(event) && event.type === 'timeupdate') { ui.toggleControls.call(this);
return; },
}
// Toggle controls // 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 // Migrate any custom properties from the media to the parent
ui.toggleControls.call(this); migrateStyles() {
}, // Loop through values (as they are the keys when the object is spread 🤔)
this.loading ? 250 : 0, Object.values({ ...this.media.style })
); // We're only fussed about Plyr specific properties
}, .filter(key => key.startsWith('--plyr'))
.forEach(key => {
// Set on the container
this.elements.container.style.setProperty(key, this.media.style.getPropertyValue(key));
// Toggle controls based on state and `force` argument // Clean up from media element
toggleControls(force) { this.media.style.removeProperty(key);
const { controls: controlsElement } = this.elements; });
if (controlsElement && this.config.hideControls) { // Remove attribute if empty
// 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.) if (is.empty(this.media.style)) {
const recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now(); this.media.removeAttribute('style');
}
// 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;
+173 -175
View File
@@ -7,279 +7,277 @@ 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);
} }
// Closest ancestor element matching selector (also tests element itself) // Closest ancestor element matching selector (also tests element itself)
export function closest(element, selector) { export function closest(element, selector) {
const {prototype} = Element; const { prototype } = Element;
// https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
function closestElement() { function closestElement() {
let el = this; let el = this;
do { do {
if (matches.matches(el, selector)) return el; if (matches.matches(el, selector)) return el;
el = el.parentElement || el.parentNode; el = el.parentElement || el.parentNode;
} while (el !== null && el.nodeType === 1); } while (el !== null && el.nodeType === 1);
return null; return null;
} }
const method = const method = prototype.closest || closestElement;
prototype.closest ||
closestElement;
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;
} }
+47 -46
View File
@@ -5,74 +5,75 @@
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.config.vimeo.premium && this.supported.ui) {
const height = 240; const height = (100 / this.media.offsetWidth) * parseInt(window.getComputedStyle(this.media).paddingBottom, 10);
const offset = (height - padding) / (height / 50); const offset = (height - padding) / (height / 50);
this.media.style.transform = `translateY(-${offset}%)`;
} else if (this.isHTML5) {
wrapper.classList.toggle(this.config.classNames.videoFixedRatio, ratio !== null);
}
return { padding, ratio }; this.media.style.transform = `translateY(-${offset}%)`;
} else if (this.isHTML5) {
wrapper.classList.toggle(this.config.classNames.videoFixedRatio, ratio !== null);
}
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: $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: $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: $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: $plyr-badge-bg; background: $plyr-badge-background;
border-radius: 2px; border-radius: $plyr-badge-border-radius;
color: $plyr-badge-color; color: $plyr-badge-text-color;
font-size: $plyr-font-size-badge; font-size: $plyr-font-size-badge;
line-height: 1; line-height: 1;
padding: 3px 4px; padding: 3px 4px;
} }
+37 -38
View File
@@ -4,56 +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;
color: $plyr-captions-color; 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%;
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-bg; font-size: $plyr-font-size-captions-base;
border-radius: 2px; padding: calc(#{$plyr-control-spacing} * 2);
box-decoration-break: clone; }
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));
} }
.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;
}
}
+31 -31
View File
@@ -3,44 +3,44 @@
// -------------------------------------------------------------- // --------------------------------------------------------------
.plyr__control { .plyr__control {
background: transparent; background: transparent;
border: 0; border: 0;
border-radius: $plyr-control-radius; border-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: $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: $plyr-control-icon-size; height: $plyr-control-icon-size;
pointer-events: none; pointer-events: none;
width: $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
@@ -48,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-bg; }
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: $plyr-menu-arrow-size solid transparent;
border-top-color: $plyr-menu-background;
content: '';
height: 0;
position: absolute;
right: calc(((#{$plyr-control-icon-size} / 2) + #{$plyr-control-padding}) - (#{$plyr-menu-arrow-size} / 2));
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: $plyr-menu-item-arrow-size 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-item-arrow-color;
border-top-color: $plyr-menu-bg; right: calc((#{$plyr-control-padding} * 1.5) - #{$plyr-menu-item-arrow-size});
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-item-arrow-color;
left: calc((#{$plyr-control-padding} * 1.5) - #{$plyr-menu-item-arrow-size});
} }
[role='menuitem'], &::before {
[role='menuitemradio'] { background: $plyr-menu-back-border-color;
margin-top: 2px; box-shadow: 0 1px 0 $plyr-menu-back-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: $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,
$plyr-progress-loading-bg 25%, $plyr-progress-loading-background 25%,
transparent 25%, transparent 25%,
transparent 50%, transparent 50%,
$plyr-progress-loading-bg 50%, $plyr-progress-loading-background 50%,
$plyr-progress-loading-bg 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-bg; 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-bg; 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` property is used in JS to populate lower fill for WebKit
color: $plyr-range-fill-background;
display: block;
height: calc((#{$plyr-range-thumb-active-shadow-width} * 2) + #{$plyr-range-thumb-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-bg;
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();
}
} }
}
} }

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