Merge pull request #1142 from sampotts/a11y-improvements

A11y improvements
This commit is contained in:
Sam Potts 2018-08-02 00:47:57 +10:00 committed by GitHub
commit 18b4d26bee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 2762 additions and 1920 deletions

View File

@ -2,5 +2,6 @@
"useTabs": false, "useTabs": false,
"tabWidth": 4, "tabWidth": 4,
"singleQuote": true, "singleQuote": true,
"trailingComma": "all" "trailingComma": "all",
"printWidth": 120
} }

2
demo/dist/demo.css vendored

File diff suppressed because one or more lines are too long

58
demo/dist/demo.js vendored
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(dsn)); var globalServer = this._getGlobalServer(this._parseDSN(options.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);
}, },
@ -4097,6 +4103,9 @@ typeof navigator === "object" && (function () {
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 +4119,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 +4134,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,7 +4155,7 @@ typeof navigator === "object" && (function () {
tooltips: { tooltips: {
controls: true controls: true
}, },
clickToPlay: false, // clickToPlay: false,
/* controls: [ /* controls: [
'play-large', 'play-large',
'restart', 'restart',

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
demo/dist/error.css vendored

File diff suppressed because one or more lines are too long

View File

@ -91,12 +91,12 @@
</header> </header>
<main> <main>
<div id="container">
<video controls crossorigin playsinline poster="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg" id="player"> <video controls crossorigin playsinline poster="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg" id="player">
<!-- Video files --> <!-- Video files -->
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" type="video/mp4" size="576"> <source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" type="video/mp4" size="576">
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4" type="video/mp4" size="720"> <source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4" type="video/mp4" size="720">
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4" type="video/mp4" size="1080"> <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-1440p.mp4" type="video/mp4" size="1440"> -->
<!-- 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"
@ -106,6 +106,7 @@
<!-- 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>

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] ||

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;

2
dist/plyr.css vendored

File diff suppressed because one or more lines are too long

1325
dist/plyr.js vendored

File diff suppressed because it is too large Load Diff

2
dist/plyr.js.map vendored

File diff suppressed because one or more lines are too long

2
dist/plyr.min.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1297
dist/plyr.polyfilled.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{ {
"name": "plyr", "name": "plyr",
"version": "3.3.23", "version": "3.4.0-beta.1",
"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",

View File

@ -162,9 +162,9 @@ reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.3.23
## 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

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();

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',

658
src/js/controls.js vendored
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,143 @@ 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) {
// Handle space or -> to open menu
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,
);
},
// 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
@ -666,19 +777,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 +876,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 +889,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 +919,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 +968,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 +989,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 +1020,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 +1040,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 +1053,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,29 +1068,35 @@ 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 // Show/hide menu
toggleMenu(event) { toggleMenu(input) {
const { form } = this.elements.settings; const { popup } = this.elements.settings;
const button = this.elements.buttons.settings; const button = this.elements.buttons.settings;
// Menu and button are required // Menu and button are required
if (!is.element(form) || !is.element(button)) { if (!is.element(popup) || !is.element(button)) {
return; return;
} }
const show = is.boolean(event) ? event : is.element(form) && form.hasAttribute('hidden'); // True toggle by default
const hidden = popup.hasAttribute('hidden');
let show = hidden;
if (is.event(event)) { if (is.boolean(input)) {
const isMenuItem = is.element(form) && form.contains(event.target); show = input;
const isButton = event.target === this.elements.buttons.settings; } else if (is.keyboardEvent(input) && input.which === 27) {
show = false;
} else if (is.event(input)) {
const isMenuItem = popup.contains(input.target);
const isButton = input.target === button;
// If the click was inside the form or if the click // 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 && !isButton && show)) {
@ -989,40 +1105,38 @@ const controls = {
// Prevent the toggle being caught by the doc listener // Prevent the toggle being caught by the doc listener
if (isButton) { if (isButton) {
event.stopPropagation(); input.stopPropagation();
} }
} }
// Set form and button attributes // Set button attributes
if (is.element(button)) {
button.setAttribute('aria-expanded', show); button.setAttribute('aria-expanded', show);
}
if (is.element(form)) { // Show the actual popup
toggleHidden(form, !show); toggleHidden(popup, !show);
// Add class hook
toggleClass(this.elements.container, this.config.classNames.menu.open, show); toggleClass(this.elements.container, this.config.classNames.menu.open, show);
if (show) { // Focus the first item if key interaction
form.removeAttribute('tabindex'); if (show && is.keyboardEvent(input)) {
} else { const pane = Object.values(this.elements.settings.panels).find(pane => !pane.hidden);
form.setAttribute('tabindex', -1); const firstItem = pane.querySelector('[role^="menuitem"]');
setFocus.call(this, firstItem, true);
} }
// If closing, re-focus the button
else if (!show && !hidden) {
setFocus.call(this, button, is.keyboardEvent(input));
} }
}, },
// 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 +1153,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 +1173,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,19 +1200,13 @@ 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(); const firstItem = target.querySelector('[role^="menuitem"]');
setFocus.call(this, firstItem, tabFocus);
}, },
// Build the default HTML // Build the default HTML
@ -1225,12 +1320,12 @@ 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}`, id: `plyr-settings-toggle-${data.id}`,
'aria-haspopup': true, 'aria-haspopup': true,
@ -1239,48 +1334,52 @@ const controls = {
}), }),
); );
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}`, '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,
}); });
@ -1288,54 +1387,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;
});
home.appendChild(tabs);
inner.appendChild(home);
// Build the panes // 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`,
'aria-expanded': false,
},
i18n.get(type, this.config),
);
pane.appendChild(back);
const options = createElement('ul');
pane.appendChild(options);
inner.appendChild(pane);
this.elements.settings.panes[type] = pane;
}); });
form.appendChild(inner); // Visible label
menu.appendChild(form); backButton.appendChild(
container.appendChild(menu); createElement(
'span',
{
'aria-hidden': true,
},
i18n.get(type, this.config),
),
);
this.elements.settings.form = form; // Screen reader label
this.elements.settings.menu = menu; backButton.appendChild(
createElement(
'span',
{
class: this.config.classNames.hidden,
},
i18n.get('menuBack', this.config),
),
);
// Go back via keyboard
on(
pane,
'keydown',
event => {
// We only care about <-
if (event.which !== 37) {
return;
}
// Prevent seek
event.preventDefault();
event.stopPropagation();
// Show the respective menu
controls.showMenuPanel.call(this, 'home', true);
},
false,
);
// Go back via button click
on(backButton, 'click', () => {
controls.showMenuPanel.call(this, 'home', false);
});
// Add to pane
pane.appendChild(backButton);
// Menu
pane.appendChild(
createElement('div', {
role: 'menu',
}),
);
inner.appendChild(pane);
this.elements.settings.buttons[type] = menuItem;
this.elements.settings.panels[type] = pane;
});
popup.appendChild(inner);
control.appendChild(popup);
container.appendChild(control);
this.elements.settings.popup = popup;
this.elements.settings.menu = control;
} }
// Picture in picture button // Picture in picture button

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,227 @@ 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);
}); });
} }
// Listen for control events
controls() {
// IE doesn't support input event, so we fallback to change
const inputEvent = browser.isIE ? 'change' : 'input';
// Run default and custom handlers // Run default and custom handlers
const proxy = (event, defaultHandler, customHandlerKey) => { proxy(event, defaultHandler, customHandlerKey) {
const customHandler = this.player.config.listeners[customHandlerKey]; const { player } = this;
const customHandler = player.config.listeners[customHandlerKey];
const hasCustomHandler = is.function(customHandler); const hasCustomHandler = is.function(customHandler);
let returned = true; let returned = true;
// Execute custom handler // Execute custom handler
if (hasCustomHandler) { if (hasCustomHandler) {
returned = customHandler.call(this.player, event); returned = customHandler.call(player, event);
} }
// Only call default handler if not prevented in custom handler // Only call default handler if not prevented in custom handler
if (returned && is.function(defaultHandler)) { if (returned && is.function(defaultHandler)) {
defaultHandler.call(this.player, event); defaultHandler.call(player, event);
}
} }
};
// Trigger custom and default handlers // Trigger custom and default handlers
const bind = (element, type, defaultHandler, customHandlerKey, passive = true) => { bind(element, type, defaultHandler, customHandlerKey, passive = true) {
const customHandler = this.player.config.listeners[customHandlerKey]; const { player } = this;
const customHandler = player.config.listeners[customHandlerKey];
const hasCustomHandler = is.function(customHandler); const hasCustomHandler = is.function(customHandler);
on.call( on.call(
this.player, player,
element, element,
type, type,
event => proxy(event, defaultHandler, customHandlerKey), event => this.proxy(event, defaultHandler, customHandlerKey),
passive && !hasCustomHandler, passive && !hasCustomHandler,
); );
}; }
// Listen for control events
controls() {
const { player } = this;
const { elements } = player;
// IE doesn't support input event, so we fallback to change
const inputEvent = browser.isIE ? 'change' : 'input';
// 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); controls.toggleMenu.call(player, event);
}); });
// Settings menu // Settings menu - keyboard toggle
bind(this.player.elements.settings.form, 'click', event => { // 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
this.bind(
elements.buttons.settings,
'keyup',
event => {
// We only care about space and return
if (event.which !== 32 && event.which !== 13) {
return;
}
// Prevent scroll
event.preventDefault();
// Prevent playing video (Firefox)
if (event.which === 32) {
event.stopPropagation(); event.stopPropagation();
}
// Go back to home tab on click // Toggle menu
const showHomeTab = () => { controls.toggleMenu.call(player, event);
const id = `plyr-settings-${this.player.id}-home`; },
controls.showTab.call(this.player, id); null,
}; false,
);
// Settings menu items - use event delegation as items are added/removed // Escape closes menu
if (matches(event.target, this.player.config.selectors.inputs.language)) { this.bind(elements.settings.menu, 'keydown', event => {
proxy( if (event.which === 27) {
event, controls.toggleMenu.call(player, event);
() => {
this.player.currentTrack = Number(event.target.value);
showHomeTab();
},
'language',
);
} else if (matches(event.target, this.player.config.selectors.inputs.quality)) {
proxy(
event,
() => {
this.player.quality = event.target.value;
showHomeTab();
},
'quality',
);
} else if (matches(event.target, this.player.config.selectors.inputs.speed)) {
proxy(
event,
() => {
this.player.speed = parseFloat(event.target.value);
showHomeTab();
},
'speed',
);
} else {
const tab = event.target;
controls.showTab.call(this.player, tab.getAttribute('aria-controls'));
} }
}); });
// 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 +647,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 +722,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 +744,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();
} }

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);

View File

@ -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
if (this.config.ads.enabled) {
this.ads = new Ads(this); 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;
} }

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(() => {
try {
toggleHidden(element, true); toggleHidden(element, true);
element.offsetHeight; // eslint-disable-line element.offsetHeight; // eslint-disable-line
toggleHidden(element, false); toggleHidden(element, false);
} catch (e) {
// Do nothing
}
}, 0); }, 0);
} }

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) {
@ -105,7 +116,11 @@ export function emptyElement(element) {
// Replace element // Replace element
export function replaceElement(newChild, oldChild) { export function replaceElement(newChild, oldChild) {
if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) { if (
!is.element(oldChild) ||
!is.element(oldChild.parentNode) ||
!is.element(newChild)
) {
return null; return null;
} }
@ -192,6 +207,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 +221,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,26 +257,16 @@ 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)) {
return; return;
} }
const focusable = getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]'); const focusable = getElements.call(
this,
'button:not(:disabled), input:not(:disabled), [tabindex]',
);
const first = focusable[0]; const first = focusable[0];
const last = focusable[focusable.length - 1]; const last = focusable[focusable.length - 1];
@ -268,7 +277,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
@ -281,5 +290,27 @@ 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);
}
} }

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,

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);

View File

@ -2,11 +2,6 @@
// Controls // Controls
// -------------------------------------------------------------- // --------------------------------------------------------------
// Hide empty controls
.plyr__controls:empty {
display: none;
}
// Hide native controls // Hide native controls
.plyr--full-ui ::-webkit-media-controls { .plyr--full-ui ::-webkit-media-controls {
display: none; display: none;
@ -37,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,
@ -53,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(
@ -69,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 // Hide video controls
&.plyr__tab-focus,
&:hover,
&[aria-expanded='true'] {
background: $plyr-video-control-bg-hover;
color: $plyr-video-control-color-hover;
}
}
}
// Audio 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;

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,20 +55,18 @@
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'],
[role='menuitemradio'] {
margin-top: 2px; margin-top: 2px;
&:first-child { &:first-child {
margin-top: 0; margin-top: 0;
} }
} }
}
// Options // Options
.plyr__control { .plyr__control {
@ -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 { &::after {
background: #fff; background: #fff;
border-radius: 100%; border: 0;
content: '';
height: 6px; height: 6px;
left: 5px; left: 12px;
opacity: 0; opacity: 0;
position: absolute; top: 50%;
top: 5px; transform: translateY(-50%) scale(0);
transform: scale(0);
transition: transform 0.3s ease, opacity 0.3s ease; transition: transform 0.3s ease, opacity 0.3s ease;
width: 6px; width: 6px;
} }
}
input[type='radio']:checked + span { &[aria-checked='true'] {
&::before {
background: $plyr-color-main; 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;

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;
} }

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;

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
);
} }
} }
} }

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;

View File

@ -22,3 +22,7 @@
width: 1px; width: 1px;
} }
} }
.plyr [hidden] {
display: none !important;
}

146
yarn.lock
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"