Keyboard and focus improvements

This commit is contained in:
Sam Potts 2018-07-15 19:23:28 +10:00
parent ead6601394
commit e63ad7c74b
20 changed files with 1326 additions and 646 deletions

29
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.2', VERSION: '3.26.3',
debug: false, debug: false,
@ -2351,7 +2351,9 @@ typeof navigator === "object" && (function () {
return; return;
} }
if (this._globalOptions.stacktrace || (options && options.stacktrace)) { // Always attempt to get stacktrace if message is empty.
// It's the only way to provide any helpful information to the user.
if (this._globalOptions.stacktrace || options.stacktrace || data.message === '') {
// fingerprint on msg, not stack trace (legacy behavior, could be revisited) // fingerprint on msg, not stack trace (legacy behavior, could be revisited)
data.fingerprint = data.fingerprint == null ? msg : data.fingerprint; data.fingerprint = data.fingerprint == null ? msg : data.fingerprint;
@ -3508,6 +3510,11 @@ typeof navigator === "object" && (function () {
options options
); );
var ex = data.exception.values[0];
if (ex.type == null && ex.value === '') {
ex.value = 'Unrecoverable error caught';
}
// Move mechanism from options to exception interface // Move mechanism from options to exception interface
// We do this, as requiring user to pass `{exception:{mechanism:{ ... }}}` would be // We do this, as requiring user to pass `{exception:{mechanism:{ ... }}}` would be
// too much // too much
@ -4090,6 +4097,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: {
@ -4103,6 +4113,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);
}); });
@ -4115,12 +4128,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',

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

@ -91,21 +91,22 @@
</header> </header>
<main> <main>
<video controls crossorigin playsinline poster="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg" id="player"> <div id="container">
<!-- Video files --> <video controls crossorigin playsinline poster="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg" id="player">
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" type="video/mp4" size="576"> <!-- Video files -->
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4" type="video/mp4" size="720"> <source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" type="video/mp4" size="576">
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4" type="video/mp4" size="1080"> <source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4" type="video/mp4" size="720">
<!-- <source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1440p.mp4" type="video/mp4" size="1440"> --> <source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4" type="video/mp4" size="1080">
<!-- Caption files --> <!-- Caption files -->
<track kind="captions" label="English" srclang="en" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt" <track kind="captions" label="English" srclang="en" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt"
default> default>
<track kind="captions" label="Français" srclang="fr" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt"> <track kind="captions" label="Français" srclang="fr" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt">
<!-- Fallback for browsers that don't support the <video> element --> <!-- Fallback for browsers that don't support the <video> element -->
<a href="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" download>Download</a> <a href="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" download>Download</a>
</video> </video>
</div>
<ul> <ul>
<li class="plyr__cite plyr__cite--video" hidden> <li class="plyr__cite plyr__cite--video" hidden>

View File

@ -12,11 +12,16 @@ import Raven from 'raven-js';
// Raven / Sentry // Raven / Sentry
// For demo site (https://plyr.io) only // For demo site (https://plyr.io) only
if (isLive) { if (isLive) {
Raven.config('https://d4ad9866ad834437a4754e23937071e4@sentry.io/305555').install(); 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 +35,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 +50,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',
@ -159,40 +173,47 @@ import Raven from 'raven-js';
title: 'View From A Blue Moon', title: 'View From A Blue Moon',
sources: [ sources: [
{ {
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4', src:
'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4',
type: 'video/mp4', type: 'video/mp4',
size: 576, size: 576,
}, },
{ {
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4', src:
'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4',
type: 'video/mp4', type: 'video/mp4',
size: 720, size: 720,
}, },
{ {
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4', src:
'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4',
type: 'video/mp4', type: 'video/mp4',
size: 1080, size: 1080,
}, },
{ {
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1440p.mp4', src:
'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1440p.mp4',
type: 'video/mp4', type: 'video/mp4',
size: 1440, size: 1440,
}, },
], ],
poster: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg', poster:
'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg',
tracks: [ tracks: [
{ {
kind: 'captions', kind: 'captions',
label: 'English', label: 'English',
srclang: 'en', srclang: 'en',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt', src:
'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',
default: true, default: true,
}, },
{ {
kind: 'captions', kind: 'captions',
label: 'French', label: 'French',
srclang: 'fr', srclang: 'fr',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt', src:
'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt',
}, },
], ],
}; };
@ -202,14 +223,17 @@ import Raven from 'raven-js';
case types.audio: case types.audio:
player.source = { player.source = {
type: 'audio', type: 'audio',
title: 'Kishi Bashi &ndash; &ldquo;It All Began With A Burst&rdquo;', title:
'Kishi Bashi &ndash; &ldquo;It All Began With A Burst&rdquo;',
sources: [ sources: [
{ {
src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.mp3', src:
'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.mp3',
type: 'audio/mp3', type: 'audio/mp3',
}, },
{ {
src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.ogg', src:
'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.ogg',
type: 'audio/ogg', type: 'audio/ogg',
}, },
], ],
@ -222,7 +246,8 @@ import Raven from 'raven-js';
type: 'video', type: 'video',
sources: [ sources: [
{ {
src: 'https://youtube.com/watch?v=bTqVqk7FSmY', src:
'https://youtube.com/watch?v=bTqVqk7FSmY',
provider: 'youtube', provider: 'youtube',
}, },
], ],
@ -251,16 +276,26 @@ import Raven from 'raven-js';
currentType = type; currentType = type;
// Remove active classes // Remove active classes
Array.from(buttons).forEach(button => toggleClass(button.parentElement, 'active', false)); Array.from(buttons).forEach(button =>
toggleClass(button.parentElement, 'active', false),
);
// Set active on parent // Set active on parent
toggleClass(document.querySelector(`[data-source="${type}"]`), 'active', true); toggleClass(
document.querySelector(`[data-source="${type}"]`),
'active',
true,
);
// Show cite // Show cite
Array.from(document.querySelectorAll('.plyr__cite')).forEach(cite => { Array.from(document.querySelectorAll('.plyr__cite')).forEach(
cite.setAttribute('hidden', ''); cite => {
}); cite.setAttribute('hidden', '');
document.querySelector(`.plyr__cite--${type}`).removeAttribute('hidden'); },
);
document
.querySelector(`.plyr__cite--${type}`)
.removeAttribute('hidden');
} }
// Bind to each button // Bind to each button
@ -328,7 +363,13 @@ import Raven from 'raven-js';
a.async = 1; a.async = 1;
a.src = g; a.src = g;
m.parentNode.insertBefore(a, m); m.parentNode.insertBefore(a, m);
})(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga'); })(
window,
document,
'script',
'https://www.google-analytics.com/analytics.js',
'ga',
);
window.ga('create', 'UA-40881672-11', 'auto'); window.ga('create', 'UA-40881672-11', 'auto');
window.ga('send', 'pageview'); window.ga('send', 'pageview');
} }

474
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

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,10 +1,19 @@
{ {
"name": "plyr", "name": "plyr",
"version": "3.3.16", "version": "3.3.16",
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player", "description":
"A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
"homepage": "https://plyr.io", "homepage": "https://plyr.io",
"author": "Sam Potts <sam@potts.es>", "author": "Sam Potts <sam@potts.es>",
"keywords": ["HTML5 Video", "HTML5 Audio", "Media Player", "DASH", "Shaka", "WordPress", "HLS"], "keywords": [
"HTML5 Video",
"HTML5 Audio",
"Media Player",
"DASH",
"Shaka",
"WordPress",
"HLS"
],
"main": "./dist/plyr.js", "main": "./dist/plyr.js",
"browser": "./dist/plyr.min.js", "browser": "./dist/plyr.min.js",
"sass": "./src/sass/plyr.scss", "sass": "./src/sass/plyr.scss",
@ -27,11 +36,11 @@
}, },
"devDependencies": { "devDependencies": {
"babel-core": "^6.26.3", "babel-core": "^6.26.3",
"babel-eslint": "^7.2.3", "babel-eslint": "^8.2.5",
"babel-plugin-external-helpers": "^6.22.0", "babel-plugin-external-helpers": "^6.22.0",
"babel-preset-env": "^1.7.0", "babel-preset-env": "^1.7.0",
"del": "^3.0.0", "del": "^3.0.0",
"eslint": "^5.0.1", "eslint": "^5.1.0",
"eslint-config-airbnb-base": "^13.0.0", "eslint-config-airbnb-base": "^13.0.0",
"eslint-config-prettier": "^2.9.0", "eslint-config-prettier": "^2.9.0",
"eslint-plugin-import": "^2.13.0", "eslint-plugin-import": "^2.13.0",
@ -58,7 +67,7 @@
"postcss-custom-properties": "^7.0.0", "postcss-custom-properties": "^7.0.0",
"prettier-eslint": "^8.8.2", "prettier-eslint": "^8.8.2",
"prettier-stylelint": "^0.4.2", "prettier-stylelint": "^0.4.2",
"rollup-plugin-babel": "^3.0.5", "rollup-plugin-babel": "^3.0.7",
"rollup-plugin-commonjs": "^9.1.3", "rollup-plugin-commonjs": "^9.1.3",
"rollup-plugin-node-resolve": "^3.3.0", "rollup-plugin-node-resolve": "^3.3.0",
"run-sequence": "^2.2.1", "run-sequence": "^2.2.1",

128
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';
@ -360,11 +361,53 @@ const controls = {
return container; return container;
}, },
// Bind keyboard shortcuts for a menu item
bindMenuItemShortcuts(menuItem, type) {
// Handle space or -> to open menu
on(menuItem, 'keydown', event => {
// We only care about space and ⬆️ ⬇️️ ➡️
if (![32,38,39,40].includes(event.which)) {
return;
}
// Prevent play / seek
event.preventDefault();
event.stopPropagation();
const isRadioButton = matches(menuItem, '[role="menuitemradio"]');
// Show the respective menu
if (!isRadioButton && [32,39].includes(event.which)) {
controls.showMenuPanel.call(this, type);
} 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 attributes = getAttributesFromSelector(this.config.selectors.inputs[type]); const attributes = getAttributesFromSelector(this.config.selectors.inputs[type]);
const item = createElement( const menuItem = createElement(
'button', 'button',
extend(attributes, { extend(attributes, {
type: 'button', type: 'button',
@ -384,30 +427,38 @@ const controls = {
flex.appendChild(badge); flex.appendChild(badge);
} }
item.appendChild(flex); menuItem.appendChild(flex);
Object.defineProperty(item, 'checked', { // Replicate radio button behaviour
Object.defineProperty(menuItem, 'checked', {
enumerable: true, enumerable: true,
get() { get() {
return item.getAttribute('aria-checked') === 'true'; return menuItem.getAttribute('aria-checked') === 'true';
}, },
set(checked) { set(checked) {
// Ensure exclusivity // Ensure exclusivity
if (checked) { if (checked) {
Array.from(item.parentNode.children) Array.from(menuItem.parentNode.children)
.filter(node => matches(node, '[role="menuitemradio"]')) .filter(node => matches(node, '[role="menuitemradio"]'))
.forEach(node => node.setAttribute('aria-checked', 'false')); .forEach(node => node.setAttribute('aria-checked', 'false'));
} }
item.setAttribute('aria-checked', checked ? 'true' : 'false'); menuItem.setAttribute('aria-checked', checked ? 'true' : 'false');
}, },
}); });
this.listeners.bind( this.listeners.bind(
item, menuItem,
'click', 'click keydown',
() => { event => {
item.checked = true; if (event.type === 'keydown' && event.which !== 32) {
return;
}
event.preventDefault();
event.stopPropagation();
menuItem.checked = true;
switch (type) { switch (type) {
case 'language': case 'language':
@ -429,9 +480,12 @@ const controls = {
controls.showMenuPanel.call(this, 'home'); controls.showMenuPanel.call(this, 'home');
}, },
type, type,
false,
); );
list.appendChild(item); controls.bindMenuItemShortcuts.call(this, menuItem, type);
list.appendChild(menuItem);
}, },
// Format a time for display // Format a time for display
@ -993,7 +1047,7 @@ const controls = {
}, },
// Show/hide menu // Show/hide menu
toggleMenu(event) { toggleMenu(input) {
const { popup } = this.elements.settings; const { popup } = this.elements.settings;
const button = this.elements.buttons.settings; const button = this.elements.buttons.settings;
@ -1002,11 +1056,11 @@ const controls = {
return; return;
} }
const show = is.boolean(event) ? event : is.element(popup) && popup.hasAttribute('hidden'); const show = is.boolean(input) ? input : is.element(popup) && popup.hasAttribute('hidden');
if (is.event(event)) { if (is.event(input)) {
const isMenuItem = is.element(popup) && popup.contains(event.target); const isMenuItem = is.element(popup) && popup.contains(input.target);
const isButton = event.target === this.elements.buttons.settings; const isButton = input.target === this.elements.buttons.settings;
// If the click was inside the form or if the click // If the click was inside the form 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
@ -1017,7 +1071,7 @@ 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();
} }
} }
@ -1031,17 +1085,11 @@ const controls = {
toggleHidden(popup, !show); toggleHidden(popup, !show);
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
popup.removeAttribute('tabindex'); if (show && is.event(input) && input.type === 'keydown') {
const pane = Object.values(this.elements.settings.panels).find(pane => !pane.hidden);
// Focus the first item if key interaction const firstItem = pane.querySelector('[role^="menuitem"]');
if (event.type === 'keydown') { setFocus.call(this, firstItem, true);
const pane = Object.values(this.elements.settings.panels).find(pane => !pane.hidden);
const firstItem = pane.querySelector('[role^="menuitem"]');
setFocus.call(this, firstItem, true);
}
} else {
popup.setAttribute('tabindex', -1);
} }
} }
}, },
@ -1275,9 +1323,11 @@ const controls = {
home.appendChild(menu); home.appendChild(menu);
inner.appendChild(home); inner.appendChild(home);
this.elements.settings.panels.home = home;
// Build the menu items // Build the menu items
this.config.settings.forEach(type => { this.config.settings.forEach(type => {
// TODO: bundle this with the createMenuItem helper and bindings
const menuItem = createElement( const menuItem = createElement(
'button', 'button',
extend(getAttributesFromSelector(this.config.selectors.buttons.settings), { extend(getAttributesFromSelector(this.config.selectors.buttons.settings), {
@ -1289,20 +1339,8 @@ const controls = {
}), }),
); );
// Handle space or -> to open menu // Bind menu shortcuts for keyboard users
on(menuItem, 'keydown', event => { controls.bindMenuItemShortcuts.call(this, menuItem, type);
// We only care about space and ->
if (![32,39].includes(event.which)) {
return;
}
// Prevent play / seek
event.preventDefault();
event.stopPropagation();
// Show the respective menu
controls.showMenuPanel.call(this, type);
}, false);
// Show menu on click // Show menu on click
on(menuItem, 'click', () => { on(menuItem, 'click', () => {
@ -1356,8 +1394,8 @@ const controls = {
), ),
); );
// Handle space or -> to open menu // Go back via keyboard
on(backButton, 'keydown', event => { on(pane, 'keydown', event => {
// We only care about <- // We only care about <-
if (event.which !== 37) { if (event.which !== 37) {
return; return;
@ -1371,7 +1409,7 @@ const controls = {
controls.showMenuPanel.call(this, 'home'); controls.showMenuPanel.call(this, 'home');
}, false); }, false);
// Go back // Go back via button click
on(backButton, 'click', () => { on(backButton, 'click', () => {
controls.showMenuPanel.call(this, 'home'); controls.showMenuPanel.call(this, 'home');
}); });

View File

@ -5,7 +5,13 @@
import controls from './controls'; import controls from './controls';
import ui from './ui'; import ui from './ui';
import browser from './utils/browser'; import browser from './utils/browser';
import { getElement, getElements, getFocusElement, matches, toggleClass, toggleHidden } from './utils/elements'; import {
getElement,
getElements,
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,9 +19,12 @@ 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);
} }
@ -45,21 +54,51 @@ class Listeners {
// 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 } = this.player.config.selectors;
(focused !== this.player.elements.inputs.seek && const { seek } = this.player.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();
@ -153,7 +192,11 @@ 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 (
!this.player.fullscreen.enabled &&
this.player.fullscreen.active &&
code === 27
) {
this.player.fullscreen.toggle(); this.player.fullscreen.toggle();
} }
@ -174,48 +217,117 @@ class Listeners {
this.player.touch = true; this.player.touch = true;
// Add touch class // Add touch class
toggleClass(this.player.elements.container, this.player.config.classNames.isTouch, true); toggleClass(
this.player.elements.container,
this.player.config.classNames.isTouch,
true,
);
}
setTabFocus(event) {
clearTimeout(this.focusTimer);
// Ignore any key other than tab
if (event.type === 'keydown' && event.code !== 'Tab') {
return;
}
// Store reference to event timeStamp
if (event.type === 'keydown') {
this.lastKeyDown = event.timeStamp;
}
// Remove current classes
const removeCurrent = () => {
const className = this.player.config.classNames.tabFocus;
const current = getElements.call(this.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 (!this.player.elements.container.contains(focused)) {
return;
}
toggleClass(
document.activeElement,
this.player.config.classNames.tabFocus,
true,
);
}, 10);
} }
// Global window & document listeners // Global window & document listeners
global(toggle = true) { global(toggle = true) {
// Keyboard shortcuts // Keyboard shortcuts
if (this.player.config.keyboard.global) { if (this.player.config.keyboard.global) {
toggleListener.call(this.player, window, 'keydown keyup', this.handleKey, toggle, false); toggleListener.call(
this.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(
this.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(this.player, document.body, 'touchstart', this.firstTouch);
// Tab focus detection
toggleListener.call(
this.player,
document.body,
'keydown focus blur',
this.setTabFocus,
toggle,
false,
true,
);
} }
// Container listeners // Container listeners
container() { container() {
// Keyboard shortcuts // Keyboard shortcuts
if (!this.player.config.keyboard.global && this.player.config.keyboard.focused) { if (
on.call(this.player, this.player.elements.container, 'keydown keyup', this.handleKey, false); !this.player.config.keyboard.global &&
this.player.config.keyboard.focused
) {
on.call(
this.player,
this.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, this.player,
@ -231,7 +343,9 @@ class Listeners {
} }
// Show, then hide after a timeout unless another control event occurs // Show, then hide after a timeout unless another control event occurs
const show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type); const show = ['touchstart', 'touchmove', 'mousemove'].includes(
event.type,
);
let delay = 0; let delay = 0;
@ -245,7 +359,10 @@ class Listeners {
clearTimeout(this.player.timers.controls); clearTimeout(this.player.timers.controls);
// Set new timer to prevent flicker when seeking // Set new timer to prevent flicker when seeking
this.player.timers.controls = setTimeout(() => ui.toggleControls.call(this.player, false), delay); this.player.timers.controls = setTimeout(
() => ui.toggleControls.call(this.player, false),
delay,
);
}, },
); );
} }
@ -253,34 +370,50 @@ class Listeners {
// Listen for media events // Listen for media events
media() { media() {
// Time change on media // Time change on media
on.call(this.player, this.player.media, 'timeupdate seeking seeked', event => on.call(
controls.timeUpdate.call(this.player, event), this.player,
this.player.media,
'timeupdate seeking seeked',
event => controls.timeUpdate.call(this.player, event),
); );
// Display duration // Display duration
on.call(this.player, this.player.media, 'durationchange loadeddata loadedmetadata', event => on.call(
controls.durationUpdate.call(this.player, event), this.player,
this.player.media,
'durationchange loadeddata loadedmetadata',
event => controls.durationUpdate.call(this.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(this.player, this.player.media, 'canplay', () => {
toggleHidden(this.player.elements.volume, !this.player.hasAudio); toggleHidden(this.player.elements.volume, !this.player.hasAudio);
toggleHidden(this.player.elements.buttons.mute, !this.player.hasAudio); toggleHidden(
this.player.elements.buttons.mute,
!this.player.hasAudio,
);
}); });
// Handle the media finishing // Handle the media finishing
on.call(this.player, this.player.media, 'ended', () => { on.call(this.player, this.player.media, 'ended', () => {
// Show poster on end // Show poster on end
if (this.player.isHTML5 && this.player.isVideo && this.player.config.resetOnEnd) { if (
this.player.isHTML5 &&
this.player.isVideo &&
this.player.config.resetOnEnd
) {
// Restart // Restart
this.player.restart(); this.player.restart();
} }
}); });
// Check for buffer progress // Check for buffer progress
on.call(this.player, this.player.media, 'progress playing seeking seeked', event => on.call(
controls.updateProgress.call(this.player, event), this.player,
this.player.media,
'progress playing seeking seeked',
event => controls.updateProgress.call(this.player, event),
); );
// Handle volume changes // Handle volume changes
@ -289,13 +422,19 @@ class Listeners {
); );
// Handle play/pause // Handle play/pause
on.call(this.player, this.player.media, 'playing play pause ended emptied timeupdate', event => on.call(
ui.checkPlaying.call(this.player, event), this.player,
this.player.media,
'playing play pause ended emptied timeupdate',
event => ui.checkPlaying.call(this.player, event),
); );
// Loading state // Loading state
on.call(this.player, this.player.media, 'waiting canplay seeked playing', event => on.call(
ui.checkLoading.call(this.player, event), this.player,
this.player.media,
'waiting canplay seeked playing',
event => ui.checkLoading.call(this.player, event),
); );
// If autoplay, then load advertisement if required // If autoplay, then load advertisement if required
@ -308,14 +447,23 @@ class Listeners {
// 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 (this.player.ads.enabled && !this.player.ads.initialized) {
// Wait for manager response // Wait for manager response
this.player.ads.managerPromise.then(() => this.player.ads.play()).catch(() => this.player.play()); this.player.ads.managerPromise
.then(() => this.player.ads.play())
.catch(() => this.player.play());
} }
}); });
// Click video // Click video
if (this.player.supported.ui && this.player.config.clickToPlay && !this.player.isAudio) { if (
this.player.supported.ui &&
this.player.config.clickToPlay &&
!this.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(
this.player,
`.${this.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)) {
@ -325,7 +473,11 @@ class Listeners {
// On click play, pause ore restart // On click play, pause ore restart
on.call(this.player, wrapper, 'click', () => { on.call(this.player, wrapper, 'click', () => {
// Touch devices will just show controls (if we're hiding controls) // Touch devices will just show controls (if we're hiding controls)
if (this.player.config.hideControls && this.player.touch && !this.player.paused) { if (
this.player.config.hideControls &&
this.player.touch &&
!this.player.paused
) {
return; return;
} }
@ -356,7 +508,10 @@ class Listeners {
// Volume change // Volume change
on.call(this.player, this.player.media, 'volumechange', () => { on.call(this.player, this.player.media, 'volumechange', () => {
// Save to storage // Save to storage
this.player.storage.set({ volume: this.player.volume, muted: this.player.muted }); this.player.storage.set({
volume: this.player.volume,
muted: this.player.muted,
});
}); });
// Speed change // Speed change
@ -377,12 +532,20 @@ class Listeners {
// Quality change // Quality change
on.call(this.player, this.player.media, 'qualitychange', event => { on.call(this.player, this.player.media, 'qualitychange', event => {
// Update UI // Update UI
controls.updateSetting.call(this.player, 'quality', null, event.detail.quality); controls.updateSetting.call(
this.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 = this.player.config.events
.concat(['keyup', 'keydown'])
.join(' ');
on.call(this.player, this.player.media, proxyEvents, event => { on.call(this.player, this.player.media, proxyEvents, event => {
let { detail = {} } = event; let { detail = {} } = event;
@ -391,7 +554,13 @@ class Listeners {
detail = this.player.media.error; detail = this.player.media.error;
} }
triggerEvent.call(this.player, this.player.elements.container, event.type, true, detail); triggerEvent.call(
this.player,
this.player.elements.container,
event.type,
true,
detail,
);
}); });
} }
@ -439,13 +608,28 @@ class Listeners {
} }
// Pause // Pause
this.bind(this.player.elements.buttons.restart, 'click', this.player.restart, 'restart'); this.bind(
this.player.elements.buttons.restart,
'click',
this.player.restart,
'restart',
);
// Rewind // Rewind
this.bind(this.player.elements.buttons.rewind, 'click', this.player.rewind, 'rewind'); this.bind(
this.player.elements.buttons.rewind,
'click',
this.player.rewind,
'rewind',
);
// Rewind // Rewind
this.bind(this.player.elements.buttons.fastForward, 'click', this.player.forward, 'fastForward'); this.bind(
this.player.elements.buttons.fastForward,
'click',
this.player.forward,
'fastForward',
);
// Mute toggle // Mute toggle
this.bind( this.bind(
@ -458,7 +642,9 @@ class Listeners {
); );
// Captions toggle // Captions toggle
this.bind(this.player.elements.buttons.captions, 'click', () => this.player.toggleCaptions()); this.bind(this.player.elements.buttons.captions, 'click', () =>
this.player.toggleCaptions(),
);
// Fullscreen toggle // Fullscreen toggle
this.bind( this.bind(
@ -481,7 +667,12 @@ class Listeners {
); );
// Airplay // Airplay
this.bind(this.player.elements.buttons.airplay, 'click', this.player.airplay, 'airplay'); this.bind(
this.player.elements.buttons.airplay,
'click',
this.player.airplay,
'airplay',
);
// Settings menu - click toggle // Settings menu - click toggle
this.bind(this.player.elements.buttons.settings, 'click', event => { this.bind(this.player.elements.buttons.settings, 'click', event => {
@ -512,37 +703,51 @@ class Listeners {
); );
// Set range input alternative "value", which matches the tooltip time (#954) // Set range input alternative "value", which matches the tooltip time (#954)
this.bind(this.player.elements.inputs.seek, 'mousedown mousemove', event => { this.bind(
const clientRect = this.player.elements.progress.getBoundingClientRect(); this.player.elements.inputs.seek,
const percent = 100 / clientRect.width * (event.pageX - clientRect.left); 'mousedown mousemove',
event.currentTarget.setAttribute('seek-value', percent); event => {
}); const clientRect = this.player.elements.progress.getBoundingClientRect();
const percent =
100 / clientRect.width * (event.pageX - clientRect.left);
event.currentTarget.setAttribute('seek-value', percent);
},
);
// Pause while seeking // Pause while seeking
this.bind(this.player.elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => { this.bind(
const seek = event.currentTarget; this.player.elements.inputs.seek,
'mousedown mouseup keydown keyup touchstart touchend',
event => {
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 eventType = event.type;
if ((eventType === 'keydown' || eventType === 'keyup') && (code !== 39 && code !== 37)) { if (
return; (eventType === 'keydown' || eventType === 'keyup') &&
} (code !== 39 && code !== 37)
// Was playing before? ) {
const play = seek.hasAttribute('play-on-seeked'); return;
}
// Was playing before?
const play = seek.hasAttribute('play-on-seeked');
// 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('play-on-seeked');
this.player.play(); this.player.play();
} else if (!done && this.player.playing) { } else if (!done && this.player.playing) {
seek.setAttribute('play-on-seeked', ''); seek.setAttribute('play-on-seeked', '');
this.player.pause(); this.player.pause();
} }
}); },
);
// Seek // Seek
this.bind( this.bind(
@ -560,14 +765,18 @@ class Listeners {
seek.removeAttribute('seek-value'); seek.removeAttribute('seek-value');
this.player.currentTime = seekTo / seek.max * this.player.duration; this.player.currentTime =
seekTo / seek.max * this.player.duration;
}, },
'seek', 'seek',
); );
// Current time invert // Current time invert
// Only if one time element is used for both currentTime and duration // Only if one time element is used for both currentTime and duration
if (this.player.config.toggleInvert && !is.element(this.player.elements.display.duration)) { if (
this.player.config.toggleInvert &&
!is.element(this.player.elements.display.duration)
) {
this.bind(this.player.elements.display.currentTime, 'click', () => { this.bind(this.player.elements.display.currentTime, 'click', () => {
// Do nothing if we're at the start // Do nothing if we're at the start
if (this.player.currentTime === 0) { if (this.player.currentTime === 0) {
@ -592,32 +801,54 @@ class Listeners {
// 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(
this.bind(element, 'input', event => controls.updateRangeFill.call(this.player, event.target)); getElements.call(this.player, 'input[type="range"]'),
).forEach(element => {
this.bind(element, 'input', event =>
controls.updateRangeFill.call(this.player, event.target),
);
}); });
} }
// Seek tooltip // Seek tooltip
this.bind(this.player.elements.progress, 'mouseenter mouseleave mousemove', event => this.bind(
controls.updateSeekTooltip.call(this.player, event), this.player.elements.progress,
'mouseenter mouseleave mousemove',
event => controls.updateSeekTooltip.call(this.player, event),
); );
// 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)
this.bind(this.player.elements.controls, 'mouseenter mouseleave', event => { this.bind(
this.player.elements.controls.hover = !this.player.touch && event.type === 'mouseenter'; this.player.elements.controls,
}); 'mouseenter mouseleave',
event => {
this.player.elements.controls.hover =
!this.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)
this.bind(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => { this.bind(
this.player.elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type); this.player.elements.controls,
}); 'mousedown mouseup touchstart touchend touchcancel',
event => {
this.player.elements.controls.pressed = [
'mousedown',
'touchstart',
].includes(event.type);
},
);
// Focus in/out on controls // Focus in/out on controls
this.bind(this.player.elements.controls, 'focusin focusout', event => { this.bind(this.player.elements.controls, 'focusin focusout', event => {
const { config, elements, timers } = this.player; const { config, elements, timers } = this.player;
// 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,
event.type === 'focusin',
);
// Toggle // Toggle
ui.toggleControls.call(this.player, event.type === 'focusin'); ui.toggleControls.call(this.player, event.type === 'focusin');
@ -626,7 +857,11 @@ class Listeners {
if (event.type === 'focusin') { if (event.type === 'focusin') {
// Restore transition // Restore transition
setTimeout(() => { setTimeout(() => {
toggleClass(elements.controls, config.classNames.noTransition, false); toggleClass(
elements.controls,
config.classNames.noTransition,
false,
);
}, 0); }, 0);
// Delay a little more for keyboard users // Delay a little more for keyboard users
@ -634,8 +869,12 @@ 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(this.player, false),
delay,
);
} }
}); });
@ -649,7 +888,9 @@ class Listeners {
const inverted = event.webkitDirectionInvertedFromDevice; const inverted = event.webkitDirectionInvertedFromDevice;
// Get delta from event. Invert if `inverted` is true // Get delta from event. Invert if `inverted` is true
const [x, y] = [event.deltaX, -event.deltaY].map(value => (inverted ? -value : value)); const [x, y] = [event.deltaX, -event.deltaY].map(
value => (inverted ? -value : value),
);
// Using the biggest delta, normalize to 1 or -1 (or 0 if no delta) // Using the biggest delta, normalize to 1 or -1 (or 0 if no delta)
const direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y); const direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y);
@ -659,7 +900,10 @@ class Listeners {
// 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 } = this.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

@ -116,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;
} }
@ -203,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') {
@ -213,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
@ -249,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];
@ -279,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
@ -292,7 +290,14 @@ 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 // Set focus and tab focus class

View File

@ -27,9 +27,21 @@ const supportsPassiveListeners = (() => {
})(); })();
// Toggle event listener // Toggle event listener
export function toggleListener(element, event, callback, toggle = false, passive = true, capture = false) { export function toggleListener(
element,
event,
callback,
toggle = false,
passive = true,
capture = false,
) {
// Bail if no element, event, or callback // Bail if no element, event, or callback
if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) { if (
!element ||
!('addEventListener' in element) ||
is.empty(event) ||
!is.function(callback)
) {
return; return;
} }
@ -57,28 +69,74 @@ export function toggleListener(element, event, callback, toggle = false, passive
this.eventListeners.push({ element, type, callback, options }); this.eventListeners.push({ element, type, callback, options });
} }
element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options); element[toggle ? 'addEventListener' : 'removeEventListener'](
type,
callback,
options,
);
}); });
} }
// Bind event handler // Bind event handler
export function on(element, events = '', callback, passive = true, capture = false) { export function on(
toggleListener.call(this, element, events, callback, true, passive, capture); element,
events = '',
callback,
passive = true,
capture = false,
) {
toggleListener.call(
this,
element,
events,
callback,
true,
passive,
capture,
);
} }
// Unbind event handler // Unbind event handler
export function off(element, events = '', callback, passive = true, capture = false) { export function off(
toggleListener.call(this, element, events, callback, false, passive, capture); element,
events = '',
callback,
passive = true,
capture = false,
) {
toggleListener.call(
this,
element,
events,
callback,
false,
passive,
capture,
);
} }
// Bind once-only event handler // Bind once-only event handler
export function once(element, events = '', callback, passive = true, capture = false) { export function once(
element,
events = '',
callback,
passive = true,
capture = false,
) {
function onceCallback(...args) { function onceCallback(...args) {
off(element, events, onceCallback, passive, capture); off(element, events, onceCallback, passive, capture);
callback.apply(this, args); callback.apply(this, args);
} }
toggleListener.call(this, element, events, onceCallback, true, passive, capture); toggleListener.call(
this,
element,
events,
onceCallback,
true,
passive,
capture,
);
} }
// Trigger event // Trigger event
@ -115,6 +173,9 @@ export function unbindListeners() {
// Run method when / if player is ready // Run method when / if player is ready
export function ready() { export function ready() {
return new Promise( return new Promise(
resolve => (this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve)), resolve =>
this.ready
? setTimeout(resolve, 0)
: on.call(this, this.elements.container, 'ready', resolve),
).then(() => {}); ).then(() => {});
} }

145
yarn.lock
View File

@ -2,6 +2,82 @@
# yarn lockfile v1 # yarn lockfile v1
"@babel/code-frame@7.0.0-beta.44":
version "7.0.0-beta.44"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.44.tgz#2a02643368de80916162be70865c97774f3adbd9"
dependencies:
"@babel/highlight" "7.0.0-beta.44"
"@babel/generator@7.0.0-beta.44":
version "7.0.0-beta.44"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.0.0-beta.44.tgz#c7e67b9b5284afcf69b309b50d7d37f3e5033d42"
dependencies:
"@babel/types" "7.0.0-beta.44"
jsesc "^2.5.1"
lodash "^4.2.0"
source-map "^0.5.0"
trim-right "^1.0.1"
"@babel/helper-function-name@7.0.0-beta.44":
version "7.0.0-beta.44"
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.44.tgz#e18552aaae2231100a6e485e03854bc3532d44dd"
dependencies:
"@babel/helper-get-function-arity" "7.0.0-beta.44"
"@babel/template" "7.0.0-beta.44"
"@babel/types" "7.0.0-beta.44"
"@babel/helper-get-function-arity@7.0.0-beta.44":
version "7.0.0-beta.44"
resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.44.tgz#d03ca6dd2b9f7b0b1e6b32c56c72836140db3a15"
dependencies:
"@babel/types" "7.0.0-beta.44"
"@babel/helper-split-export-declaration@7.0.0-beta.44":
version "7.0.0-beta.44"
resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0-beta.44.tgz#c0b351735e0fbcb3822c8ad8db4e583b05ebd9dc"
dependencies:
"@babel/types" "7.0.0-beta.44"
"@babel/highlight@7.0.0-beta.44":
version "7.0.0-beta.44"
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0-beta.44.tgz#18c94ce543916a80553edcdcf681890b200747d5"
dependencies:
chalk "^2.0.0"
esutils "^2.0.2"
js-tokens "^3.0.0"
"@babel/template@7.0.0-beta.44":
version "7.0.0-beta.44"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.44.tgz#f8832f4fdcee5d59bf515e595fc5106c529b394f"
dependencies:
"@babel/code-frame" "7.0.0-beta.44"
"@babel/types" "7.0.0-beta.44"
babylon "7.0.0-beta.44"
lodash "^4.2.0"
"@babel/traverse@7.0.0-beta.44":
version "7.0.0-beta.44"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.0.0-beta.44.tgz#a970a2c45477ad18017e2e465a0606feee0d2966"
dependencies:
"@babel/code-frame" "7.0.0-beta.44"
"@babel/generator" "7.0.0-beta.44"
"@babel/helper-function-name" "7.0.0-beta.44"
"@babel/helper-split-export-declaration" "7.0.0-beta.44"
"@babel/types" "7.0.0-beta.44"
babylon "7.0.0-beta.44"
debug "^3.1.0"
globals "^11.1.0"
invariant "^2.2.0"
lodash "^4.2.0"
"@babel/types@7.0.0-beta.44":
version "7.0.0-beta.44"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.0.0-beta.44.tgz#6b1b164591f77dec0a0342aca995f2d046b3a757"
dependencies:
esutils "^2.0.2"
lodash "^4.2.0"
to-fast-properties "^2.0.0"
"@gulp-sourcemaps/identity-map@1.X": "@gulp-sourcemaps/identity-map@1.X":
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/@gulp-sourcemaps/identity-map/-/identity-map-1.0.1.tgz#cfa23bc5840f9104ce32a65e74db7e7a974bbee1" resolved "https://registry.yarnpkg.com/@gulp-sourcemaps/identity-map/-/identity-map-1.0.1.tgz#cfa23bc5840f9104ce32a65e74db7e7a974bbee1"
@ -388,14 +464,16 @@ babel-core@^6.26.3:
slash "^1.0.0" slash "^1.0.0"
source-map "^0.5.7" source-map "^0.5.7"
babel-eslint@^7.2.3: babel-eslint@^8.2.5:
version "7.2.3" version "8.2.5"
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-7.2.3.tgz#b2fe2d80126470f5c19442dc757253a897710827" resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-8.2.5.tgz#dc2331c259d36782aa189da510c43dedd5adc7a3"
dependencies: dependencies:
babel-code-frame "^6.22.0" "@babel/code-frame" "7.0.0-beta.44"
babel-traverse "^6.23.1" "@babel/traverse" "7.0.0-beta.44"
babel-types "^6.23.0" "@babel/types" "7.0.0-beta.44"
babylon "^6.17.0" babylon "7.0.0-beta.44"
eslint-scope "~3.7.1"
eslint-visitor-keys "^1.0.0"
babel-generator@^6.26.0: babel-generator@^6.26.0:
version "6.26.1" version "6.26.1"
@ -810,7 +888,7 @@ babel-template@^6.24.1, babel-template@^6.26.0:
babylon "^6.18.0" babylon "^6.18.0"
lodash "^4.17.4" lodash "^4.17.4"
babel-traverse@^6.23.1, babel-traverse@^6.24.1, babel-traverse@^6.26.0: babel-traverse@^6.24.1, babel-traverse@^6.26.0:
version "6.26.0" version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee"
dependencies: dependencies:
@ -824,7 +902,7 @@ babel-traverse@^6.23.1, babel-traverse@^6.24.1, babel-traverse@^6.26.0:
invariant "^2.2.2" invariant "^2.2.2"
lodash "^4.17.4" lodash "^4.17.4"
babel-types@^6.19.0, babel-types@^6.23.0, babel-types@^6.24.1, babel-types@^6.26.0: babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26.0:
version "6.26.0" version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497"
dependencies: dependencies:
@ -833,7 +911,11 @@ babel-types@^6.19.0, babel-types@^6.23.0, babel-types@^6.24.1, babel-types@^6.26
lodash "^4.17.4" lodash "^4.17.4"
to-fast-properties "^1.0.3" to-fast-properties "^1.0.3"
babylon@^6.17.0, babylon@^6.18.0: babylon@7.0.0-beta.44:
version "7.0.0-beta.44"
resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.44.tgz#89159e15e6e30c5096e22d738d8c0af8a0e8ca1d"
babylon@^6.18.0:
version "6.18.0" version "6.18.0"
resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
@ -1838,7 +1920,7 @@ eslint-restricted-globals@^0.1.1:
version "0.1.1" version "0.1.1"
resolved "https://registry.yarnpkg.com/eslint-restricted-globals/-/eslint-restricted-globals-0.1.1.tgz#35f0d5cbc64c2e3ed62e93b4b1a7af05ba7ed4d7" resolved "https://registry.yarnpkg.com/eslint-restricted-globals/-/eslint-restricted-globals-0.1.1.tgz#35f0d5cbc64c2e3ed62e93b4b1a7af05ba7ed4d7"
eslint-scope@^3.7.1: eslint-scope@^3.7.1, eslint-scope@~3.7.1:
version "3.7.1" version "3.7.1"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8"
dependencies: dependencies:
@ -1852,6 +1934,10 @@ eslint-scope@^4.0.0:
esrecurse "^4.1.0" esrecurse "^4.1.0"
estraverse "^4.1.1" estraverse "^4.1.1"
eslint-utils@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.3.1.tgz#9a851ba89ee7c460346f97cf8939c7298827e512"
eslint-visitor-keys@^1.0.0: eslint-visitor-keys@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d"
@ -1899,9 +1985,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.0.1: eslint@^5.1.0:
version "5.0.1" version "5.1.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.0.1.tgz#109b90ab7f7a736f54e0f341c8bb9d09777494c3" resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.1.0.tgz#2ed611f1ce163c0fb99e1e0cda5af8f662dff645"
dependencies: dependencies:
ajv "^6.5.0" ajv "^6.5.0"
babel-code-frame "^6.26.0" babel-code-frame "^6.26.0"
@ -1910,6 +1996,7 @@ eslint@^5.0.1:
debug "^3.1.0" debug "^3.1.0"
doctrine "^2.1.0" doctrine "^2.1.0"
eslint-scope "^4.0.0" eslint-scope "^4.0.0"
eslint-utils "^1.3.1"
eslint-visitor-keys "^1.0.0" eslint-visitor-keys "^1.0.0"
espree "^4.0.0" espree "^4.0.0"
esquery "^1.0.1" esquery "^1.0.1"
@ -1917,7 +2004,7 @@ eslint@^5.0.1:
file-entry-cache "^2.0.0" file-entry-cache "^2.0.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.5.0" globals "^11.7.0"
ignore "^3.3.3" ignore "^3.3.3"
imurmurhash "^0.1.4" imurmurhash "^0.1.4"
inquirer "^5.2.0" inquirer "^5.2.0"
@ -2513,7 +2600,7 @@ globals@^11.0.1:
version "11.3.0" version "11.3.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-11.3.0.tgz#e04fdb7b9796d8adac9c8f64c14837b2313378b0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.3.0.tgz#e04fdb7b9796d8adac9c8f64c14837b2313378b0"
globals@^11.5.0: globals@^11.1.0, globals@^11.7.0:
version "11.7.0" version "11.7.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-11.7.0.tgz#a583faa43055b1aca771914bf68258e2fc125673" resolved "https://registry.yarnpkg.com/globals/-/globals-11.7.0.tgz#a583faa43055b1aca771914bf68258e2fc125673"
@ -3122,7 +3209,7 @@ interpret@^1.0.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614"
invariant@^2.2.2: invariant@^2.2.0, invariant@^2.2.2:
version "2.2.4" version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
dependencies: dependencies:
@ -3528,6 +3615,10 @@ jsesc@^1.3.0:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b"
jsesc@^2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.1.tgz#e421a2a8e20d6b0819df28908f782526b96dd1fe"
jsesc@~0.5.0: jsesc@~0.5.0:
version "0.5.0" version "0.5.0"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
@ -3947,7 +4038,7 @@ lodash@>=3.10.0, lodash@^4.0.0, lodash@^4.14.0, lodash@^4.17.4, lodash@^4.3.0, l
version "4.17.5" version "4.17.5"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
lodash@^4.17.10, lodash@^4.17.5: lodash@^4.17.10, lodash@^4.17.5, lodash@^4.2.0:
version "4.17.10" version "4.17.10"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
@ -5092,9 +5183,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.2: raven-js@^3.26.3:
version "3.26.2" version "3.26.3"
resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.26.2.tgz#9153af2416e96ccf4e0b9cbc6c90c34dda0d7e88" resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.26.3.tgz#0efb49969b5b11ab965f7b0d6da4ca102b763cb0"
rc@^1.0.1, rc@^1.1.6: rc@^1.0.1, rc@^1.1.6:
version "1.2.6" version "1.2.6"
@ -5543,9 +5634,9 @@ rimraf@2, rimraf@^2.2.8:
dependencies: dependencies:
glob "^7.0.5" glob "^7.0.5"
rollup-plugin-babel@^3.0.5: rollup-plugin-babel@^3.0.7:
version "3.0.5" version "3.0.7"
resolved "https://registry.yarnpkg.com/rollup-plugin-babel/-/rollup-plugin-babel-3.0.5.tgz#9769a7977098da1dce5b5888fe38dfd8666bf08d" resolved "https://registry.yarnpkg.com/rollup-plugin-babel/-/rollup-plugin-babel-3.0.7.tgz#5b13611f1ab8922497e9d15197ae5d8a23fe3b1e"
dependencies: dependencies:
rollup-pluginutils "^1.5.0" rollup-pluginutils "^1.5.0"
@ -5791,7 +5882,7 @@ source-map-url@~0.3.0:
version "0.3.0" version "0.3.0"
resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.3.0.tgz#7ecaf13b57bcd09da8a40c5d269db33799d4aaf9" resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.3.0.tgz#7ecaf13b57bcd09da8a40c5d269db33799d4aaf9"
source-map@0.5.x, source-map@^0.5.1, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7: source-map@0.5.x, source-map@^0.5.0, source-map@^0.5.1, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7:
version "0.5.7" version "0.5.7"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
@ -6389,6 +6480,10 @@ to-fast-properties@^1.0.3:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47"
to-fast-properties@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
to-object-path@^0.3.0: to-object-path@^0.3.0:
version "0.3.0" version "0.3.0"
resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af"