Compare commits

...

41 Commits

Author SHA1 Message Date
Sam Potts 3ad118c026 3.4.0-beta.2 2018-08-05 22:43:35 +10:00
Sam Potts 0bc6b1f1b3 Fix issue where enter key wasn’t setting focus correctly 2018-08-05 22:41:21 +10:00
Sam Potts 4ea458e1a3 Rounded aria-valuetext to 1 decimal place 2018-08-05 21:48:42 +10:00
Sam Potts aacb172017 Removed aria-labelled-by 2018-08-05 21:48:21 +10:00
Sam Potts b96fcfc8ac v3.4.0-beta.1 2018-08-02 00:55:48 +10:00
Sam Potts 18b4d26bee Merge pull request #1142 from sampotts/a11y-improvements
A11y improvements
2018-08-02 00:47:57 +10:00
Sam Potts 7f4b74e2d4 Fix for hover over iframed players not showing controls 2018-08-02 00:47:03 +10:00
Sam Potts 0892d69ba2 Handle race condition for ads lib loading after source change 2018-08-01 13:56:49 +10:00
Sam Potts ba511b51c7 Box shadow fix for range track 2018-08-01 13:00:51 +10:00
Sam Potts e090581913 Ads on dev or prod only 2018-08-01 11:49:42 +10:00
Sam Potts aaa56caa9c Only focus button if menu wasn’t hidden already 2018-08-01 01:38:57 +10:00
Sam Potts c8db1e55dd Escape closes menu 2018-08-01 01:26:15 +10:00
Sam Potts 58079393e6 Build 2018-08-01 00:58:27 +10:00
Sam Potts 0b44f2d897 Demo config 2018-08-01 00:57:45 +10:00
Sam Potts 2371619486 Linting 2018-08-01 00:56:44 +10:00
Sam Potts 13a54b5dbe Merge branch 'develop' into a11y-improvements
# Conflicts:
#	src/js/controls.js
2018-08-01 00:46:26 +10:00
Sam Potts fa0861ff2e Merge pull request #1141 from friday/1137
Improve captions positioning consistency
2018-08-01 00:41:48 +10:00
Sam Potts 748aa5179f Comments about keydown vs keyup for Firefox 2018-08-01 00:38:19 +10:00
Sam Potts 56a485bac6 Fix Firefox spacebar issue 2018-08-01 00:37:55 +10:00
Albin Larsson 9488de30e5 Fix #1137: Improve captions positioning consistency 2018-07-31 16:26:34 +02:00
Sam Potts e3dfd16096 Merge pull request #1139 from friday/controls-input
Controls input fixes
2018-07-31 09:08:08 +10:00
Albin Larsson c230ccce86 Update controls.md docs 2018-07-31 00:44:07 +02:00
Albin Larsson db22a8e9c4 Improve handling of the 'controls' argument 2018-07-31 00:43:56 +02:00
Sam Potts 3a3358e2b4 Make iOS range fix more universal 2018-07-30 23:29:14 +10:00
Sam Potts 248005e8e0 Fix merge 2018-07-30 23:29:02 +10:00
Sam Potts dae272ef66 Merge branch 'develop' into a11y-improvements
# Conflicts:
#	demo/dist/demo.css
#	dist/plyr.css
#	dist/plyr.js.map
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.polyfilled.js.map
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
#	package.json
#	src/js/plyr.js
2018-07-30 23:09:12 +10:00
Sam Potts 599b33e55f Click to play fix, poster fix, iOS controls fixes 2018-07-30 01:13:12 +10:00
Sam Potts 3a8332bdb3 Fix for webkit redrawing issue 2018-07-29 12:32:26 +10:00
Sam Potts 53a3d06103 Merge branch 'develop' into a11y-improvements
# Conflicts:
#	demo/dist/demo.css
#	demo/dist/demo.js.map
#	demo/dist/demo.min.js
#	demo/dist/demo.min.js.map
#	dist/plyr.css
#	dist/plyr.js.map
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.polyfilled.js.map
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
#	package.json
#	yarn.lock
2018-07-24 09:38:26 +10:00
Sam Potts e63ad7c74b Keyboard and focus improvements 2018-07-15 19:23:28 +10:00
Sam Potts ead6601394 Merge 2018-07-02 23:11:59 +10:00
Sam Potts e61ebd8d05 Merge branch 'develop' into a11y-improvements 2018-07-02 23:11:50 +10:00
Sam Potts 3bf1c59bd6 Work on key bindings for menu 2018-06-28 23:44:07 +10:00
Sam Potts e59fe1aacf Merge branch 'develop' into a11y-improvements
# Conflicts:
#	dist/plyr.js
#	dist/plyr.js.map
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.polyfilled.js
#	dist/plyr.polyfilled.js.map
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
#	src/js/listeners.js
2018-06-25 23:09:13 +10:00
Sam Potts 1f1d74ba50 Work on menus 2018-06-21 09:01:16 +10:00
Sam Potts bb546fe43f Merge branch 'develop' into a11y-improvements
# Conflicts:
#	dist/plyr.js.map
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.polyfilled.js.map
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
2018-06-19 19:24:47 +10:00
Sam Potts 9e1218547b WIP 2018-06-19 09:11:35 +10:00
Sam Potts 715b88c09b Merge branch 'develop' into a11y-improvements
# Conflicts:
#	dist/plyr.js.map
#	dist/plyr.min.js
#	dist/plyr.min.js.map
#	dist/plyr.polyfilled.js.map
#	dist/plyr.polyfilled.min.js
#	dist/plyr.polyfilled.min.js.map
2018-06-18 23:29:25 +10:00
Sam Potts 7b9ef7d757 More work on menus 2018-06-18 23:13:40 +10:00
Sam Potts d64ed4ba5a Merge branch 'develop' into a11y-improvements
# Conflicts:
#	dist/plyr.js.map
#	dist/plyr.min.js
#	dist/plyr.min.js.map
2018-06-18 22:17:34 +10:00
Sam Potts ffd864ed39 Work on controls 2018-06-18 21:39:47 +10:00
44 changed files with 2957 additions and 2060 deletions
+1 -1
View File
@@ -8,4 +8,4 @@ npm-debug.log
yarn-error.log yarn-error.log
package-lock.json package-lock.json
*.webm *.webm
.idea/ .idea/
+2 -1
View File
@@ -2,5 +2,6 @@
"useTabs": false, "useTabs": false,
"tabWidth": 4, "tabWidth": 4,
"singleQuote": true, "singleQuote": true,
"trailingComma": "all" "trailingComma": "all",
"printWidth": 120
} }
+6
View File
@@ -1,3 +1,9 @@
# v3.4.0
- Accessibility improvements (see #905)
- Improvements to the way the controls work on iOS
- Demo code clean up
# v3.3.23 # v3.3.23
- Add support for YouTube's hl param (thanks @renaudleo) - Add support for YouTube's hl param (thanks @renaudleo)
+2
View File
@@ -3,7 +3,9 @@
This is the markup that is rendered for the Plyr controls. You can use the default controls or provide a customized version of markup based on your needs. You can pass the following to the `controls` option: This is the markup that is rendered for the Plyr controls. You can use the default controls or provide a customized version of markup based on your needs. You can pass the following to the `controls` option:
- `Array` of options (this builds the default controls based on your choices) - `Array` of options (this builds the default controls based on your choices)
- `Element` with the controls
- `String` containing the desired HTML - `String` containing the desired HTML
- `false` (or empty string or array) to disable all controls
- `Function` that will be executed and should return one of the above - `Function` that will be executed and should return one of the above
## Using default controls ## Using default controls
+1 -1
View File
File diff suppressed because one or more lines are too long
+50 -78
View File
@@ -1874,7 +1874,7 @@ typeof navigator === "object" && (function () {
// webpack (using a build step causes webpack #1617). Grunt verifies that // webpack (using a build step causes webpack #1617). Grunt verifies that
// this value matches package.json during build. // this value matches package.json during build.
// See: https://github.com/getsentry/raven-js/issues/465 // See: https://github.com/getsentry/raven-js/issues/465
VERSION: '3.26.3', VERSION: '3.26.4',
debug: false, debug: false,
@@ -2612,34 +2612,40 @@ typeof navigator === "object" && (function () {
) )
return; return;
options = options || {}; options = Object.assign(
{
eventId: this.lastEventId(),
dsn: this._dsn,
user: this._globalContext.user || {}
},
options
);
var lastEventId = options.eventId || this.lastEventId(); if (!options.eventId) {
if (!lastEventId) {
throw new configError('Missing eventId'); throw new configError('Missing eventId');
} }
var dsn = options.dsn || this._dsn; if (!options.dsn) {
if (!dsn) {
throw new configError('Missing DSN'); throw new configError('Missing DSN');
} }
var encode = encodeURIComponent; var encode = encodeURIComponent;
var qs = ''; var encodedOptions = [];
qs += '?eventId=' + encode(lastEventId);
qs += '&dsn=' + encode(dsn);
var user = options.user || this._globalContext.user; for (var key in options) {
if (user) { if (key === 'user') {
if (user.name) qs += '&name=' + encode(user.name); var user = options.user;
if (user.email) qs += '&email=' + encode(user.email); if (user.name) encodedOptions.push('name=' + encode(user.name));
if (user.email) encodedOptions.push('email=' + encode(user.email));
} else {
encodedOptions.push(encode(key) + '=' + encode(options[key]));
}
} }
var globalServer = this._getGlobalServer(this._parseDSN(options.dsn));
var globalServer = this._getGlobalServer(this._parseDSN(dsn));
var script = _document.createElement('script'); var script = _document.createElement('script');
script.async = true; script.async = true;
script.src = globalServer + '/api/embed/error-page/' + qs; script.src = globalServer + '/api/embed/error-page/?' + encodedOptions.join('&');
(_document.head || _document.body).appendChild(script); (_document.head || _document.body).appendChild(script);
}, },
@@ -4087,16 +4093,18 @@ typeof navigator === "object" && (function () {
// ========================================================================== // ==========================================================================
(function () { (function () {
var isLive = window.location.host === 'plyr.io'; var host = window.location.host;
// Raven / Sentry var env = {
// For demo site (https://plyr.io) only prod: host === 'plyr.io',
if (isLive) { dev: host === 'dev.plyr.io'
singleton.config('https://d4ad9866ad834437a4754e23937071e4@sentry.io/305555').install(); };
}
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
singleton.context(function () { singleton.context(function () {
var selector = '#player';
var container = document.getElementById('container');
if (window.shr) { if (window.shr) {
window.shr.setup({ window.shr.setup({
count: { count: {
@@ -4110,6 +4118,9 @@ typeof navigator === "object" && (function () {
// Remove class on blur // Remove class on blur
document.addEventListener('focusout', function (event) { document.addEventListener('focusout', function (event) {
if (container.contains(event.target)) {
return;
}
event.target.classList.remove(tabClassName); event.target.classList.remove(tabClassName);
}); });
@@ -4122,12 +4133,18 @@ typeof navigator === "object" && (function () {
// Delay the adding of classname until the focus has changed // Delay the adding of classname until the focus has changed
// This event fires before the focusin event // This event fires before the focusin event
setTimeout(function () { setTimeout(function () {
document.activeElement.classList.add(tabClassName); var focused = document.activeElement;
}, 0);
if (!focused || container.contains(focused)) {
return;
}
focused.classList.add(tabClassName);
}, 10);
}); });
// Setup the player // Setup the player
var player = new Plyr('#player', { var player = new Plyr(selector, {
debug: true, debug: true,
title: 'View From A Blue Moon', title: 'View From A Blue Moon',
iconUrl: '../dist/plyr.svg', iconUrl: '../dist/plyr.svg',
@@ -4137,57 +4154,6 @@ typeof navigator === "object" && (function () {
tooltips: { tooltips: {
controls: true controls: true
}, },
clickToPlay: false,
/* controls: [
'play-large',
'restart',
'rewind',
'play',
'fast-forward',
'progress',
'current-time',
'duration',
'mute',
'volume',
'captions',
'settings',
'pip',
'airplay',
'fullscreen',
], */
/* i18n: {
restart: '重新開始',
rewind: '快退{seektime}秒',
play: '播放',
pause: '暫停',
fastForward: '快進{seektime}秒',
seek: '尋求',
played: '發揮',
buffered: '緩衝的',
currentTime: '當前時間戳',
duration: '長短',
volume: '音量',
mute: '靜音',
unmute: '取消靜音',
enableCaptions: '開啟字幕',
disableCaptions: '關閉字幕',
enterFullscreen: '進入全螢幕',
exitFullscreen: '退出全螢幕',
frameTitle: '球員為{title}',
captions: '字幕',
settings: '設定',
speed: '速度',
normal: '正常',
quality: '質量',
loop: '循環',
start: 'Start',
end: 'End',
all: 'All',
reset: '重啟',
disabled: '殘',
enabled: '啟用',
advertisement: '廣告',
}, */
captions: { captions: {
active: true active: true
}, },
@@ -4195,7 +4161,7 @@ typeof navigator === "object" && (function () {
google: 'AIzaSyDrNwtN3nLH_8rjCmu5Wq3ZCm4MNAVdc0c' google: 'AIzaSyDrNwtN3nLH_8rjCmu5Wq3ZCm4MNAVdc0c'
}, },
ads: { ads: {
enabled: true, enabled: env.prod || env.dev,
publisherId: '918848828995742' publisherId: '918848828995742'
} }
}); });
@@ -4370,10 +4336,16 @@ typeof navigator === "object" && (function () {
}); });
}); });
// Raven / Sentry
// For demo site (https://plyr.io) only
if (env.prod) {
singleton.config('https://d4ad9866ad834437a4754e23937071e4@sentry.io/305555').install();
}
// Google analytics // Google analytics
// For demo site (https://plyr.io) only // For demo site (https://plyr.io) only
/* eslint-disable */ /* eslint-disable */
if (isLive) { if (env.prod) {
(function (i, s, o, g, r, a, m) { (function (i, s, o, g, r, a, m) {
i.GoogleAnalyticsObject = r; i.GoogleAnalyticsObject = r;
i[r] = i[r] || function () { i[r] = i[r] || function () {
+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
+15 -14
View File
@@ -91,21 +91,22 @@
</header> </header>
<main> <main>
<video controls crossorigin playsinline poster="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg" id="player"> <div id="container">
<!-- Video files --> <video controls crossorigin playsinline poster="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg" id="player">
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" type="video/mp4" size="576"> <!-- Video files -->
<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-576p.mp4" type="video/mp4" size="576">
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4" type="video/mp4" size="1080"> <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-1440p.mp4" type="video/mp4" size="1440"> --> <source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4" type="video/mp4" size="1080">
<!-- Caption files --> <!-- Caption files -->
<track kind="captions" label="English" srclang="en" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt" <track kind="captions" label="English" srclang="en" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt"
default> 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"> <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 --> <!-- 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> <a href="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" download>Download</a>
</video> </video>
</div>
<ul> <ul>
<li class="plyr__cite plyr__cite--video" hidden> <li class="plyr__cite plyr__cite--video" hidden>
@@ -166,7 +167,7 @@
</svg> </svg>
<p>If you think Plyr's good, <p>If you think Plyr's good,
<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" <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"
target="_blank" data-shr-network="twitter">tweet it</a> target="_blank" data-shr-network="twitter">tweet it</a> 👍
</p> </p>
</aside> </aside>
+29 -64
View File
@@ -7,16 +7,17 @@
import Raven from 'raven-js'; import Raven from 'raven-js';
(() => { (() => {
const isLive = window.location.host === 'plyr.io'; const { host } = window.location;
const env = {
// Raven / Sentry prod: host === 'plyr.io',
// For demo site (https://plyr.io) only dev: host === 'dev.plyr.io',
if (isLive) { };
Raven.config('https://d4ad9866ad834437a4754e23937071e4@sentry.io/305555').install();
}
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
Raven.context(() => { Raven.context(() => {
const selector = '#player';
const container = document.getElementById('container');
if (window.shr) { if (window.shr) {
window.shr.setup({ window.shr.setup({
count: { count: {
@@ -30,6 +31,9 @@ import Raven from 'raven-js';
// Remove class on blur // Remove class on blur
document.addEventListener('focusout', event => { document.addEventListener('focusout', event => {
if (container.contains(event.target)) {
return;
}
event.target.classList.remove(tabClassName); event.target.classList.remove(tabClassName);
}); });
@@ -42,12 +46,18 @@ import Raven from 'raven-js';
// Delay the adding of classname until the focus has changed // Delay the adding of classname until the focus has changed
// This event fires before the focusin event // This event fires before the focusin event
setTimeout(() => { setTimeout(() => {
document.activeElement.classList.add(tabClassName); const focused = document.activeElement;
}, 0);
if (!focused || container.contains(focused)) {
return;
}
focused.classList.add(tabClassName);
}, 10);
}); });
// Setup the player // Setup the player
const player = new Plyr('#player', { const player = new Plyr(selector, {
debug: true, debug: true,
title: 'View From A Blue Moon', title: 'View From A Blue Moon',
iconUrl: '../dist/plyr.svg', iconUrl: '../dist/plyr.svg',
@@ -57,57 +67,6 @@ import Raven from 'raven-js';
tooltips: { tooltips: {
controls: true, controls: true,
}, },
clickToPlay: false,
/* controls: [
'play-large',
'restart',
'rewind',
'play',
'fast-forward',
'progress',
'current-time',
'duration',
'mute',
'volume',
'captions',
'settings',
'pip',
'airplay',
'fullscreen',
], */
/* i18n: {
restart: '重新開始',
rewind: '快退{seektime}秒',
play: '播放',
pause: '暫停',
fastForward: '快進{seektime}秒',
seek: '尋求',
played: '發揮',
buffered: '緩衝的',
currentTime: '當前時間戳',
duration: '長短',
volume: '音量',
mute: '靜音',
unmute: '取消靜音',
enableCaptions: '開啟字幕',
disableCaptions: '關閉字幕',
enterFullscreen: '進入全螢幕',
exitFullscreen: '退出全螢幕',
frameTitle: '球員為{title}',
captions: '字幕',
settings: '設定',
speed: '速度',
normal: '正常',
quality: '質量',
loop: '循環',
start: 'Start',
end: 'End',
all: 'All',
reset: '重啟',
disabled: '殘',
enabled: '啟用',
advertisement: '廣告',
}, */
captions: { captions: {
active: true, active: true,
}, },
@@ -115,7 +74,7 @@ import Raven from 'raven-js';
google: 'AIzaSyDrNwtN3nLH_8rjCmu5Wq3ZCm4MNAVdc0c', google: 'AIzaSyDrNwtN3nLH_8rjCmu5Wq3ZCm4MNAVdc0c',
}, },
ads: { ads: {
enabled: true, enabled: env.prod || env.dev,
publisherId: '918848828995742', publisherId: '918848828995742',
}, },
}); });
@@ -311,11 +270,17 @@ import Raven from 'raven-js';
}); });
}); });
// Raven / Sentry
// For demo site (https://plyr.io) only
if (env.prod) {
Raven.config('https://d4ad9866ad834437a4754e23937071e4@sentry.io/305555').install();
}
// Google analytics // Google analytics
// For demo site (https://plyr.io) only // For demo site (https://plyr.io) only
/* eslint-disable */ /* eslint-disable */
if (isLive) { if (env.prod) {
(function(i, s, o, g, r, a, m) { ((i, s, o, g, r, a, m) => {
i.GoogleAnalyticsObject = r; i.GoogleAnalyticsObject = r;
i[r] = i[r] =
i[r] || i[r] ||
+2 -1
View File
@@ -2,7 +2,8 @@
// Typography // Typography
// ========================================================================== // ==========================================================================
$font-sans-serif: 'Gordita', 'Avenir', 'Helvetica Neue', sans-serif; $font-sans-serif: 'Gordita', 'Avenir', 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol';
$font-size-base: 15; $font-size-base: 15;
$font-size-small: 13; $font-size-small: 13;
+1 -1
View File
File diff suppressed because one or more lines are too long
+866 -573
View File
File diff suppressed because it is too large Load Diff
+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
+910 -606
View File
File diff suppressed because it is too large Load Diff
+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
@@ -1,6 +1,6 @@
{ {
"name": "plyr", "name": "plyr",
"version": "3.3.23", "version": "3.4.0-beta.2",
"description": "description":
"A simple, accessible and customizable HTML5, YouTube and Vimeo media player", "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
"homepage": "https://plyr.io", "homepage": "https://plyr.io",
+6 -6
View File
@@ -132,13 +132,13 @@ See [initialising](#initialising) for more information on advanced setups.
You can use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript. There's 2 versions; one with and one without [polyfills](#polyfills). My recommendation would be to manage polyfills seperately as part of your application but to make life easier you can use the polyfilled build. You can use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript. There's 2 versions; one with and one without [polyfills](#polyfills). My recommendation would be to manage polyfills seperately as part of your application but to make life easier you can use the polyfilled build.
```html ```html
<script src="https://cdn.plyr.io/3.3.23/plyr.js"></script> <script src="https://cdn.plyr.io/3.4.0-beta.2/plyr.js"></script>
``` ```
...or... ...or...
```html ```html
<script src="https://cdn.plyr.io/3.3.23/plyr.polyfilled.js"></script> <script src="https://cdn.plyr.io/3.4.0-beta.2/plyr.polyfilled.js"></script>
``` ```
### CSS ### CSS
@@ -152,19 +152,19 @@ Include the `plyr.css` stylsheet into your `<head>`
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the default CSS, you can use the following: If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the default CSS, you can use the following:
```html ```html
<link rel="stylesheet" href="https://cdn.plyr.io/3.3.23/plyr.css"> <link rel="stylesheet" href="https://cdn.plyr.io/3.4.0-beta.2/plyr.css">
``` ```
### SVG Sprite ### SVG Sprite
The SVG sprite is loaded automatically from our CDN (provided by [Fastly](https://www.fastly.com/)). To change this, see the [options](#options) below. For The SVG sprite is loaded automatically from our CDN (provided by [Fastly](https://www.fastly.com/)). To change this, see the [options](#options) below. For
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.3.23/plyr.svg`. reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.4.0-beta.2/plyr.svg`.
## Ads ## Ads
Plyr has partnered up with [vi.ai](http://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](http://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
+3 -1
View File
@@ -84,7 +84,9 @@ const captions = {
// * toggled: The real captions state // * toggled: The real captions state
const languages = dedupe( const languages = dedupe(
Array.from(navigator.languages || navigator.language || navigator.userLanguage).map(language => language.split('-')[0]), Array.from(navigator.languages || navigator.language || navigator.userLanguage).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();
+3
View File
@@ -354,6 +354,9 @@ const defaults = {
isTouch: 'plyr--is-touch', isTouch: 'plyr--is-touch',
uiSupported: 'plyr--full-ui', uiSupported: 'plyr--full-ui',
noTransition: 'plyr--no-transition', noTransition: 'plyr--no-transition',
display: {
time: 'plyr__time',
},
menu: { menu: {
value: 'plyr__menu__value', value: 'plyr__menu__value',
badge: 'plyr__badge', badge: 'plyr__badge',
+439 -286
View File
@@ -1,5 +1,6 @@
// ========================================================================== // ==========================================================================
// Plyr controls // Plyr controls
// TODO: This needs to be split into smaller files and cleaned up
// ========================================================================== // ==========================================================================
import captions from './captions'; import captions from './captions';
@@ -9,19 +10,7 @@ import support from './support';
import { repaint, transitionEndEvent } from './utils/animation'; import { repaint, transitionEndEvent } from './utils/animation';
import { dedupe } from './utils/arrays'; import { dedupe } from './utils/arrays';
import browser from './utils/browser'; import browser from './utils/browser';
import { import { createElement, emptyElement, getAttributesFromSelector, getElement, getElements, hasClass, matches, removeElement, setAttributes, setFocus, toggleClass, toggleHidden } from './utils/elements';
createElement,
emptyElement,
getAttributesFromSelector,
getElement,
getElements,
hasClass,
matches,
removeElement,
setAttributes,
toggleClass,
toggleHidden,
} from './utils/elements';
import { off, on } from './utils/events'; import { off, on } from './utils/events';
import is from './utils/is'; import is from './utils/is';
import loadSprite from './utils/loadSprite'; import loadSprite from './utils/loadSprite';
@@ -243,12 +232,28 @@ const controls = {
// Setup toggle icon and labels // Setup toggle icon and labels
if (toggle) { if (toggle) {
// Icon // Icon
button.appendChild(controls.createIcon.call(this, iconPressed, { class: 'icon--pressed' })); button.appendChild(
button.appendChild(controls.createIcon.call(this, icon, { class: 'icon--not-pressed' })); controls.createIcon.call(this, iconPressed, {
class: 'icon--pressed',
}),
);
button.appendChild(
controls.createIcon.call(this, icon, {
class: 'icon--not-pressed',
}),
);
// Label/Tooltip // Label/Tooltip
button.appendChild(controls.createLabel.call(this, labelPressed, { class: 'label--pressed' })); button.appendChild(
button.appendChild(controls.createLabel.call(this, label, { class: 'label--not-pressed' })); controls.createLabel.call(this, labelPressed, {
class: 'label--pressed',
}),
);
button.appendChild(
controls.createLabel.call(this, label, {
class: 'label--not-pressed',
}),
);
} else { } else {
button.appendChild(controls.createIcon.call(this, icon)); button.appendChild(controls.createIcon.call(this, icon));
button.appendChild(controls.createLabel.call(this, label)); button.appendChild(controls.createLabel.call(this, label));
@@ -360,7 +365,7 @@ const controls = {
const container = createElement( const container = createElement(
'div', 'div',
extend(attributes, { extend(attributes, {
class: `plyr__time ${attributes.class}`, class: `${this.config.classNames.display.time} ${attributes.class ? attributes.class : ''}`.trim(),
'aria-label': i18n.get(type, this.config), 'aria-label': i18n.get(type, this.config),
}), }),
'00:00', '00:00',
@@ -372,37 +377,153 @@ const controls = {
return container; return container;
}, },
// Bind keyboard shortcuts for a menu item
// We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus
// https://bugzilla.mozilla.org/show_bug.cgi?id=1220143
bindMenuItemShortcuts(menuItem, type) {
// Navigate through menus via arrow keys and space
on(
menuItem,
'keydown keyup',
event => {
// We only care about space and ⬆️ ⬇️ ➡️
if (![32, 38, 39, 40].includes(event.which)) {
return;
}
// Prevent play / seek
event.preventDefault();
event.stopPropagation();
// We're just here to prevent the keydown bubbling
if (event.type === 'keydown') {
return;
}
const isRadioButton = matches(menuItem, '[role="menuitemradio"]');
// Show the respective menu
if (!isRadioButton && [32, 39].includes(event.which)) {
controls.showMenuPanel.call(this, type, true);
} else {
let target;
if (event.which !== 32) {
if (event.which === 40 || (isRadioButton && event.which === 39)) {
target = menuItem.nextElementSibling;
if (!is.element(target)) {
target = menuItem.parentNode.firstElementChild;
}
} else {
target = menuItem.previousElementSibling;
if (!is.element(target)) {
target = menuItem.parentNode.lastElementChild;
}
}
setFocus.call(this, target, true);
}
}
},
false,
);
// Enter will fire a `click` event but we still need to manage focus
// So we bind to keyup which fires after and set focus here
on(menuItem, 'keyup', event => {
if (event.which !== 13) {
return;
}
controls.focusFirstMenuItem.call(this, null, true);
});
},
// Create a settings menu item // Create a settings menu item
createMenuItem({ value, list, type, title, badge = null, checked = false }) { createMenuItem({ value, list, type, title, badge = null, checked = false }) {
const item = createElement('li'); const attributes = getAttributesFromSelector(this.config.selectors.inputs[type]);
const label = createElement('label', { const menuItem = createElement(
class: this.config.classNames.control, 'button',
}); extend(attributes, {
type: 'button',
const radio = createElement( role: 'menuitemradio',
'input', class: `${this.config.classNames.control} ${attributes.class ? attributes.class : ''}`.trim(),
extend(getAttributesFromSelector(this.config.selectors.inputs[type]), { 'aria-checked': checked,
type: 'radio',
name: `plyr-${type}`,
value, value,
checked,
class: 'plyr__sr-only',
}), }),
); );
const faux = createElement('span', { hidden: '' }); const flex = createElement('span');
label.appendChild(radio); // We have to set as HTML incase of special characters
label.appendChild(faux); flex.innerHTML = title;
label.insertAdjacentHTML('beforeend', title);
if (is.element(badge)) { if (is.element(badge)) {
label.appendChild(badge); flex.appendChild(badge);
} }
item.appendChild(label); menuItem.appendChild(flex);
list.appendChild(item);
// Replicate radio button behaviour
Object.defineProperty(menuItem, 'checked', {
enumerable: true,
get() {
return menuItem.getAttribute('aria-checked') === 'true';
},
set(checked) {
// Ensure exclusivity
if (checked) {
Array.from(menuItem.parentNode.children)
.filter(node => matches(node, '[role="menuitemradio"]'))
.forEach(node => node.setAttribute('aria-checked', 'false'));
}
menuItem.setAttribute('aria-checked', checked ? 'true' : 'false');
},
});
this.listeners.bind(
menuItem,
'click keyup',
event => {
if (is.keyboardEvent(event) && event.which !== 32) {
return;
}
event.preventDefault();
event.stopPropagation();
menuItem.checked = true;
switch (type) {
case 'language':
this.currentTrack = Number(value);
break;
case 'quality':
this.quality = value;
break;
case 'speed':
this.speed = parseFloat(value);
break;
default:
break;
}
controls.showMenuPanel.call(this, 'home', is.keyboardEvent(event));
},
type,
false,
);
controls.bindMenuItemShortcuts.call(this, menuItem, type);
list.appendChild(menuItem);
}, },
// Format a time for display // Format a time for display
@@ -534,7 +655,7 @@ const controls = {
} else if (matches(range, this.config.selectors.inputs.volume)) { } else if (matches(range, this.config.selectors.inputs.volume)) {
const percent = range.value * 100; const percent = range.value * 100;
range.setAttribute('aria-valuenow', percent); range.setAttribute('aria-valuenow', percent);
range.setAttribute('aria-valuetext', `${percent}%`); range.setAttribute('aria-valuetext', `${percent.toFixed(1)}%`);
} else { } else {
range.setAttribute('aria-valuenow', range.value); range.setAttribute('aria-valuenow', range.value);
} }
@@ -637,7 +758,7 @@ const controls = {
// https://github.com/video-dev/hls.js/blob/5820d29d3c4c8a46e8b75f1e3afa3e68c1a9a2db/src/controller/buffer-controller.js#L415 // https://github.com/video-dev/hls.js/blob/5820d29d3c4c8a46e8b75f1e3afa3e68c1a9a2db/src/controller/buffer-controller.js#L415
// https://github.com/google/shaka-player/blob/4d889054631f4e1cf0fbd80ddd2b71887c02e232/lib/media/streaming_engine.js#L1062 // https://github.com/google/shaka-player/blob/4d889054631f4e1cf0fbd80ddd2b71887c02e232/lib/media/streaming_engine.js#L1062
// https://github.com/Dash-Industry-Forum/dash.js/blob/69859f51b969645b234666800d4cb596d89c602d/src/dash/models/DashManifestModel.js#L338 // https://github.com/Dash-Industry-Forum/dash.js/blob/69859f51b969645b234666800d4cb596d89c602d/src/dash/models/DashManifestModel.js#L338
if (this.duration >= 2**32) { if (this.duration >= 2 ** 32) {
toggleHidden(this.elements.display.currentTime, true); toggleHidden(this.elements.display.currentTime, true);
toggleHidden(this.elements.progress, true); toggleHidden(this.elements.progress, true);
return; return;
@@ -666,19 +787,97 @@ const controls = {
}, },
// Hide/show a tab // Hide/show a tab
toggleTab(setting, toggle) { toggleMenuButton(setting, toggle) {
toggleHidden(this.elements.settings.tabs[setting], !toggle); toggleHidden(this.elements.settings.buttons[setting], !toggle);
},
// Update the selected setting
updateSetting(setting, container, input) {
const pane = this.elements.settings.panels[setting];
let value = null;
let list = container;
if (setting === 'captions') {
value = this.currentTrack;
} else {
value = !is.empty(input) ? input : this[setting];
// Get default
if (is.empty(value)) {
value = this.config[setting].default;
}
// Unsupported value
if (!is.empty(this.options[setting]) && !this.options[setting].includes(value)) {
this.debug.warn(`Unsupported value of '${value}' for ${setting}`);
return;
}
// Disabled value
if (!this.config[setting].options.includes(value)) {
this.debug.warn(`Disabled value of '${value}' for ${setting}`);
return;
}
}
// Get the list if we need to
if (!is.element(list)) {
list = pane && pane.querySelector('[role="menu"]');
}
// If there's no list it means it's not been rendered...
if (!is.element(list)) {
return;
}
// Update the label
const label = this.elements.settings.buttons[setting].querySelector(`.${this.config.classNames.menu.value}`);
label.innerHTML = controls.getLabel.call(this, setting, value);
// Find the radio option and check it
const target = list && list.querySelector(`[value="${value}"]`);
if (is.element(target)) {
target.checked = true;
}
},
// Translate a value into a nice label
getLabel(setting, value) {
switch (setting) {
case 'speed':
return value === 1 ? i18n.get('normal', this.config) : `${value}&times;`;
case 'quality':
if (is.number(value)) {
const label = i18n.get(`qualityLabel.${value}`, this.config);
if (!label.length) {
return `${value}p`;
}
return label;
}
return toTitleCase(value);
case 'captions':
return captions.getLabel.call(this);
default:
return null;
}
}, },
// Set the quality menu // Set the quality menu
setQualityMenu(options) { setQualityMenu(options) {
// Menu required // Menu required
if (!is.element(this.elements.settings.panes.quality)) { if (!is.element(this.elements.settings.panels.quality)) {
return; return;
} }
const type = 'quality'; const type = 'quality';
const list = this.elements.settings.panes.quality.querySelector('ul'); const list = this.elements.settings.panels.quality.querySelector('[role="menu"]');
// Set options if passed and filter based on uniqueness and config // Set options if passed and filter based on uniqueness and config
if (is.array(options)) { if (is.array(options)) {
@@ -687,7 +886,10 @@ const controls = {
// Toggle the pane and tab // Toggle the pane and tab
const toggle = !is.empty(this.options.quality) && this.options.quality.length > 1; const toggle = !is.empty(this.options.quality) && this.options.quality.length > 1;
controls.toggleTab.call(this, type, toggle); controls.toggleMenuButton.call(this, type, toggle);
// Empty the menu
emptyElement(list);
// Check if we need to toggle the parent // Check if we need to toggle the parent
controls.checkMenu.call(this); controls.checkMenu.call(this);
@@ -697,9 +899,6 @@ const controls = {
return; return;
} }
// Empty the menu
emptyElement(list);
// Get the badge HTML for HD, 4K etc // Get the badge HTML for HD, 4K etc
const getBadge = quality => { const getBadge = quality => {
const label = i18n.get(`qualityBadge.${quality}`, this.config); const label = i18n.get(`qualityBadge.${quality}`, this.config);
@@ -730,101 +929,23 @@ const controls = {
controls.updateSetting.call(this, type, list); controls.updateSetting.call(this, type, list);
}, },
// Translate a value into a nice label
getLabel(setting, value) {
switch (setting) {
case 'speed':
return value === 1 ? i18n.get('normal', this.config) : `${value}&times;`;
case 'quality':
if (is.number(value)) {
const label = i18n.get(`qualityLabel.${value}`, this.config);
if (!label.length) {
return `${value}p`;
}
return label;
}
return toTitleCase(value);
case 'captions':
return captions.getLabel.call(this);
default:
return null;
}
},
// Update the selected setting
updateSetting(setting, container, input) {
const pane = this.elements.settings.panes[setting];
let value = null;
let list = container;
if (setting === 'captions') {
value = this.currentTrack;
} else {
value = !is.empty(input) ? input : this[setting];
// Get default
if (is.empty(value)) {
value = this.config[setting].default;
}
// Unsupported value
if (!is.empty(this.options[setting]) && !this.options[setting].includes(value)) {
this.debug.warn(`Unsupported value of '${value}' for ${setting}`);
return;
}
// Disabled value
if (!this.config[setting].options.includes(value)) {
this.debug.warn(`Disabled value of '${value}' for ${setting}`);
return;
}
}
// Get the list if we need to
if (!is.element(list)) {
list = pane && pane.querySelector('ul');
}
// If there's no list it means it's not been rendered...
if (!is.element(list)) {
return;
}
// Update the label
const label = this.elements.settings.tabs[setting].querySelector(`.${this.config.classNames.menu.value}`);
label.innerHTML = controls.getLabel.call(this, setting, value);
// Find the radio option and check it
const target = list && list.querySelector(`input[value="${value}"]`);
if (is.element(target)) {
target.checked = true;
}
},
// Set the looping options // Set the looping options
/* setLoopMenu() { /* setLoopMenu() {
// Menu required // Menu required
if (!is.element(this.elements.settings.panes.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.panes.loop.querySelector('ul'); const list = this.elements.settings.panels.loop.querySelector('[role="menu"]');
// Show the pane and tab // Show the pane and tab
toggleHidden(this.elements.settings.tabs.loop, false); toggleHidden(this.elements.settings.buttons.loop, false);
toggleHidden(this.elements.settings.panes.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.toggleTab.call(this, 'loop', toggle); controls.toggleMenuButton.call(this, 'loop', toggle);
// Empty the menu // Empty the menu
emptyElement(list); emptyElement(list);
@@ -857,13 +978,19 @@ const controls = {
// Set a list of available captions languages // Set a list of available captions languages
setCaptionsMenu() { setCaptionsMenu() {
// Menu required
if (!is.element(this.elements.settings.panels.captions)) {
return;
}
// TODO: Captions or language? Currently it's mixed // TODO: Captions or language? Currently it's mixed
const type = 'captions'; const type = 'captions';
const list = this.elements.settings.panes.captions.querySelector('ul'); const list = this.elements.settings.panels.captions.querySelector('[role="menu"]');
const tracks = captions.getTracks.call(this); const tracks = captions.getTracks.call(this);
const toggle = Boolean(tracks.length);
// Toggle the pane and tab // Toggle the pane and tab
controls.toggleTab.call(this, type, tracks.length); controls.toggleMenuButton.call(this, type, toggle);
// Empty the menu // Empty the menu
emptyElement(list); emptyElement(list);
@@ -872,7 +999,7 @@ const controls = {
controls.checkMenu.call(this); controls.checkMenu.call(this);
// If there's no captions, bail // If there's no captions, bail
if (!tracks.length) { if (!toggle) {
return; return;
} }
@@ -903,17 +1030,13 @@ const controls = {
// Set a list of available captions languages // Set a list of available captions languages
setSpeedMenu(options) { setSpeedMenu(options) {
// Do nothing if not selected
if (!this.config.controls.includes('settings') || !this.config.settings.includes('speed')) {
return;
}
// Menu required // Menu required
if (!is.element(this.elements.settings.panes.speed)) { if (!is.element(this.elements.settings.panels.speed)) {
return; return;
} }
const type = 'speed'; const type = 'speed';
const list = this.elements.settings.panels.speed.querySelector('[role="menu"]');
// Set the speed options // Set the speed options
if (is.array(options)) { if (is.array(options)) {
@@ -927,7 +1050,10 @@ const controls = {
// Toggle the pane and tab // Toggle the pane and tab
const toggle = !is.empty(this.options.speed) && this.options.speed.length > 1; const toggle = !is.empty(this.options.speed) && this.options.speed.length > 1;
controls.toggleTab.call(this, type, toggle); controls.toggleMenuButton.call(this, type, toggle);
// Empty the menu
emptyElement(list);
// Check if we need to toggle the parent // Check if we need to toggle the parent
controls.checkMenu.call(this); controls.checkMenu.call(this);
@@ -937,12 +1063,6 @@ const controls = {
return; return;
} }
// Get the list to populate
const list = this.elements.settings.panes.speed.querySelector('ul');
// Empty the menu
emptyElement(list);
// Create items // Create items
this.options.speed.forEach(speed => { this.options.speed.forEach(speed => {
controls.createMenuItem.call(this, { controls.createMenuItem.call(this, {
@@ -958,71 +1078,84 @@ const controls = {
// Check if we need to hide/show the settings menu // Check if we need to hide/show the settings menu
checkMenu() { checkMenu() {
const { tabs } = this.elements.settings; const { buttons } = this.elements.settings;
const visible = !is.empty(tabs) && Object.values(tabs).some(tab => !tab.hidden); const visible = !is.empty(buttons) && Object.values(buttons).some(button => !button.hidden);
toggleHidden(this.elements.settings.menu, !visible); toggleHidden(this.elements.settings.menu, !visible);
}, },
// Show/hide menu // Focus the first menu item in a given (or visible) menu
toggleMenu(event) { focusFirstMenuItem(pane, tabFocus = false) {
const { form } = this.elements.settings; if (this.elements.settings.popup.hidden) {
const button = this.elements.buttons.settings;
// Menu and button are required
if (!is.element(form) || !is.element(button)) {
return; return;
} }
const show = is.boolean(event) ? event : is.element(form) && form.hasAttribute('hidden'); let target = pane;
if (is.event(event)) { if (!is.element(target)) {
const isMenuItem = is.element(form) && form.contains(event.target); target = Object.values(this.elements.settings.panels).find(pane => !pane.hidden);
const isButton = event.target === this.elements.buttons.settings; }
// If the click was inside the form or if the click const firstItem = target.querySelector('[role^="menuitem"]');
setFocus.call(this, firstItem, tabFocus);
},
// Show/hide menu
toggleMenu(input) {
const { popup } = this.elements.settings;
const button = this.elements.buttons.settings;
// Menu and button are required
if (!is.element(popup) || !is.element(button)) {
return;
}
// True toggle by default
const { hidden } = popup;
let show = hidden;
if (is.boolean(input)) {
show = input;
} else if (is.keyboardEvent(input) && input.which === 27) {
show = false;
} else if (is.event(input)) {
const isMenuItem = popup.contains(input.target);
// If the click was inside the menu or if the click
// wasn't the button or menu item and we're trying to // wasn't the button or menu item and we're trying to
// show the menu (a doc click shouldn't show the menu) // show the menu (a doc click shouldn't show the menu)
if (isMenuItem || (!isMenuItem && !isButton && show)) { if (isMenuItem || (!isMenuItem && input.target !== button && show)) {
return; return;
} }
// Prevent the toggle being caught by the doc listener
if (isButton) {
event.stopPropagation();
}
} }
// Set form and button attributes // Set button attributes
if (is.element(button)) { button.setAttribute('aria-expanded', show);
button.setAttribute('aria-expanded', show);
// Show the actual popup
toggleHidden(popup, !show);
// Add class hook
toggleClass(this.elements.container, this.config.classNames.menu.open, show);
// Focus the first item if key interaction
if (show && is.keyboardEvent(input)) {
controls.focusFirstMenuItem.call(this, null, true);
} }
// If closing, re-focus the button
if (is.element(form)) { else if (!show && !hidden) {
toggleHidden(form, !show); setFocus.call(this, button, is.keyboardEvent(input));
toggleClass(this.elements.container, this.config.classNames.menu.open, show);
if (show) {
form.removeAttribute('tabindex');
} else {
form.setAttribute('tabindex', -1);
}
} }
}, },
// Get the natural size of a tab // Get the natural size of a menu panel
getTabSize(tab) { getMenuSize(tab) {
const clone = tab.cloneNode(true); const clone = tab.cloneNode(true);
clone.style.position = 'absolute'; clone.style.position = 'absolute';
clone.style.opacity = 0; clone.style.opacity = 0;
clone.removeAttribute('hidden'); clone.removeAttribute('hidden');
// Prevent input's being unchecked due to the name being identical
Array.from(clone.querySelectorAll('input[name]')).forEach(input => {
const name = input.getAttribute('name');
input.setAttribute('name', `${name}-clone`);
});
// Append to parent so we get the "real" size // Append to parent so we get the "real" size
tab.parentNode.appendChild(clone); tab.parentNode.appendChild(clone);
@@ -1039,31 +1172,18 @@ const controls = {
}; };
}, },
// Toggle Menu // Show a panel in the menu
showTab(target = '') { showMenuPanel(type = '', tabFocus = false) {
const { menu } = this.elements.settings; const target = document.getElementById(`plyr-settings-${this.id}-${type}`);
const pane = document.getElementById(target);
// Nothing to show, bail // Nothing to show, bail
if (!is.element(pane)) { if (!is.element(target)) {
return; return;
} }
// Are we targeting a tab? If not, bail // Hide all other panels
const isTab = pane.getAttribute('role') === 'tabpanel'; const container = target.parentNode;
if (!isTab) { const current = Array.from(container.children).find(node => !node.hidden);
return;
}
// Hide all other tabs
// Get other tabs
const current = menu.querySelector('[role="tabpanel"]:not([hidden])');
const container = current.parentNode;
// Set other toggles to be expanded false
Array.from(menu.querySelectorAll(`[aria-controls="${current.getAttribute('id')}"]`)).forEach(toggle => {
toggle.setAttribute('aria-expanded', false);
});
// If we can do fancy animations, we'll animate the height/width // If we can do fancy animations, we'll animate the height/width
if (support.transitions && !support.reducedMotion) { if (support.transitions && !support.reducedMotion) {
@@ -1072,12 +1192,12 @@ const controls = {
container.style.height = `${current.scrollHeight}px`; container.style.height = `${current.scrollHeight}px`;
// Get potential sizes // Get potential sizes
const size = controls.getTabSize.call(this, pane); const size = controls.getMenuSize.call(this, target);
// Restore auto height/width // Restore auto height/width
const restore = e => { const restore = event => {
// We're only bothered about height and width on the container // We're only bothered about height and width on the container
if (e.target !== container || !['width', 'height'].includes(e.propertyName)) { if (event.target !== container || !['width', 'height'].includes(event.propertyName)) {
return; return;
} }
@@ -1099,29 +1219,17 @@ const controls = {
// Set attributes on current tab // Set attributes on current tab
toggleHidden(current, true); toggleHidden(current, true);
current.setAttribute('tabindex', -1);
// Set attributes on target // Set attributes on target
toggleHidden(pane, false); toggleHidden(target, false);
const tabs = getElements.call(this, `[aria-controls="${target}"]`);
Array.from(tabs).forEach(tab => {
tab.setAttribute('aria-expanded', true);
});
pane.removeAttribute('tabindex');
// Focus the first item // Focus the first item
pane.querySelectorAll('button:not(:disabled), input:not(:disabled), [tabindex]')[0].focus(); controls.focusFirstMenuItem.call(this, target, tabFocus);
}, },
// Build the default HTML // Build the default HTML
// TODO: Set order based on order in the config.controls array? // TODO: Set order based on order in the config.controls array?
create(data) { create(data) {
// Do nothing if we want no controls
if (is.empty(this.config.controls)) {
return null;
}
// Create the container // Create the container
const container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper)); const container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper));
@@ -1230,62 +1338,64 @@ const controls = {
// Settings button / menu // Settings button / menu
if (this.config.controls.includes('settings') && !is.empty(this.config.settings)) { if (this.config.controls.includes('settings') && !is.empty(this.config.settings)) {
const menu = createElement('div', { const control = createElement('div', {
class: 'plyr__menu', class: 'plyr__menu',
hidden: '', hidden: '',
}); });
menu.appendChild( control.appendChild(
controls.createButton.call(this, 'settings', { controls.createButton.call(this, 'settings', {
id: `plyr-settings-toggle-${data.id}`,
'aria-haspopup': true, 'aria-haspopup': true,
'aria-controls': `plyr-settings-${data.id}`, 'aria-controls': `plyr-settings-${data.id}`,
'aria-expanded': false, 'aria-expanded': false,
}), }),
); );
const form = createElement('form', { const popup = createElement('div', {
class: 'plyr__menu__container', class: 'plyr__menu__container',
id: `plyr-settings-${data.id}`, id: `plyr-settings-${data.id}`,
hidden: '', hidden: '',
'aria-labelled-by': `plyr-settings-toggle-${data.id}`,
role: 'tablist',
tabindex: -1,
}); });
const inner = createElement('div'); const inner = createElement('div');
const home = createElement('div', { const home = createElement('div', {
id: `plyr-settings-${data.id}-home`, id: `plyr-settings-${data.id}-home`,
'aria-labelled-by': `plyr-settings-toggle-${data.id}`,
role: 'tabpanel',
}); });
// Create the tab list // Create the menu
const tabs = createElement('ul', { const menu = createElement('div', {
role: 'tablist', role: 'menu',
}); });
// Build the tabs home.appendChild(menu);
inner.appendChild(home);
this.elements.settings.panels.home = home;
// Build the menu items
this.config.settings.forEach(type => { this.config.settings.forEach(type => {
const tab = createElement('li', { // TODO: bundle this with the createMenuItem helper and bindings
role: 'tab', const menuItem = createElement(
hidden: '',
});
const button = createElement(
'button', 'button',
extend(getAttributesFromSelector(this.config.selectors.buttons.settings), { extend(getAttributesFromSelector(this.config.selectors.buttons.settings), {
type: 'button', type: 'button',
class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`, class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`,
id: `plyr-settings-${data.id}-${type}-tab`, role: 'menuitem',
'aria-haspopup': true, 'aria-haspopup': true,
'aria-controls': `plyr-settings-${data.id}-${type}`, hidden: '',
'aria-expanded': false,
}), }),
i18n.get(type, this.config),
); );
// Bind menu shortcuts for keyboard users
controls.bindMenuItemShortcuts.call(this, menuItem, type);
// Show menu on click
on(menuItem, 'click', () => {
controls.showMenuPanel.call(this, type, false);
});
const flex = createElement('span', null, i18n.get(type, this.config));
const value = createElement('span', { const value = createElement('span', {
class: this.config.classNames.menu.value, class: this.config.classNames.menu.value,
}); });
@@ -1293,54 +1403,91 @@ const controls = {
// Speed contains HTML entities // Speed contains HTML entities
value.innerHTML = data[type]; value.innerHTML = data[type];
button.appendChild(value); flex.appendChild(value);
tab.appendChild(button); menuItem.appendChild(flex);
tabs.appendChild(tab); menu.appendChild(menuItem);
this.elements.settings.tabs[type] = tab; // Build the panes
});
home.appendChild(tabs);
inner.appendChild(home);
// Build the panes
this.config.settings.forEach(type => {
const pane = createElement('div', { const pane = createElement('div', {
id: `plyr-settings-${data.id}-${type}`, id: `plyr-settings-${data.id}-${type}`,
hidden: '', hidden: '',
'aria-labelled-by': `plyr-settings-${data.id}-${type}-tab`,
role: 'tabpanel',
tabindex: -1,
}); });
const back = createElement( // Back button
'button', const backButton = createElement('button', {
{ type: 'button',
type: 'button', class: `${this.config.classNames.control} ${this.config.classNames.control}--back`,
class: `${this.config.classNames.control} ${this.config.classNames.control}--back`, });
'aria-haspopup': true,
'aria-controls': `plyr-settings-${data.id}-home`, // Visible label
'aria-expanded': false, backButton.appendChild(
}, createElement(
i18n.get(type, this.config), 'span',
{
'aria-hidden': true,
},
i18n.get(type, this.config),
),
); );
pane.appendChild(back); // Screen reader label
backButton.appendChild(
createElement(
'span',
{
class: this.config.classNames.hidden,
},
i18n.get('menuBack', this.config),
),
);
const options = createElement('ul'); // Go back via keyboard
on(
pane,
'keydown',
event => {
// We only care about <-
if (event.which !== 37) {
return;
}
// Prevent seek
event.preventDefault();
event.stopPropagation();
// Show the respective menu
controls.showMenuPanel.call(this, 'home', true);
},
false,
);
// Go back via button click
on(backButton, 'click', () => {
controls.showMenuPanel.call(this, 'home', false);
});
// Add to pane
pane.appendChild(backButton);
// Menu
pane.appendChild(
createElement('div', {
role: 'menu',
}),
);
pane.appendChild(options);
inner.appendChild(pane); inner.appendChild(pane);
this.elements.settings.panes[type] = pane; this.elements.settings.buttons[type] = menuItem;
this.elements.settings.panels[type] = pane;
}); });
form.appendChild(inner); popup.appendChild(inner);
menu.appendChild(form); control.appendChild(popup);
container.appendChild(menu); container.appendChild(control);
this.elements.settings.form = form; this.elements.settings.popup = popup;
this.elements.settings.menu = menu; this.elements.settings.menu = control;
} }
// Picture in picture button // Picture in picture button
@@ -1401,13 +1548,19 @@ const controls = {
}; };
let update = true; let update = true;
if (is.string(this.config.controls) || is.element(this.config.controls)) { // If function, run it and use output
// String or HTMLElement passed as the option if (is.function(this.config.controls)) {
this.config.controls = this.config.controls.call(this.props);
}
// Convert falsy controls to empty array (primarily for empty strings)
if (!this.config.controls) {
this.config.controls = [];
}
if (is.element(this.config.controls) || is.string(this.config.controls)) {
// HTMLElement or Non-empty string passed as the option
container = this.config.controls; container = this.config.controls;
} else if (is.function(this.config.controls)) {
// A custom function to build controls
// The function can return a HTMLElement or String
container = this.config.controls.call(this, props);
} else { } else {
// Create controls // Create controls
container = controls.create.call(this, { container = controls.create.call(this, {
+336 -258
View File
@@ -4,8 +4,9 @@
import controls from './controls'; import controls from './controls';
import ui from './ui'; import ui from './ui';
import { repaint } from './utils/animation';
import browser from './utils/browser'; import browser from './utils/browser';
import { getElement, getElements, getFocusElement, matches, toggleClass, toggleHidden } from './utils/elements'; import { getElement, getElements, hasClass, matches, toggleClass, toggleHidden } from './utils/elements';
import { on, once, toggleListener, triggerEvent } from './utils/events'; import { on, once, toggleListener, triggerEvent } from './utils/events';
import is from './utils/is'; import is from './utils/is';
@@ -13,14 +14,19 @@ class Listeners {
constructor(player) { constructor(player) {
this.player = player; this.player = player;
this.lastKey = null; this.lastKey = null;
this.focusTimer = null;
this.lastKeyDown = null;
this.handleKey = this.handleKey.bind(this); this.handleKey = this.handleKey.bind(this);
this.toggleMenu = this.toggleMenu.bind(this); this.toggleMenu = this.toggleMenu.bind(this);
this.setTabFocus = this.setTabFocus.bind(this);
this.firstTouch = this.firstTouch.bind(this); this.firstTouch = this.firstTouch.bind(this);
} }
// Handle key presses // Handle key presses
handleKey(event) { handleKey(event) {
const { player } = this;
const { elements } = player;
const code = event.keyCode ? event.keyCode : event.which; const code = event.keyCode ? event.keyCode : event.which;
const pressed = event.type === 'keydown'; const pressed = event.type === 'keydown';
const repeat = pressed && code === this.lastKey; const repeat = pressed && code === this.lastKey;
@@ -39,27 +45,32 @@ class Listeners {
// Seek by the number keys // Seek by the number keys
const seekByKey = () => { const seekByKey = () => {
// Divide the max duration into 10th's and times by the number value // Divide the max duration into 10th's and times by the number value
this.player.currentTime = this.player.duration / 10 * (code - 48); player.currentTime = player.duration / 10 * (code - 48);
}; };
// Handle the key on keydown // Handle the key on keydown
// Reset on keyup // Reset on keyup
if (pressed) { if (pressed) {
// Which keycodes should we prevent default
const preventDefault = [32, 37, 38, 39, 40, 48, 49, 50, 51, 52, 53, 54, 56, 57, 67, 70, 73, 75, 76, 77, 79];
// Check focused element // Check focused element
// and if the focused element is not editable (e.g. text input) // and if the focused element is not editable (e.g. text input)
// and any that accept key input http://webaim.org/techniques/keyboard/ // and any that accept key input http://webaim.org/techniques/keyboard/
const focused = getFocusElement(); const focused = document.activeElement;
if ( if (is.element(focused)) {
is.element(focused) && const { editable } = player.config.selectors;
(focused !== this.player.elements.inputs.seek && const { seek } = elements.inputs;
matches(focused, this.player.config.selectors.editable))
) { if (focused !== seek && matches(focused, editable)) {
return; return;
}
if (event.which === 32 && matches(focused, 'button, [role^="menuitem"]')) {
return;
}
} }
// Which keycodes should we prevent default
const preventDefault = [32, 37, 38, 39, 40, 48, 49, 50, 51, 52, 53, 54, 56, 57, 67, 70, 73, 75, 76, 77, 79];
// If the code is found prevent default (e.g. prevent scrolling for arrows) // If the code is found prevent default (e.g. prevent scrolling for arrows)
if (preventDefault.includes(code)) { if (preventDefault.includes(code)) {
event.preventDefault(); event.preventDefault();
@@ -87,52 +98,52 @@ class Listeners {
case 75: case 75:
// Space and K key // Space and K key
if (!repeat) { if (!repeat) {
this.player.togglePlay(); player.togglePlay();
} }
break; break;
case 38: case 38:
// Arrow up // Arrow up
this.player.increaseVolume(0.1); player.increaseVolume(0.1);
break; break;
case 40: case 40:
// Arrow down // Arrow down
this.player.decreaseVolume(0.1); player.decreaseVolume(0.1);
break; break;
case 77: case 77:
// M key // M key
if (!repeat) { if (!repeat) {
this.player.muted = !this.player.muted; player.muted = !player.muted;
} }
break; break;
case 39: case 39:
// Arrow forward // Arrow forward
this.player.forward(); player.forward();
break; break;
case 37: case 37:
// Arrow back // Arrow back
this.player.rewind(); player.rewind();
break; break;
case 70: case 70:
// F key // F key
this.player.fullscreen.toggle(); player.fullscreen.toggle();
break; break;
case 67: case 67:
// C key // C key
if (!repeat) { if (!repeat) {
this.player.toggleCaptions(); player.toggleCaptions();
} }
break; break;
case 76: case 76:
// L key // L key
this.player.loop = !this.player.loop; player.loop = !player.loop;
break; break;
/* case 73: /* case 73:
@@ -153,8 +164,8 @@ class Listeners {
// Escape is handle natively when in full screen // Escape is handle natively when in full screen
// So we only need to worry about non native // So we only need to worry about non native
if (!this.player.fullscreen.enabled && this.player.fullscreen.active && code === 27) { if (!player.fullscreen.enabled && player.fullscreen.active && code === 27) {
this.player.fullscreen.toggle(); player.fullscreen.toggle();
} }
// Store last code for next cycle // Store last code for next cycle
@@ -171,58 +182,99 @@ class Listeners {
// Device is touch enabled // Device is touch enabled
firstTouch() { firstTouch() {
this.player.touch = true; const { player } = this;
const { elements } = player;
player.touch = true;
// Add touch class // Add touch class
toggleClass(this.player.elements.container, this.player.config.classNames.isTouch, true); toggleClass(elements.container, player.config.classNames.isTouch, true);
}
setTabFocus(event) {
const { player } = this;
const { elements } = player;
clearTimeout(this.focusTimer);
// Ignore any key other than tab
if (event.type === 'keydown' && event.which !== 9) {
return;
}
// Store reference to event timeStamp
if (event.type === 'keydown') {
this.lastKeyDown = event.timeStamp;
}
// Remove current classes
const removeCurrent = () => {
const className = player.config.classNames.tabFocus;
const current = getElements.call(player, `.${className}`);
toggleClass(current, className, false);
};
// Determine if a key was pressed to trigger this event
const wasKeyDown = event.timeStamp - this.lastKeyDown <= 20;
// Ignore focus events if a key was pressed prior
if (event.type === 'focus' && !wasKeyDown) {
return;
}
// Remove all current
removeCurrent();
// Delay the adding of classname until the focus has changed
// This event fires before the focusin event
this.focusTimer = setTimeout(() => {
const focused = document.activeElement;
// Ignore if current focus element isn't inside the player
if (!elements.container.contains(focused)) {
return;
}
toggleClass(document.activeElement, player.config.classNames.tabFocus, true);
}, 10);
} }
// Global window & document listeners // Global window & document listeners
global(toggle = true) { global(toggle = true) {
const { player } = this;
// Keyboard shortcuts // Keyboard shortcuts
if (this.player.config.keyboard.global) { if (player.config.keyboard.global) {
toggleListener.call(this.player, window, 'keydown keyup', this.handleKey, toggle, false); toggleListener.call(player, window, 'keydown keyup', this.handleKey, toggle, false);
} }
// Click anywhere closes menu // Click anywhere closes menu
toggleListener.call(this.player, document.body, 'click', this.toggleMenu, toggle); toggleListener.call(player, document.body, 'click', this.toggleMenu, toggle);
// Detect touch by events // Detect touch by events
once.call(this.player, document.body, 'touchstart', this.firstTouch); once.call(player, document.body, 'touchstart', this.firstTouch);
// Tab focus detection
toggleListener.call(player, document.body, 'keydown focus blur', this.setTabFocus, toggle, false, true);
} }
// Container listeners // Container listeners
container() { container() {
const { player } = this;
const { elements } = player;
// Keyboard shortcuts // Keyboard shortcuts
if (!this.player.config.keyboard.global && this.player.config.keyboard.focused) { if (!player.config.keyboard.global && player.config.keyboard.focused) {
on.call(this.player, this.player.elements.container, 'keydown keyup', this.handleKey, false); on.call(player, elements.container, 'keydown keyup', this.handleKey, false);
} }
// Detect tab focus
// Remove class on blur/focusout
on.call(this.player, this.player.elements.container, 'focusout', event => {
toggleClass(event.target, this.player.config.classNames.tabFocus, false);
});
// Add classname to tabbed elements
on.call(this.player, this.player.elements.container, 'keydown', event => {
if (event.keyCode !== 9) {
return;
}
// Delay the adding of classname until the focus has changed
// This event fires before the focusin event
setTimeout(() => {
toggleClass(getFocusElement(), this.player.config.classNames.tabFocus, true);
}, 0);
});
// Toggle controls on mouse events and entering fullscreen // Toggle controls on mouse events and entering fullscreen
on.call( on.call(
this.player, player,
this.player.elements.container, elements.container,
'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen',
event => { event => {
const { controls } = this.player.elements; const { controls } = elements;
// Remove button states for fullscreen // Remove button states for fullscreen
if (event.type === 'enterfullscreen') { if (event.type === 'enterfullscreen') {
@@ -236,85 +288,83 @@ class Listeners {
let delay = 0; let delay = 0;
if (show) { if (show) {
ui.toggleControls.call(this.player, true); ui.toggleControls.call(player, true);
// Use longer timeout for touch devices // Use longer timeout for touch devices
delay = this.player.touch ? 3000 : 2000; delay = player.touch ? 3000 : 2000;
} }
// Clear timer // Clear timer
clearTimeout(this.player.timers.controls); clearTimeout(player.timers.controls);
// Timer to prevent flicker when seeking
this.player.timers.controls = setTimeout(() => ui.toggleControls.call(this.player, false), delay); // Set new timer to prevent flicker when seeking
player.timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);
}, },
); );
} }
// Listen for media events // Listen for media events
media() { media() {
const { player } = this;
const { elements } = player;
// Time change on media // Time change on media
on.call(this.player, this.player.media, 'timeupdate seeking seeked', event => on.call(player, player.media, 'timeupdate seeking seeked', event => controls.timeUpdate.call(player, event));
controls.timeUpdate.call(this.player, event),
);
// Display duration // Display duration
on.call(this.player, this.player.media, 'durationchange loadeddata loadedmetadata', event => on.call(player, player.media, 'durationchange loadeddata loadedmetadata', event =>
controls.durationUpdate.call(this.player, event), controls.durationUpdate.call(player, event),
); );
// Check for audio tracks on load // Check for audio tracks on load
// We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point // We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point
on.call(this.player, this.player.media, 'canplay', () => { on.call(player, player.media, 'canplay', () => {
toggleHidden(this.player.elements.volume, !this.player.hasAudio); toggleHidden(elements.volume, !player.hasAudio);
toggleHidden(this.player.elements.buttons.mute, !this.player.hasAudio); toggleHidden(elements.buttons.mute, !player.hasAudio);
}); });
// Handle the media finishing // Handle the media finishing
on.call(this.player, this.player.media, 'ended', () => { on.call(player, player.media, 'ended', () => {
// Show poster on end // Show poster on end
if (this.player.isHTML5 && this.player.isVideo && this.player.config.resetOnEnd) { if (player.isHTML5 && player.isVideo && player.config.resetOnEnd) {
// Restart // Restart
this.player.restart(); player.restart();
} }
}); });
// Check for buffer progress // Check for buffer progress
on.call(this.player, this.player.media, 'progress playing seeking seeked', event => on.call(player, player.media, 'progress playing seeking seeked', event =>
controls.updateProgress.call(this.player, event), controls.updateProgress.call(player, event),
); );
// Handle volume changes // Handle volume changes
on.call(this.player, this.player.media, 'volumechange', event => on.call(player, player.media, 'volumechange', event => controls.updateVolume.call(player, event));
controls.updateVolume.call(this.player, event),
);
// Handle play/pause // Handle play/pause
on.call(this.player, this.player.media, 'playing play pause ended emptied timeupdate', event => on.call(player, player.media, 'playing play pause ended emptied timeupdate', event =>
ui.checkPlaying.call(this.player, event), ui.checkPlaying.call(player, event),
); );
// Loading state // Loading state
on.call(this.player, this.player.media, 'waiting canplay seeked playing', event => on.call(player, player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(player, event));
ui.checkLoading.call(this.player, event),
);
// If autoplay, then load advertisement if required // If autoplay, then load advertisement if required
// TODO: Show some sort of loading state while the ad manager loads else there's a delay before ad shows // TODO: Show some sort of loading state while the ad manager loads else there's a delay before ad shows
on.call(this.player, this.player.media, 'playing', () => { on.call(player, player.media, 'playing', () => {
if (!this.player.ads) { if (!player.ads) {
return; return;
} }
// If ads are enabled, wait for them first // If ads are enabled, wait for them first
if (this.player.ads.enabled && !this.player.ads.initialized) { if (player.ads.enabled && !player.ads.initialized) {
// Wait for manager response // Wait for manager response
this.player.ads.managerPromise.then(() => this.player.ads.play()).catch(() => this.player.play()); player.ads.managerPromise.then(() => player.ads.play()).catch(() => player.play());
} }
}); });
// Click video // Click video
if (this.player.supported.ui && this.player.config.clickToPlay && !this.player.isAudio) { if (player.supported.ui && player.config.clickToPlay && !player.isAudio) {
// Re-fetch the wrapper // Re-fetch the wrapper
const wrapper = getElement.call(this.player, `.${this.player.config.classNames.video}`); const wrapper = getElement.call(player, `.${player.config.classNames.video}`);
// Bail if there's no wrapper (this should never happen) // Bail if there's no wrapper (this should never happen)
if (!is.element(wrapper)) { if (!is.element(wrapper)) {
@@ -322,28 +372,38 @@ class Listeners {
} }
// On click play, pause ore restart // On click play, pause ore restart
on.call(this.player, wrapper, 'click', () => { on.call(player, elements.container, 'click touchstart', event => {
// Touch devices will just show controls (if we're hiding controls) const targets = [elements.container, wrapper];
if (this.player.config.hideControls && this.player.touch && !this.player.paused) {
// Ignore if click if not container or in video wrapper
if (!targets.includes(event.target) && !wrapper.contains(event.target)) {
return; return;
} }
if (this.player.paused) { // First touch on touch devices will just show controls (if we're hiding controls)
this.player.play(); // If controls are shown then it'll toggle like a pointer device
} else if (this.player.ended) { if (
this.player.restart(); player.config.hideControls &&
this.player.play(); player.touch &&
hasClass(elements.container, player.config.classNames.hideControls)
) {
return;
}
if (player.ended) {
player.restart();
player.play();
} else { } else {
this.player.pause(); player.togglePlay();
} }
}); });
} }
// Disable right click // Disable right click
if (this.player.supported.ui && this.player.config.disableContextMenu) { if (player.supported.ui && player.config.disableContextMenu) {
on.call( on.call(
this.player, player,
this.player.elements.wrapper, elements.wrapper,
'contextmenu', 'contextmenu',
event => { event => {
event.preventDefault(); event.preventDefault();
@@ -353,220 +413,236 @@ class Listeners {
} }
// Volume change // Volume change
on.call(this.player, this.player.media, 'volumechange', () => { on.call(player, player.media, 'volumechange', () => {
// Save to storage // Save to storage
this.player.storage.set({ volume: this.player.volume, muted: this.player.muted }); player.storage.set({
volume: player.volume,
muted: player.muted,
});
}); });
// Speed change // Speed change
on.call(this.player, this.player.media, 'ratechange', () => { on.call(player, player.media, 'ratechange', () => {
// Update UI // Update UI
controls.updateSetting.call(this.player, 'speed'); controls.updateSetting.call(player, 'speed');
// Save to storage // Save to storage
this.player.storage.set({ speed: this.player.speed }); player.storage.set({ speed: player.speed });
}); });
// Quality request // Quality request
on.call(this.player, this.player.media, 'qualityrequested', event => { on.call(player, player.media, 'qualityrequested', event => {
// Save to storage // Save to storage
this.player.storage.set({ quality: event.detail.quality }); player.storage.set({ quality: event.detail.quality });
}); });
// Quality change // Quality change
on.call(this.player, this.player.media, 'qualitychange', event => { on.call(player, player.media, 'qualitychange', event => {
// Update UI // Update UI
controls.updateSetting.call(this.player, 'quality', null, event.detail.quality); controls.updateSetting.call(player, 'quality', null, event.detail.quality);
}); });
// Proxy events to container // Proxy events to container
// Bubble up key events for Edge // Bubble up key events for Edge
const proxyEvents = this.player.config.events.concat(['keyup', 'keydown']).join(' '); const proxyEvents = player.config.events.concat(['keyup', 'keydown']).join(' ');
on.call(this.player, this.player.media, proxyEvents, event => {
on.call(player, player.media, proxyEvents, event => {
let { detail = {} } = event; let { detail = {} } = event;
// Get error details from media // Get error details from media
if (event.type === 'error') { if (event.type === 'error') {
detail = this.player.media.error; detail = player.media.error;
} }
triggerEvent.call(this.player, this.player.elements.container, event.type, true, detail); triggerEvent.call(player, elements.container, event.type, true, detail);
}); });
} }
// Run default and custom handlers
proxy(event, defaultHandler, customHandlerKey) {
const { player } = this;
const customHandler = player.config.listeners[customHandlerKey];
const hasCustomHandler = is.function(customHandler);
let returned = true;
// Execute custom handler
if (hasCustomHandler) {
returned = customHandler.call(player, event);
}
// Only call default handler if not prevented in custom handler
if (returned && is.function(defaultHandler)) {
defaultHandler.call(player, event);
}
}
// Trigger custom and default handlers
bind(element, type, defaultHandler, customHandlerKey, passive = true) {
const { player } = this;
const customHandler = player.config.listeners[customHandlerKey];
const hasCustomHandler = is.function(customHandler);
on.call(
player,
element,
type,
event => this.proxy(event, defaultHandler, customHandlerKey),
passive && !hasCustomHandler,
);
}
// Listen for control events // Listen for control events
controls() { controls() {
const { player } = this;
const { elements } = player;
// IE doesn't support input event, so we fallback to change // IE doesn't support input event, so we fallback to change
const inputEvent = browser.isIE ? 'change' : 'input'; const inputEvent = browser.isIE ? 'change' : 'input';
// Run default and custom handlers
const proxy = (event, defaultHandler, customHandlerKey) => {
const customHandler = this.player.config.listeners[customHandlerKey];
const hasCustomHandler = is.function(customHandler);
let returned = true;
// Execute custom handler
if (hasCustomHandler) {
returned = customHandler.call(this.player, event);
}
// Only call default handler if not prevented in custom handler
if (returned && is.function(defaultHandler)) {
defaultHandler.call(this.player, event);
}
};
// Trigger custom and default handlers
const bind = (element, type, defaultHandler, customHandlerKey, passive = true) => {
const customHandler = this.player.config.listeners[customHandlerKey];
const hasCustomHandler = is.function(customHandler);
on.call(
this.player,
element,
type,
event => proxy(event, defaultHandler, customHandlerKey),
passive && !hasCustomHandler,
);
};
// Play/pause toggle // Play/pause toggle
if (this.player.elements.buttons.play) { if (elements.buttons.play) {
Array.from(this.player.elements.buttons.play).forEach(button => { Array.from(elements.buttons.play).forEach(button => {
bind(button, 'click', this.player.togglePlay, 'play'); this.bind(button, 'click', player.togglePlay, 'play');
}); });
} }
// Pause // Pause
bind(this.player.elements.buttons.restart, 'click', this.player.restart, 'restart'); this.bind(elements.buttons.restart, 'click', player.restart, 'restart');
// Rewind // Rewind
bind(this.player.elements.buttons.rewind, 'click', this.player.rewind, 'rewind'); this.bind(elements.buttons.rewind, 'click', player.rewind, 'rewind');
// Rewind // Rewind
bind(this.player.elements.buttons.fastForward, 'click', this.player.forward, 'fastForward'); this.bind(elements.buttons.fastForward, 'click', player.forward, 'fastForward');
// Mute toggle // Mute toggle
bind( this.bind(
this.player.elements.buttons.mute, elements.buttons.mute,
'click', 'click',
() => { () => {
this.player.muted = !this.player.muted; player.muted = !player.muted;
}, },
'mute', 'mute',
); );
// Captions toggle // Captions toggle
bind(this.player.elements.buttons.captions, 'click', () => this.player.toggleCaptions()); this.bind(elements.buttons.captions, 'click', () => player.toggleCaptions());
// Fullscreen toggle // Fullscreen toggle
bind( this.bind(
this.player.elements.buttons.fullscreen, elements.buttons.fullscreen,
'click', 'click',
() => { () => {
this.player.fullscreen.toggle(); player.fullscreen.toggle();
}, },
'fullscreen', 'fullscreen',
); );
// Picture-in-Picture // Picture-in-Picture
bind( this.bind(
this.player.elements.buttons.pip, elements.buttons.pip,
'click', 'click',
() => { () => {
this.player.pip = 'toggle'; player.pip = 'toggle';
}, },
'pip', 'pip',
); );
// Airplay // Airplay
bind(this.player.elements.buttons.airplay, 'click', this.player.airplay, 'airplay'); this.bind(elements.buttons.airplay, 'click', player.airplay, 'airplay');
// Settings menu // Settings menu - click toggle
bind(this.player.elements.buttons.settings, 'click', event => { this.bind(elements.buttons.settings, 'click', event => {
controls.toggleMenu.call(this.player, event); // Prevent the document click listener closing the menu
});
// Settings menu
bind(this.player.elements.settings.form, 'click', event => {
event.stopPropagation(); event.stopPropagation();
// Go back to home tab on click controls.toggleMenu.call(player, event);
const showHomeTab = () => { });
const id = `plyr-settings-${this.player.id}-home`;
controls.showTab.call(this.player, id);
};
// Settings menu items - use event delegation as items are added/removed // Settings menu - keyboard toggle
if (matches(event.target, this.player.config.selectors.inputs.language)) { // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus
proxy( // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143
event, this.bind(
() => { elements.buttons.settings,
this.player.currentTrack = Number(event.target.value); 'keyup',
showHomeTab(); event => {
}, const code = event.which;
'language',
); // We only care about space and return
} else if (matches(event.target, this.player.config.selectors.inputs.quality)) { if (![13, 32].includes(code)) {
proxy( return;
event, }
() => {
this.player.quality = event.target.value; // Because return triggers a click anyway, all we need to do is set focus
showHomeTab(); if (code === 13) {
}, controls.focusFirstMenuItem.call(player, null, true);
'quality', return;
); }
} else if (matches(event.target, this.player.config.selectors.inputs.speed)) {
proxy( // Prevent scroll
event, event.preventDefault();
() => {
this.player.speed = parseFloat(event.target.value); // Prevent playing video (Firefox)
showHomeTab(); event.stopPropagation();
},
'speed', // Toggle menu
); controls.toggleMenu.call(player, event);
} else { },
const tab = event.target; null,
controls.showTab.call(this.player, tab.getAttribute('aria-controls')); false, // Can't be passive as we're preventing default
);
// Escape closes menu
this.bind(elements.settings.menu, 'keydown', event => {
if (event.which === 27) {
controls.toggleMenu.call(player, event);
} }
}); });
// Set range input alternative "value", which matches the tooltip time (#954) // Set range input alternative "value", which matches the tooltip time (#954)
bind(this.player.elements.inputs.seek, 'mousedown mousemove', event => { this.bind(elements.inputs.seek, 'mousedown mousemove', event => {
const clientRect = this.player.elements.progress.getBoundingClientRect(); const rect = elements.progress.getBoundingClientRect();
const percent = 100 / clientRect.width * (event.pageX - clientRect.left); const percent = 100 / rect.width * (event.pageX - rect.left);
event.currentTarget.setAttribute('seek-value', percent); event.currentTarget.setAttribute('seek-value', percent);
}); });
// Pause while seeking // Pause while seeking
bind(this.player.elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => { this.bind(elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => {
const seek = event.currentTarget; const seek = event.currentTarget;
const code = event.keyCode ? event.keyCode : event.which; const code = event.keyCode ? event.keyCode : event.which;
const eventType = event.type; const attribute = 'play-on-seeked';
if ((eventType === 'keydown' || eventType === 'keyup') && (code !== 39 && code !== 37)) { if (is.keyboardEvent(event) && (code !== 39 && code !== 37)) {
return; return;
} }
// Was playing before? // Was playing before?
const play = seek.hasAttribute('play-on-seeked'); const play = seek.hasAttribute(attribute);
// Done seeking // Done seeking
const done = ['mouseup', 'touchend', 'keyup'].includes(event.type); const done = ['mouseup', 'touchend', 'keyup'].includes(event.type);
// If we're done seeking and it was playing, resume playback // If we're done seeking and it was playing, resume playback
if (play && done) { if (play && done) {
seek.removeAttribute('play-on-seeked'); seek.removeAttribute(attribute);
this.player.play(); player.play();
} else if (!done && this.player.playing) { } else if (!done && player.playing) {
seek.setAttribute('play-on-seeked', ''); seek.setAttribute(attribute, '');
this.player.pause(); player.pause();
} }
}); });
// Fix range inputs on iOS
// Super weird iOS bug where after you interact with an <input type="range">,
// it takes over further interactions on the page. This is a hack
if (browser.isIos) {
const inputs = getElements.call(player, 'input[type="range"]');
Array.from(inputs).forEach(input => this.bind(input, inputEvent, event => repaint(event.target)));
}
// Seek // Seek
bind( this.bind(
this.player.elements.inputs.seek, elements.inputs.seek,
inputEvent, inputEvent,
event => { event => {
const seek = event.currentTarget; const seek = event.currentTarget;
@@ -580,70 +656,71 @@ class Listeners {
seek.removeAttribute('seek-value'); seek.removeAttribute('seek-value');
this.player.currentTime = seekTo / seek.max * this.player.duration; player.currentTime = seekTo / seek.max * player.duration;
}, },
'seek', 'seek',
); );
// Current time invert // Seek tooltip
// Only if one time element is used for both currentTime and duration this.bind(elements.progress, 'mouseenter mouseleave mousemove', event =>
if (this.player.config.toggleInvert && !is.element(this.player.elements.display.duration)) { controls.updateSeekTooltip.call(player, event),
bind(this.player.elements.display.currentTime, 'click', () => {
// Do nothing if we're at the start
if (this.player.currentTime === 0) {
return;
}
this.player.config.invertTime = !this.player.config.invertTime;
controls.timeUpdate.call(this.player);
});
}
// Volume
bind(
this.player.elements.inputs.volume,
inputEvent,
event => {
this.player.volume = event.target.value;
},
'volume',
); );
// Polyfill for lower fill in <input type="range"> for webkit // Polyfill for lower fill in <input type="range"> for webkit
if (browser.isWebkit) { if (browser.isWebkit) {
Array.from(getElements.call(this.player, 'input[type="range"]')).forEach(element => { Array.from(getElements.call(player, 'input[type="range"]')).forEach(element => {
bind(element, 'input', event => controls.updateRangeFill.call(this.player, event.target)); this.bind(element, 'input', event => controls.updateRangeFill.call(player, event.target));
}); });
} }
// Seek tooltip // Current time invert
bind(this.player.elements.progress, 'mouseenter mouseleave mousemove', event => // Only if one time element is used for both currentTime and duration
controls.updateSeekTooltip.call(this.player, event), if (player.config.toggleInvert && !is.element(elements.display.duration)) {
this.bind(elements.display.currentTime, 'click', () => {
// Do nothing if we're at the start
if (player.currentTime === 0) {
return;
}
player.config.invertTime = !player.config.invertTime;
controls.timeUpdate.call(player);
});
}
// Volume
this.bind(
elements.inputs.volume,
inputEvent,
event => {
player.volume = event.target.value;
},
'volume',
); );
// Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting) // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)
bind(this.player.elements.controls, 'mouseenter mouseleave', event => { this.bind(elements.controls, 'mouseenter mouseleave', event => {
this.player.elements.controls.hover = !this.player.touch && event.type === 'mouseenter'; elements.controls.hover = !player.touch && event.type === 'mouseenter';
}); });
// Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting) // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)
bind(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => { this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {
this.player.elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type); elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);
}); });
// Focus in/out on controls // Focus in/out on controls
bind(this.player.elements.controls, 'focusin focusout', event => { this.bind(elements.controls, 'focusin focusout', event => {
const { config, elements, timers } = this.player; const { config, elements, timers } = player;
const isFocusIn = event.type === 'focusin';
// Skip transition to prevent focus from scrolling the parent element // Skip transition to prevent focus from scrolling the parent element
toggleClass(elements.controls, config.classNames.noTransition, event.type === 'focusin'); toggleClass(elements.controls, config.classNames.noTransition, isFocusIn);
// Toggle // Toggle
ui.toggleControls.call(this.player, event.type === 'focusin'); ui.toggleControls.call(player, isFocusIn);
// If focusin, hide again after delay // If focusin, hide again after delay
if (event.type === 'focusin') { if (isFocusIn) {
// Restore transition // Restore transition
setTimeout(() => { setTimeout(() => {
toggleClass(elements.controls, config.classNames.noTransition, false); toggleClass(elements.controls, config.classNames.noTransition, false);
@@ -654,14 +731,15 @@ class Listeners {
// Clear timer // Clear timer
clearTimeout(timers.controls); clearTimeout(timers.controls);
// Hide // Hide
timers.controls = setTimeout(() => ui.toggleControls.call(this.player, false), delay); timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);
} }
}); });
// Mouse wheel for volume // Mouse wheel for volume
bind( this.bind(
this.player.elements.inputs.volume, elements.inputs.volume,
'wheel', 'wheel',
event => { event => {
// Detect "natural" scroll - suppored on OS X Safari only // Detect "natural" scroll - suppored on OS X Safari only
@@ -675,10 +753,10 @@ class Listeners {
const direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y); const direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y);
// Change the volume by 2% // Change the volume by 2%
this.player.increaseVolume(direction / 50); player.increaseVolume(direction / 50);
// Don't break page scrolling at max and min // Don't break page scrolling at max and min
const { volume } = this.player.media; const { volume } = player.media;
if ((direction === 1 && volume < 1) || (direction === -1 && volume > 0)) { if ((direction === 1 && volume < 1) || (direction === -1 && volume > 0)) {
event.preventDefault(); event.preventDefault();
} }
+5 -4
View File
@@ -207,6 +207,11 @@ class Ads {
* @param {Event} adsManagerLoadedEvent * @param {Event} adsManagerLoadedEvent
*/ */
onAdsManagerLoaded(event) { onAdsManagerLoaded(event) {
// Load could occur after a source change (race condition)
if (!this.enabled) {
return;
}
// Get the ads manager // Get the ads manager
const settings = new google.ima.AdsRenderingSettings(); const settings = new google.ima.AdsRenderingSettings();
@@ -240,10 +245,6 @@ class Ads {
}); });
} }
// Get skippable state
// TODO: Skip button
// this.player.debug.warn(this.manager.getAdSkippableState());
// Set volume to match player // Set volume to match player
this.manager.setVolume(this.player.volume); this.manager.setVolume(this.player.volume);
+16 -8
View File
@@ -1,6 +1,6 @@
// ========================================================================== // ==========================================================================
// Plyr // Plyr
// plyr.js v3.3.23 // plyr.js v3.4.0-beta.2
// https://github.com/sampotts/plyr // https://github.com/sampotts/plyr
// License: The MIT License (MIT) // License: The MIT License (MIT)
// ========================================================================== // ==========================================================================
@@ -75,16 +75,17 @@ class Plyr {
// Elements cache // Elements cache
this.elements = { this.elements = {
container: null, container: null,
captions: null,
buttons: {}, buttons: {},
display: {}, display: {},
progress: {}, progress: {},
inputs: {}, inputs: {},
settings: { settings: {
popup: null,
menu: null, menu: null,
panes: {}, panels: {},
tabs: {}, buttons: {},
}, },
captions: null,
}; };
// Captions // Captions
@@ -185,7 +186,7 @@ class Plyr {
// YouTube requires the playsinline in the URL // YouTube requires the playsinline in the URL
if (this.isYouTube) { if (this.isYouTube) {
this.config.playsinline = truthy.includes(url.searchParams.get('playsinline')); this.config.playsinline = truthy.includes(url.searchParams.get('playsinline'));
this.config.hl = url.searchParams.get('hl'); this.config.hl = url.searchParams.get('hl'); // TODO: Should this be setting language?
} else { } else {
this.config.playsinline = true; this.config.playsinline = true;
} }
@@ -221,7 +222,7 @@ class Plyr {
if (this.media.hasAttribute('autoplay')) { if (this.media.hasAttribute('autoplay')) {
this.config.autoplay = true; this.config.autoplay = true;
} }
if (this.media.hasAttribute('playsinline')) { if (this.media.hasAttribute('playsinline') || this.media.hasAttribute('webkit-playsinline')) {
this.config.playsinline = true; this.config.playsinline = true;
} }
if (this.media.hasAttribute('muted')) { if (this.media.hasAttribute('muted')) {
@@ -293,7 +294,9 @@ class Plyr {
this.fullscreen = new Fullscreen(this); this.fullscreen = new Fullscreen(this);
// Setup ads if provided // Setup ads if provided
this.ads = new Ads(this); if (this.config.ads.enabled) {
this.ads = new Ads(this);
}
// Autoplay if required // Autoplay if required
if (this.config.autoplay) { if (this.config.autoplay) {
@@ -696,7 +699,9 @@ class Plyr {
} }
// Trigger request event // Trigger request event
triggerEvent.call(this, this.media, 'qualityrequested', false, { quality }); triggerEvent.call(this, this.media, 'qualityrequested', false, {
quality,
});
// Update config // Update config
config.selected = quality; config.selected = quality;
@@ -933,13 +938,16 @@ class Plyr {
if (hiding && this.config.controls.includes('settings') && !is.empty(this.config.settings)) { if (hiding && this.config.controls.includes('settings') && !is.empty(this.config.settings)) {
controls.toggleMenu.call(this, false); controls.toggleMenu.call(this, false);
} }
// Trigger event on change // Trigger event on change
if (hiding !== isHidden) { if (hiding !== isHidden) {
const eventName = hiding ? 'controlshidden' : 'controlsshown'; const eventName = hiding ? 'controlshidden' : 'controlsshown';
triggerEvent.call(this, this.media, eventName); triggerEvent.call(this, this.media, eventName);
} }
return !hiding; return !hiding;
} }
return false; return false;
} }
+1 -1
View File
@@ -1,6 +1,6 @@
// ========================================================================== // ==========================================================================
// Plyr Polyfilled Build // Plyr Polyfilled Build
// plyr.js v3.3.23 // plyr.js v3.4.0-beta.2
// https://github.com/sampotts/plyr // https://github.com/sampotts/plyr
// License: The MIT License (MIT) // License: The MIT License (MIT)
// ========================================================================== // ==========================================================================
+10 -4
View File
@@ -15,7 +15,9 @@ export const transitionEndEvent = (() => {
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;
})(); })();
@@ -23,8 +25,12 @@ export const transitionEndEvent = (() => {
// Force repaint of element // Force repaint of element
export function repaint(element) { export function repaint(element) {
setTimeout(() => { setTimeout(() => {
toggleHidden(element, true); try {
element.offsetHeight; // eslint-disable-line toggleHidden(element, true);
toggleHidden(element, false); element.offsetHeight; // eslint-disable-line
toggleHidden(element, false);
} catch (e) {
// Do nothing
}
}, 0); }, 0);
} }
+34 -17
View File
@@ -70,12 +70,19 @@ export function createElement(type, attributes, text) {
// 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)) {
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) {
// Inject the new <element> if (!is.element(parent)) {
return;
}
parent.appendChild(createElement(type, attributes, text)); parent.appendChild(createElement(type, attributes, text));
} }
@@ -95,6 +102,10 @@ export function removeElement(element) {
// Remove all child elements // Remove all child elements
export function emptyElement(element) { export function emptyElement(element) {
if (!is.element(element)) {
return;
}
let { length } = element.childNodes; let { length } = element.childNodes;
while (length > 0) { while (length > 0) {
@@ -180,7 +191,7 @@ export function toggleHidden(element, hidden) {
let hide = hidden; let hide = hidden;
if (!is.boolean(hide)) { if (!is.boolean(hide)) {
hide = !element.hasAttribute('hidden'); hide = !element.hidden;
} }
if (hide) { if (hide) {
@@ -192,6 +203,10 @@ export function toggleHidden(element, hidden) {
// 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)) {
return Array.from(element).map(e => toggleClass(e, className, force));
}
if (is.element(element)) { if (is.element(element)) {
let method = 'toggle'; let method = 'toggle';
if (typeof force !== 'undefined') { if (typeof force !== 'undefined') {
@@ -202,7 +217,7 @@ export function toggleClass(element, className, force) {
return element.classList.contains(className); return element.classList.contains(className);
} }
return null; return false;
} }
// Has class name // Has class name
@@ -238,19 +253,6 @@ export function getElement(selector) {
return this.elements.container.querySelector(selector); return this.elements.container.querySelector(selector);
} }
// Get the focused element
export function getFocusElement() {
let focused = document.activeElement;
if (!focused || focused === document.body) {
focused = null;
} else {
focused = document.querySelector(':focus');
}
return focused;
}
// Trap focus inside container // Trap focus inside container
export function trapFocus(element = null, toggle = false) { export function trapFocus(element = null, toggle = false) {
if (!is.element(element)) { if (!is.element(element)) {
@@ -268,7 +270,7 @@ export function trapFocus(element = null, toggle = false) {
} }
// Get the current focused element // Get the current focused element
const focused = getFocusElement(); const focused = document.activeElement;
if (focused === last && !event.shiftKey) { if (focused === last && !event.shiftKey) {
// Move focus to first element that can be tabbed if Shift isn't used // Move focus to first element that can be tabbed if Shift isn't used
@@ -283,3 +285,18 @@ export function trapFocus(element = null, toggle = false) {
toggleListener.call(this, this.elements.container, 'keydown', trap, toggle, false); toggleListener.call(this, this.elements.container, 'keydown', trap, toggle, false);
} }
// Set focus and tab focus class
export function setFocus(element = null, tabFocus = false) {
if (!is.element(element)) {
return;
}
// Set regular focus
element.focus();
// If we want to mimic keyboard focus via tab
if (tabFocus) {
toggleClass(element, this.config.classNames.tabFocus);
}
}
+2
View File
@@ -16,6 +16,7 @@ const isNodeList = input => instanceOf(input, NodeList);
const isElement = input => instanceOf(input, Element); const isElement = input => instanceOf(input, Element);
const isTextNode = input => getConstructor(input) === Text; const isTextNode = input => getConstructor(input) === Text;
const isEvent = input => instanceOf(input, Event); const isEvent = input => instanceOf(input, Event);
const isKeyboardEvent = input => instanceOf(input, KeyboardEvent);
const isCue = input => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue); const isCue = input => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue);
const isTrack = input => instanceOf(input, TextTrack) || (!isNullOrUndefined(input) && isString(input.kind)); const isTrack = input => instanceOf(input, TextTrack) || (!isNullOrUndefined(input) && isString(input.kind));
@@ -56,6 +57,7 @@ export default {
element: isElement, element: isElement,
textNode: isTextNode, textNode: isTextNode,
event: isEvent, event: isEvent,
keyboardEvent: isKeyboardEvent,
cue: isCue, cue: isCue,
track: isTrack, track: isTrack,
url: isUrl, url: isUrl,
+4 -3
View File
@@ -17,7 +17,6 @@
padding: $plyr-control-spacing; padding: $plyr-control-spacing;
position: absolute; position: absolute;
text-align: center; text-align: center;
transform: translateY(-($plyr-control-spacing * 4));
transition: transform 0.4s ease-in-out; transition: transform 0.4s ease-in-out;
width: 100%; width: 100%;
@@ -53,6 +52,8 @@
display: block; display: block;
} }
.plyr--hide-controls .plyr__captions { // If the lower controls are shown and not empty
transform: translateY(-($plyr-control-spacing * 1.5)); .plyr:not(.plyr--hide-controls) .plyr__controls:not(:empty) ~ .plyr__captions {
transform: translateY(-($plyr-control-spacing * 4));
} }
+16 -1
View File
@@ -41,7 +41,7 @@
display: none; display: none;
} }
// Audio styles // Audio control
.plyr--audio .plyr__control { .plyr--audio .plyr__control {
&.plyr__tab-focus, &.plyr__tab-focus,
&:hover, &:hover,
@@ -51,6 +51,21 @@
} }
} }
// Video control
.plyr--video .plyr__control {
svg {
filter: drop-shadow(0 1px 1px rgba(#000, 0.15));
}
// Hover and tab focus
&.plyr__tab-focus,
&:hover,
&[aria-expanded='true'] {
background: $plyr-video-control-bg-hover;
color: $plyr-video-control-color-hover;
}
}
// Large play button (video only) // Large play button (video only)
.plyr__control--overlaid { .plyr__control--overlaid {
background: rgba($plyr-video-control-bg-hover, 0.8); background: rgba($plyr-video-control-bg-hover, 0.8);
+15 -32
View File
@@ -32,6 +32,11 @@
margin-left: ($plyr-control-spacing / 2); margin-left: ($plyr-control-spacing / 2);
} }
// Hide empty controls
&:empty {
display: none;
}
@media (min-width: $plyr-bp-sm) { @media (min-width: $plyr-bp-sm) {
> .plyr__control, > .plyr__control,
.plyr__progress, .plyr__progress,
@@ -48,6 +53,14 @@
} }
} }
// Audio controls
.plyr--audio .plyr__controls {
background: $plyr-audio-controls-bg;
border-radius: inherit;
color: $plyr-audio-control-color;
padding: $plyr-control-spacing;
}
// Video controls // Video controls
.plyr--video .plyr__controls { .plyr--video .plyr__controls {
background: linear-gradient( background: linear-gradient(
@@ -64,32 +77,10 @@
position: absolute; position: absolute;
right: 0; right: 0;
transition: opacity 0.4s ease-in-out, transform 0.4s ease-in-out; transition: opacity 0.4s ease-in-out, transform 0.4s ease-in-out;
z-index: 2; z-index: 3;
.plyr__control {
svg {
filter: drop-shadow(0 1px 1px rgba(#000, 0.15));
}
// Hover and tab focus
&.plyr__tab-focus,
&:hover,
&[aria-expanded='true'] {
background: $plyr-video-control-bg-hover;
color: $plyr-video-control-color-hover;
}
}
} }
// Audio controls // Hide video controls
.plyr--audio .plyr__controls {
background: $plyr-audio-controls-bg;
border-radius: inherit;
color: $plyr-audio-control-color;
padding: $plyr-control-spacing;
}
// Hide controls
.plyr--video.plyr--hide-controls .plyr__controls { .plyr--video.plyr--hide-controls .plyr__controls {
opacity: 0; opacity: 0;
pointer-events: none; pointer-events: none;
@@ -109,11 +100,3 @@
.plyr--fullscreen-enabled [data-plyr='fullscreen'] { .plyr--fullscreen-enabled [data-plyr='fullscreen'] {
display: inline-block; display: inline-block;
} }
.plyr__controls:empty {
display: none;
~ .plyr__captions {
transform: translateY(0);
}
}
+44 -39
View File
@@ -39,7 +39,8 @@
> div { > div {
overflow: hidden; 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); transition: height 0.35s cubic-bezier(0.4, 0, 0.2, 1),
width 0.35s cubic-bezier(0.4, 0, 0.2, 1);
} }
// Arrow // Arrow
@@ -54,18 +55,16 @@
width: 0; width: 0;
} }
ul { [role='menu'] {
list-style: none;
margin: 0;
overflow: hidden;
padding: $plyr-control-padding; padding: $plyr-control-padding;
}
li { [role='menuitem'],
margin-top: 2px; [role='menuitemradio'] {
margin-top: 2px;
&:first-child { &:first-child {
margin-top: 0; margin-top: 0;
}
} }
} }
@@ -75,10 +74,17 @@
color: $plyr-menu-color; color: $plyr-menu-color;
display: flex; display: flex;
font-size: $plyr-font-size-menu; font-size: $plyr-font-size-menu;
padding: ceil($plyr-control-padding / 2) ($plyr-control-padding * 2); padding: ceil($plyr-control-padding / 2)
ceil($plyr-control-padding * 1.5);
user-select: none; user-select: none;
width: 100%; width: 100%;
> span {
align-items: inherit;
display: flex;
width: 100%;
}
&::after { &::after {
border: 4px solid transparent; border: 4px solid transparent;
content: ''; content: '';
@@ -135,50 +141,49 @@
} }
} }
label.plyr__control { .plyr__control[role='menuitemradio'] {
padding-left: $plyr-control-padding; padding-left: $plyr-control-padding;
input[type='radio'] + span { &::before,
background: rgba(#000, 0.1); &::after {
border-radius: 100%; border-radius: 100%;
}
&::before {
background: rgba(#000, 0.1);
content: '';
display: block; display: block;
flex-shrink: 0; flex-shrink: 0;
height: 16px; height: 16px;
margin-right: $plyr-control-spacing; margin-right: $plyr-control-spacing;
position: relative;
transition: all 0.3s ease; transition: all 0.3s ease;
width: 16px; width: 16px;
&::after {
background: #fff;
border-radius: 100%;
content: '';
height: 6px;
left: 5px;
opacity: 0;
position: absolute;
top: 5px;
transform: scale(0);
transition: transform 0.3s ease, opacity 0.3s ease;
width: 6px;
}
} }
input[type='radio']:checked + span { &::after {
background: $plyr-color-main; 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 { &::after {
opacity: 1; opacity: 1;
transform: scale(1); transform: translateY(-50%) scale(1);
} }
} }
input[type='radio']:focus + span { &.plyr__tab-focus::before,
@include plyr-tab-focus(); &:hover::before {
}
&.plyr__tab-focus input[type='radio'] + span,
&:hover input[type='radio'] + span {
background: rgba(#000, 0.1); background: rgba(#000, 0.1);
} }
} }
@@ -188,7 +193,7 @@
align-items: center; align-items: center;
display: flex; display: flex;
margin-left: auto; margin-left: auto;
margin-right: -$plyr-control-padding; margin-right: -($plyr-control-padding - 2);
overflow: hidden; overflow: hidden;
padding-left: ceil($plyr-control-padding * 3.5); padding-left: ceil($plyr-control-padding * 3.5);
pointer-events: none; pointer-events: none;
+1 -2
View File
@@ -12,12 +12,11 @@
opacity: 0; opacity: 0;
position: absolute; position: absolute;
top: 0; top: 0;
transition: opacity 0.3s 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;
pointer-events: none;
} }
-1
View File
@@ -3,7 +3,6 @@
// -------------------------------------------------------------- // --------------------------------------------------------------
.plyr__progress { .plyr__progress {
display: flex;
flex: 1; flex: 1;
left: $plyr-range-thumb-height / 2; left: $plyr-range-thumb-height / 2;
margin-right: $plyr-range-thumb-height; margin-right: $plyr-range-thumb-height;
+14 -4
View File
@@ -19,7 +19,11 @@
&::-webkit-slider-runnable-track { &::-webkit-slider-runnable-track {
@include plyr-range-track(); @include plyr-range-track();
background-image: linear-gradient(to right, currentColor var(--value, 0%), transparent var(--value, 0%)); background-image: linear-gradient(
to right,
currentColor var(--value, 0%),
transparent var(--value, 0%)
);
} }
&::-webkit-slider-thumb { &::-webkit-slider-thumb {
@@ -140,15 +144,21 @@
// Pressed styles // Pressed styles
&:active { &:active {
&::-webkit-slider-thumb { &::-webkit-slider-thumb {
@include plyr-range-thumb-active($plyr-audio-range-thumb-shadow-color); @include plyr-range-thumb-active(
$plyr-audio-range-thumb-shadow-color
);
} }
&::-moz-range-thumb { &::-moz-range-thumb {
@include plyr-range-thumb-active($plyr-audio-range-thumb-shadow-color); @include plyr-range-thumb-active(
$plyr-audio-range-thumb-shadow-color
);
} }
&::-ms-thumb { &::-ms-thumb {
@include plyr-range-thumb-active($plyr-audio-range-thumb-shadow-color); @include plyr-range-thumb-active(
$plyr-audio-range-thumb-shadow-color
);
} }
} }
} }
+2 -2
View File
@@ -5,7 +5,7 @@
// Nicer focus styles // Nicer focus styles
// --------------------------------------- // ---------------------------------------
@mixin plyr-tab-focus($color: $plyr-tab-focus-default-color) { @mixin plyr-tab-focus($color: $plyr-tab-focus-default-color) {
box-shadow: 0 0 0 3px rgba($color, 0.35); box-shadow: 0 0 0 5px rgba($color, 0.5);
outline: 0; outline: 0;
} }
@@ -28,6 +28,7 @@
border: 0; border: 0;
border-radius: ($plyr-range-track-height / 2); border-radius: ($plyr-range-track-height / 2);
height: $plyr-range-track-height; height: $plyr-range-track-height;
transition: box-shadow 0.3s ease;
user-select: none; user-select: none;
} }
@@ -36,7 +37,6 @@
border: 0; border: 0;
border-radius: 100%; border-radius: 100%;
box-shadow: $plyr-range-thumb-shadow; box-shadow: $plyr-range-thumb-shadow;
box-sizing: border-box;
height: $plyr-range-thumb-height; height: $plyr-range-thumb-height;
position: relative; position: relative;
transition: all 0.2s ease; transition: all 0.2s ease;
+4
View File
@@ -22,3 +22,7 @@
width: 1px; width: 1px;
} }
} }
.plyr [hidden] {
display: none !important;
}
+106 -40
View File
@@ -407,6 +407,17 @@ autoprefixer@^8.0.0:
postcss "^6.0.19" postcss "^6.0.19"
postcss-value-parser "^3.2.3" postcss-value-parser "^3.2.3"
autoprefixer@^9.0.0:
version "9.0.1"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.0.1.tgz#b5b74aba3fa60b4f1403729e46a6a1246f16818f"
dependencies:
browserslist "^4.0.1"
caniuse-lite "^1.0.30000865"
normalize-range "^0.1.2"
num2fraction "^1.2.2"
postcss "^7.0.1"
postcss-value-parser "^3.2.3"
aws-sign2@~0.6.0: aws-sign2@~0.6.0:
version "0.6.0" version "0.6.0"
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f"
@@ -1061,6 +1072,14 @@ browserslist@^3.2.6:
caniuse-lite "^1.0.30000844" caniuse-lite "^1.0.30000844"
electron-to-chromium "^1.3.47" electron-to-chromium "^1.3.47"
browserslist@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.0.1.tgz#61c05ce2a5843c7d96166408bc23d58b5416e818"
dependencies:
caniuse-lite "^1.0.30000865"
electron-to-chromium "^1.3.52"
node-releases "^1.0.0-alpha.10"
buffer-from@^1.0.0: buffer-from@^1.0.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.0.tgz#87fcaa3a298358e0ade6e442cfce840740d1ad04" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.0.tgz#87fcaa3a298358e0ade6e442cfce840740d1ad04"
@@ -1136,6 +1155,10 @@ caniuse-lite@^1.0.30000844:
version "1.0.30000847" version "1.0.30000847"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000847.tgz#be77f439be29bbc57ae08004b1e470b653b1ec1d" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000847.tgz#be77f439be29bbc57ae08004b1e470b653b1ec1d"
caniuse-lite@^1.0.30000865:
version "1.0.30000865"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000865.tgz#70026616e8afe6e1442f8bb4e1092987d81a2f25"
capture-stack-trace@^1.0.0: capture-stack-trace@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz#4a6fa07399c26bba47f0b2496b4d0fb408c5550d" resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz#4a6fa07399c26bba47f0b2496b4d0fb408c5550d"
@@ -1597,9 +1620,9 @@ currently-unhandled@^0.4.1:
dependencies: dependencies:
array-find-index "^1.0.1" array-find-index "^1.0.1"
custom-event-polyfill@^0.3.0: custom-event-polyfill@^1.0.6:
version "0.3.0" version "1.0.6"
resolved "https://registry.yarnpkg.com/custom-event-polyfill/-/custom-event-polyfill-0.3.0.tgz#99807839be62edb446b645832e0d80ead6fa1888" resolved "https://registry.yarnpkg.com/custom-event-polyfill/-/custom-event-polyfill-1.0.6.tgz#6b026e81cd9f7bc896bd6b016a427407bb068db1"
d@1: d@1:
version "1.0.0" version "1.0.0"
@@ -1859,6 +1882,10 @@ electron-to-chromium@^1.3.47:
version "1.3.48" version "1.3.48"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.48.tgz#d3b0d8593814044e092ece2108fc3ac9aea4b900" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.48.tgz#d3b0d8593814044e092ece2108fc3ac9aea4b900"
electron-to-chromium@^1.3.52:
version "1.3.52"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.52.tgz#d2d9f1270ba4a3b967b831c40ef71fb4d9ab5ce0"
"emoji-regex@>=6.0.0 <=6.1.1": "emoji-regex@>=6.0.0 <=6.1.1":
version "6.1.1" version "6.1.1"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.1.1.tgz#c6cd0ec1b0642e2a3c67a1137efc5e796da4f88e" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.1.1.tgz#c6cd0ec1b0642e2a3c67a1137efc5e796da4f88e"
@@ -2044,9 +2071,9 @@ eslint@^4.0.0:
table "4.0.2" table "4.0.2"
text-table "~0.2.0" text-table "~0.2.0"
eslint@^5.1.0: eslint@^5.2.0:
version "5.1.0" version "5.2.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.1.0.tgz#2ed611f1ce163c0fb99e1e0cda5af8f662dff645" resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.2.0.tgz#3901ae249195d473e633c4acbc370068b1c964dc"
dependencies: dependencies:
ajv "^6.5.0" ajv "^6.5.0"
babel-code-frame "^6.26.0" babel-code-frame "^6.26.0"
@@ -2064,7 +2091,7 @@ eslint@^5.1.0:
functional-red-black-tree "^1.0.1" functional-red-black-tree "^1.0.1"
glob "^7.1.2" glob "^7.1.2"
globals "^11.7.0" globals "^11.7.0"
ignore "^3.3.3" ignore "^4.0.2"
imurmurhash "^0.1.4" imurmurhash "^0.1.4"
inquirer "^5.2.0" inquirer "^5.2.0"
is-resolvable "^1.1.0" is-resolvable "^1.1.0"
@@ -2889,9 +2916,9 @@ gulp-postcss@^7.0.1:
postcss-load-config "^1.2.0" postcss-load-config "^1.2.0"
vinyl-sourcemaps-apply "^0.2.1" vinyl-sourcemaps-apply "^0.2.1"
gulp-rename@^1.3.0: gulp-rename@^1.4.0:
version "1.3.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/gulp-rename/-/gulp-rename-1.3.0.tgz#2e789d8f563ab0c924eeb62967576f37ff4cb826" resolved "https://registry.yarnpkg.com/gulp-rename/-/gulp-rename-1.4.0.tgz#de1c718e7c4095ae861f7296ef4f3248648240bd"
gulp-replace@^1.0.0: gulp-replace@^1.0.0:
version "1.0.0" version "1.0.0"
@@ -3238,6 +3265,10 @@ ignore@^3.3.3, ignore@^3.3.5:
version "3.3.7" version "3.3.7"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021" resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021"
ignore@^4.0.0, ignore@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.2.tgz#0a8dd228947ec78c2d7f736b1642a9f7317c1905"
import-lazy@^2.1.0: import-lazy@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43"
@@ -4634,6 +4665,12 @@ node-pre-gyp@^0.10.0:
semver "^5.3.0" semver "^5.3.0"
tar "^4" tar "^4"
node-releases@^1.0.0-alpha.10:
version "1.0.0-alpha.10"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.0.0-alpha.10.tgz#61c8d5f9b5b2e05d84eba941d05b6f5202f68a2a"
dependencies:
semver "^5.3.0"
node-sass@^4.8.3: node-sass@^4.8.3:
version "4.8.3" version "4.8.3"
resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.8.3.tgz#d077cc20a08ac06f661ca44fb6f19cd2ed41debb" resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.8.3.tgz#d077cc20a08ac06f661ca44fb6f19cd2ed41debb"
@@ -5144,9 +5181,9 @@ postcss-html@^0.15.0:
remark "^9.0.0" remark "^9.0.0"
unist-util-find-all-after "^1.0.1" unist-util-find-all-after "^1.0.1"
postcss-html@^0.28.0: postcss-html@^0.31.0:
version "0.28.0" version "0.31.0"
resolved "https://registry.yarnpkg.com/postcss-html/-/postcss-html-0.28.0.tgz#3dd0f5b5d7f886b8181bf844396d43a7898162cb" resolved "https://registry.yarnpkg.com/postcss-html/-/postcss-html-0.31.0.tgz#ea6ae2e95df60a03032e9ab5aba72143d8ca0325"
dependencies: dependencies:
htmlparser2 "^3.9.2" htmlparser2 "^3.9.2"
@@ -5185,9 +5222,9 @@ postcss-load-plugins@^2.3.0:
cosmiconfig "^2.1.1" cosmiconfig "^2.1.1"
object-assign "^4.1.0" object-assign "^4.1.0"
postcss-markdown@^0.28.0: postcss-markdown@^0.31.0:
version "0.28.0" version "0.31.0"
resolved "https://registry.yarnpkg.com/postcss-markdown/-/postcss-markdown-0.28.0.tgz#99d1c4e74967af9e9c98acb2e2b66df4b3c6ed86" resolved "https://registry.yarnpkg.com/postcss-markdown/-/postcss-markdown-0.31.0.tgz#e4c699ad34b14a29ad5d47132bb1b3100b60ef75"
dependencies: dependencies:
remark "^9.0.0" remark "^9.0.0"
unist-util-find-all-after "^1.0.2" unist-util-find-all-after "^1.0.2"
@@ -5215,6 +5252,12 @@ postcss-safe-parser@^3.0.1:
dependencies: dependencies:
postcss "^6.0.6" postcss "^6.0.6"
postcss-safe-parser@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-4.0.1.tgz#8756d9e4c36fdce2c72b091bbc8ca176ab1fcdea"
dependencies:
postcss "^7.0.0"
postcss-sass@^0.2.0: postcss-sass@^0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/postcss-sass/-/postcss-sass-0.2.0.tgz#e55516441e9526ba4b380a730d3a02e9eaa78c7a" resolved "https://registry.yarnpkg.com/postcss-sass/-/postcss-sass-0.2.0.tgz#e55516441e9526ba4b380a730d3a02e9eaa78c7a"
@@ -5235,6 +5278,12 @@ postcss-scss@^1.0.2:
dependencies: dependencies:
postcss "^6.0.19" postcss "^6.0.19"
postcss-scss@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-2.0.0.tgz#248b0a28af77ea7b32b1011aba0f738bda27dea1"
dependencies:
postcss "^7.0.0"
postcss-selector-parser@^3.1.0, postcss-selector-parser@^3.1.1: postcss-selector-parser@^3.1.0, postcss-selector-parser@^3.1.1:
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz#4f875f4afb0c96573d5cf4d74011aee250a7e865" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz#4f875f4afb0c96573d5cf4d74011aee250a7e865"
@@ -5258,9 +5307,13 @@ postcss-sorting@^3.1.0:
lodash "^4.17.4" lodash "^4.17.4"
postcss "^6.0.13" postcss "^6.0.13"
postcss-syntax@^0.28.0: postcss-styled@^0.31.0:
version "0.28.0" version "0.31.0"
resolved "https://registry.yarnpkg.com/postcss-syntax/-/postcss-syntax-0.28.0.tgz#e17572a7dcf5388f0c9b68232d2dad48fa7f0b12" resolved "https://registry.yarnpkg.com/postcss-styled/-/postcss-styled-0.31.0.tgz#ab532a2b3c469dfcca306a7623c4d4a98bb077d5"
postcss-syntax@^0.31.0:
version "0.31.0"
resolved "https://registry.yarnpkg.com/postcss-syntax/-/postcss-syntax-0.31.0.tgz#13d955c705d339595d10a19efa4a1bee82dfb78f"
postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0: postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0:
version "3.3.0" version "3.3.0"
@@ -5299,6 +5352,14 @@ postcss@^6.0.17:
source-map "^0.6.1" source-map "^0.6.1"
supports-color "^5.3.0" supports-color "^5.3.0"
postcss@^7.0.0, postcss@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.1.tgz#db20ca4fc90aa56809674eea75864148c66b67fa"
dependencies:
chalk "^2.4.1"
source-map "^0.6.1"
supports-color "^5.4.0"
prelude-ls@~1.1.2: prelude-ls@~1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
@@ -5420,9 +5481,9 @@ randomatic@^1.1.3:
is-number "^3.0.0" is-number "^3.0.0"
kind-of "^4.0.0" kind-of "^4.0.0"
raven-js@^3.26.3: raven-js@^3.26.4:
version "3.26.3" version "3.26.4"
resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.26.3.tgz#0efb49969b5b11ab965f7b0d6da4ca102b763cb0" resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.26.4.tgz#32aae3a63a9314467a453c94c89a364ea43707be"
rc@^1.0.1, rc@^1.1.6: rc@^1.0.1, rc@^1.1.6:
version "1.2.6" version "1.2.6"
@@ -5948,9 +6009,9 @@ rollup-plugin-babel@^3.0.7:
dependencies: dependencies:
rollup-pluginutils "^1.5.0" rollup-pluginutils "^1.5.0"
rollup-plugin-commonjs@^9.1.3: rollup-plugin-commonjs@^9.1.4:
version "9.1.3" version "9.1.4"
resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-9.1.3.tgz#37bfbf341292ea14f512438a56df8f9ca3ba4d67" resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-9.1.4.tgz#525b701adfd40e314b5bb6888d88edc28e10442f"
dependencies: dependencies:
estree-walker "^0.5.1" estree-walker "^0.5.1"
magic-string "^0.22.4" magic-string "^0.22.4"
@@ -6256,6 +6317,10 @@ specificity@^0.3.1:
version "0.3.2" version "0.3.2"
resolved "https://registry.yarnpkg.com/specificity/-/specificity-0.3.2.tgz#99e6511eceef0f8d9b57924937aac2cb13d13c42" resolved "https://registry.yarnpkg.com/specificity/-/specificity-0.3.2.tgz#99e6511eceef0f8d9b57924937aac2cb13d13c42"
specificity@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/specificity/-/specificity-0.4.0.tgz#301b1ab5455987c37d6d94f8c956ef9d9fb48c1d"
split-string@^3.0.1, split-string@^3.0.2: split-string@^3.0.1, split-string@^3.0.2:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
@@ -6467,9 +6532,9 @@ stylelint-scss@^2.0.0:
postcss-selector-parser "^3.1.1" postcss-selector-parser "^3.1.1"
postcss-value-parser "^3.3.0" postcss-value-parser "^3.3.0"
stylelint-scss@^3.1.3: stylelint-scss@^3.2.0:
version "3.1.3" version "3.2.0"
resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-3.1.3.tgz#28f881ae298c3f5db667b10b6cf94a1a219001d6" resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-3.2.0.tgz#13545a1be5ab5435ea94e761b2d4824eb32033b3"
dependencies: dependencies:
lodash "^4.17.10" lodash "^4.17.10"
postcss-media-query-parser "^0.2.3" postcss-media-query-parser "^0.2.3"
@@ -6575,11 +6640,11 @@ stylelint@^8.1.1:
svg-tags "^1.0.0" svg-tags "^1.0.0"
table "^4.0.1" table "^4.0.1"
stylelint@^9.3.0: stylelint@^9.4.0:
version "9.3.0" version "9.4.0"
resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-9.3.0.tgz#fe176e4e421ac10eac1a6b6d9f28e908eb58c5db" resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-9.4.0.tgz#2f2b82ae9db53a06735ae0724f41b134fdb84a10"
dependencies: dependencies:
autoprefixer "^8.0.0" autoprefixer "^9.0.0"
balanced-match "^1.0.0" balanced-match "^1.0.0"
chalk "^2.4.1" chalk "^2.4.1"
cosmiconfig "^5.0.0" cosmiconfig "^5.0.0"
@@ -6590,7 +6655,7 @@ stylelint@^9.3.0:
globby "^8.0.0" globby "^8.0.0"
globjoin "^0.1.4" globjoin "^0.1.4"
html-tags "^2.0.0" html-tags "^2.0.0"
ignore "^3.3.3" ignore "^4.0.0"
import-lazy "^3.1.0" import-lazy "^3.1.0"
imurmurhash "^0.1.4" imurmurhash "^0.1.4"
known-css-properties "^0.6.0" known-css-properties "^0.6.0"
@@ -6601,22 +6666,23 @@ stylelint@^9.3.0:
micromatch "^2.3.11" micromatch "^2.3.11"
normalize-selector "^0.2.0" normalize-selector "^0.2.0"
pify "^3.0.0" pify "^3.0.0"
postcss "^6.0.16" postcss "^7.0.0"
postcss-html "^0.28.0" postcss-html "^0.31.0"
postcss-less "^2.0.0" postcss-less "^2.0.0"
postcss-markdown "^0.28.0" postcss-markdown "^0.31.0"
postcss-media-query-parser "^0.2.3" postcss-media-query-parser "^0.2.3"
postcss-reporter "^5.0.0" postcss-reporter "^5.0.0"
postcss-resolve-nested-selector "^0.1.1" postcss-resolve-nested-selector "^0.1.1"
postcss-safe-parser "^3.0.1" postcss-safe-parser "^4.0.0"
postcss-sass "^0.3.0" postcss-sass "^0.3.0"
postcss-scss "^1.0.2" postcss-scss "^2.0.0"
postcss-selector-parser "^3.1.0" postcss-selector-parser "^3.1.0"
postcss-syntax "^0.28.0" postcss-styled "^0.31.0"
postcss-syntax "^0.31.0"
postcss-value-parser "^3.3.0" postcss-value-parser "^3.3.0"
resolve-from "^4.0.0" resolve-from "^4.0.0"
signal-exit "^3.0.2" signal-exit "^3.0.2"
specificity "^0.3.1" specificity "^0.4.0"
string-width "^2.1.0" string-width "^2.1.0"
style-search "^0.1.0" style-search "^0.1.0"
sugarss "^1.0.0" sugarss "^1.0.0"