Merge pull request #1142 from sampotts/a11y-improvements
A11y improvements
This commit is contained in:
commit
18b4d26bee
2
.gitignore
vendored
2
.gitignore
vendored
@ -8,4 +8,4 @@ npm-debug.log
|
||||
yarn-error.log
|
||||
package-lock.json
|
||||
*.webm
|
||||
.idea/
|
||||
.idea/
|
||||
|
@ -2,5 +2,6 @@
|
||||
"useTabs": false,
|
||||
"tabWidth": 4,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
"trailingComma": "all",
|
||||
"printWidth": 120
|
||||
}
|
||||
|
2
demo/dist/demo.css
vendored
2
demo/dist/demo.css
vendored
File diff suppressed because one or more lines are too long
58
demo/dist/demo.js
vendored
58
demo/dist/demo.js
vendored
@ -1874,7 +1874,7 @@ typeof navigator === "object" && (function () {
|
||||
// webpack (using a build step causes webpack #1617). Grunt verifies that
|
||||
// this value matches package.json during build.
|
||||
// See: https://github.com/getsentry/raven-js/issues/465
|
||||
VERSION: '3.26.3',
|
||||
VERSION: '3.26.4',
|
||||
|
||||
debug: false,
|
||||
|
||||
@ -2612,34 +2612,40 @@ typeof navigator === "object" && (function () {
|
||||
)
|
||||
return;
|
||||
|
||||
options = options || {};
|
||||
options = Object.assign(
|
||||
{
|
||||
eventId: this.lastEventId(),
|
||||
dsn: this._dsn,
|
||||
user: this._globalContext.user || {}
|
||||
},
|
||||
options
|
||||
);
|
||||
|
||||
var lastEventId = options.eventId || this.lastEventId();
|
||||
if (!lastEventId) {
|
||||
if (!options.eventId) {
|
||||
throw new configError('Missing eventId');
|
||||
}
|
||||
|
||||
var dsn = options.dsn || this._dsn;
|
||||
if (!dsn) {
|
||||
if (!options.dsn) {
|
||||
throw new configError('Missing DSN');
|
||||
}
|
||||
|
||||
var encode = encodeURIComponent;
|
||||
var qs = '';
|
||||
qs += '?eventId=' + encode(lastEventId);
|
||||
qs += '&dsn=' + encode(dsn);
|
||||
var encodedOptions = [];
|
||||
|
||||
var user = options.user || this._globalContext.user;
|
||||
if (user) {
|
||||
if (user.name) qs += '&name=' + encode(user.name);
|
||||
if (user.email) qs += '&email=' + encode(user.email);
|
||||
for (var key in options) {
|
||||
if (key === 'user') {
|
||||
var user = options.user;
|
||||
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');
|
||||
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);
|
||||
},
|
||||
|
||||
@ -4097,6 +4103,9 @@ typeof navigator === "object" && (function () {
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
singleton.context(function () {
|
||||
var selector = '#player';
|
||||
var container = document.getElementById('container');
|
||||
|
||||
if (window.shr) {
|
||||
window.shr.setup({
|
||||
count: {
|
||||
@ -4110,6 +4119,9 @@ typeof navigator === "object" && (function () {
|
||||
|
||||
// Remove class on blur
|
||||
document.addEventListener('focusout', function (event) {
|
||||
if (container.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
event.target.classList.remove(tabClassName);
|
||||
});
|
||||
|
||||
@ -4122,12 +4134,18 @@ typeof navigator === "object" && (function () {
|
||||
// Delay the adding of classname until the focus has changed
|
||||
// This event fires before the focusin event
|
||||
setTimeout(function () {
|
||||
document.activeElement.classList.add(tabClassName);
|
||||
}, 0);
|
||||
var focused = document.activeElement;
|
||||
|
||||
if (!focused || container.contains(focused)) {
|
||||
return;
|
||||
}
|
||||
|
||||
focused.classList.add(tabClassName);
|
||||
}, 10);
|
||||
});
|
||||
|
||||
// Setup the player
|
||||
var player = new Plyr('#player', {
|
||||
var player = new Plyr(selector, {
|
||||
debug: true,
|
||||
title: 'View From A Blue Moon',
|
||||
iconUrl: '../dist/plyr.svg',
|
||||
@ -4137,7 +4155,7 @@ typeof navigator === "object" && (function () {
|
||||
tooltips: {
|
||||
controls: true
|
||||
},
|
||||
clickToPlay: false,
|
||||
// clickToPlay: false,
|
||||
/* controls: [
|
||||
'play-large',
|
||||
'restart',
|
||||
|
2
demo/dist/demo.js.map
vendored
2
demo/dist/demo.js.map
vendored
File diff suppressed because one or more lines are too long
2
demo/dist/demo.min.js
vendored
2
demo/dist/demo.min.js
vendored
File diff suppressed because one or more lines are too long
2
demo/dist/demo.min.js.map
vendored
2
demo/dist/demo.min.js.map
vendored
File diff suppressed because one or more lines are too long
2
demo/dist/error.css
vendored
2
demo/dist/error.css
vendored
File diff suppressed because one or more lines are too long
@ -91,21 +91,22 @@
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<video controls crossorigin playsinline poster="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg" id="player">
|
||||
<!-- Video files -->
|
||||
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" type="video/mp4" size="576">
|
||||
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4" type="video/mp4" size="720">
|
||||
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4" type="video/mp4" size="1080">
|
||||
<!-- <source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1440p.mp4" type="video/mp4" size="1440"> -->
|
||||
<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 files -->
|
||||
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" type="video/mp4" size="576">
|
||||
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4" type="video/mp4" size="720">
|
||||
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4" type="video/mp4" size="1080">
|
||||
|
||||
<!-- 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"
|
||||
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">
|
||||
<!-- 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"
|
||||
default>
|
||||
<track kind="captions" label="Français" srclang="fr" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt">
|
||||
|
||||
<!-- Fallback for browsers that don't support the <video> element -->
|
||||
<a href="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" download>Download</a>
|
||||
</video>
|
||||
<!-- Fallback for browsers that don't support the <video> element -->
|
||||
<a href="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" download>Download</a>
|
||||
</video>
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
<li class="plyr__cite plyr__cite--video" hidden>
|
||||
@ -166,7 +167,7 @@
|
||||
</svg>
|
||||
<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.&url=http%3A%2F%2Fplyr.io&via=Sam_Potts"
|
||||
target="_blank" data-shr-network="twitter">tweet it</a>
|
||||
target="_blank" data-shr-network="twitter">tweet it</a> 👍
|
||||
</p>
|
||||
</aside>
|
||||
|
||||
|
@ -7,16 +7,17 @@
|
||||
import Raven from 'raven-js';
|
||||
|
||||
(() => {
|
||||
const isLive = window.location.host === 'plyr.io';
|
||||
|
||||
// Raven / Sentry
|
||||
// For demo site (https://plyr.io) only
|
||||
if (isLive) {
|
||||
Raven.config('https://d4ad9866ad834437a4754e23937071e4@sentry.io/305555').install();
|
||||
}
|
||||
const { host } = window.location;
|
||||
const env = {
|
||||
prod: host === 'plyr.io',
|
||||
dev: host === 'dev.plyr.io',
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
Raven.context(() => {
|
||||
const selector = '#player';
|
||||
const container = document.getElementById('container');
|
||||
|
||||
if (window.shr) {
|
||||
window.shr.setup({
|
||||
count: {
|
||||
@ -30,6 +31,9 @@ import Raven from 'raven-js';
|
||||
|
||||
// Remove class on blur
|
||||
document.addEventListener('focusout', event => {
|
||||
if (container.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
event.target.classList.remove(tabClassName);
|
||||
});
|
||||
|
||||
@ -42,12 +46,18 @@ import Raven from 'raven-js';
|
||||
// Delay the adding of classname until the focus has changed
|
||||
// This event fires before the focusin event
|
||||
setTimeout(() => {
|
||||
document.activeElement.classList.add(tabClassName);
|
||||
}, 0);
|
||||
const focused = document.activeElement;
|
||||
|
||||
if (!focused || container.contains(focused)) {
|
||||
return;
|
||||
}
|
||||
|
||||
focused.classList.add(tabClassName);
|
||||
}, 10);
|
||||
});
|
||||
|
||||
// Setup the player
|
||||
const player = new Plyr('#player', {
|
||||
const player = new Plyr(selector, {
|
||||
debug: true,
|
||||
title: 'View From A Blue Moon',
|
||||
iconUrl: '../dist/plyr.svg',
|
||||
@ -57,57 +67,6 @@ import Raven from 'raven-js';
|
||||
tooltips: {
|
||||
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: {
|
||||
active: true,
|
||||
},
|
||||
@ -115,7 +74,7 @@ import Raven from 'raven-js';
|
||||
google: 'AIzaSyDrNwtN3nLH_8rjCmu5Wq3ZCm4MNAVdc0c',
|
||||
},
|
||||
ads: {
|
||||
enabled: true,
|
||||
enabled: env.prod || env.dev,
|
||||
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
|
||||
// For demo site (https://plyr.io) only
|
||||
/* eslint-disable */
|
||||
if (isLive) {
|
||||
(function(i, s, o, g, r, a, m) {
|
||||
if (env.prod) {
|
||||
((i, s, o, g, r, a, m) => {
|
||||
i.GoogleAnalyticsObject = r;
|
||||
i[r] =
|
||||
i[r] ||
|
||||
|
@ -2,7 +2,8 @@
|
||||
// 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-small: 13;
|
||||
|
2
dist/plyr.css
vendored
2
dist/plyr.css
vendored
File diff suppressed because one or more lines are too long
1385
dist/plyr.js
vendored
1385
dist/plyr.js
vendored
File diff suppressed because it is too large
Load Diff
2
dist/plyr.js.map
vendored
2
dist/plyr.js.map
vendored
File diff suppressed because one or more lines are too long
2
dist/plyr.min.js
vendored
2
dist/plyr.min.js
vendored
File diff suppressed because one or more lines are too long
2
dist/plyr.min.js.map
vendored
2
dist/plyr.min.js.map
vendored
File diff suppressed because one or more lines are too long
1391
dist/plyr.polyfilled.js
vendored
1391
dist/plyr.polyfilled.js
vendored
File diff suppressed because it is too large
Load Diff
2
dist/plyr.polyfilled.js.map
vendored
2
dist/plyr.polyfilled.js.map
vendored
File diff suppressed because one or more lines are too long
2
dist/plyr.polyfilled.min.js
vendored
2
dist/plyr.polyfilled.min.js
vendored
File diff suppressed because one or more lines are too long
2
dist/plyr.polyfilled.min.js.map
vendored
2
dist/plyr.polyfilled.min.js.map
vendored
File diff suppressed because one or more lines are too long
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "plyr",
|
||||
"version": "3.3.23",
|
||||
"version": "3.4.0-beta.1",
|
||||
"description":
|
||||
"A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
|
||||
"homepage": "https://plyr.io",
|
||||
|
@ -162,9 +162,9 @@ reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.3.23
|
||||
|
||||
## 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
|
||||
* Enable ads in the [config options](#options) and enter your publisher ID
|
||||
|
||||
|
@ -84,7 +84,9 @@ const captions = {
|
||||
// * toggled: The real captions state
|
||||
|
||||
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();
|
||||
|
@ -354,6 +354,9 @@ const defaults = {
|
||||
isTouch: 'plyr--is-touch',
|
||||
uiSupported: 'plyr--full-ui',
|
||||
noTransition: 'plyr--no-transition',
|
||||
display: {
|
||||
time: 'plyr__time',
|
||||
},
|
||||
menu: {
|
||||
value: 'plyr__menu__value',
|
||||
badge: 'plyr__badge',
|
||||
|
662
src/js/controls.js
vendored
662
src/js/controls.js
vendored
@ -1,5 +1,6 @@
|
||||
// ==========================================================================
|
||||
// Plyr controls
|
||||
// TODO: This needs to be split into smaller files and cleaned up
|
||||
// ==========================================================================
|
||||
|
||||
import captions from './captions';
|
||||
@ -9,19 +10,7 @@ import support from './support';
|
||||
import { repaint, transitionEndEvent } from './utils/animation';
|
||||
import { dedupe } from './utils/arrays';
|
||||
import browser from './utils/browser';
|
||||
import {
|
||||
createElement,
|
||||
emptyElement,
|
||||
getAttributesFromSelector,
|
||||
getElement,
|
||||
getElements,
|
||||
hasClass,
|
||||
matches,
|
||||
removeElement,
|
||||
setAttributes,
|
||||
toggleClass,
|
||||
toggleHidden,
|
||||
} from './utils/elements';
|
||||
import { createElement, emptyElement, getAttributesFromSelector, getElement, getElements, hasClass, matches, removeElement, setAttributes, setFocus, toggleClass, toggleHidden } from './utils/elements';
|
||||
import { off, on } from './utils/events';
|
||||
import is from './utils/is';
|
||||
import loadSprite from './utils/loadSprite';
|
||||
@ -243,12 +232,28 @@ const controls = {
|
||||
// Setup toggle icon and labels
|
||||
if (toggle) {
|
||||
// Icon
|
||||
button.appendChild(controls.createIcon.call(this, iconPressed, { class: 'icon--pressed' }));
|
||||
button.appendChild(controls.createIcon.call(this, icon, { class: 'icon--not-pressed' }));
|
||||
button.appendChild(
|
||||
controls.createIcon.call(this, iconPressed, {
|
||||
class: 'icon--pressed',
|
||||
}),
|
||||
);
|
||||
button.appendChild(
|
||||
controls.createIcon.call(this, icon, {
|
||||
class: 'icon--not-pressed',
|
||||
}),
|
||||
);
|
||||
|
||||
// Label/Tooltip
|
||||
button.appendChild(controls.createLabel.call(this, labelPressed, { class: 'label--pressed' }));
|
||||
button.appendChild(controls.createLabel.call(this, label, { class: 'label--not-pressed' }));
|
||||
button.appendChild(
|
||||
controls.createLabel.call(this, labelPressed, {
|
||||
class: 'label--pressed',
|
||||
}),
|
||||
);
|
||||
button.appendChild(
|
||||
controls.createLabel.call(this, label, {
|
||||
class: 'label--not-pressed',
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
button.appendChild(controls.createIcon.call(this, icon));
|
||||
button.appendChild(controls.createLabel.call(this, label));
|
||||
@ -360,7 +365,7 @@ const controls = {
|
||||
const container = createElement(
|
||||
'div',
|
||||
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),
|
||||
}),
|
||||
'00:00',
|
||||
@ -372,37 +377,143 @@ const controls = {
|
||||
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
|
||||
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', {
|
||||
class: this.config.classNames.control,
|
||||
});
|
||||
|
||||
const radio = createElement(
|
||||
'input',
|
||||
extend(getAttributesFromSelector(this.config.selectors.inputs[type]), {
|
||||
type: 'radio',
|
||||
name: `plyr-${type}`,
|
||||
const menuItem = createElement(
|
||||
'button',
|
||||
extend(attributes, {
|
||||
type: 'button',
|
||||
role: 'menuitemradio',
|
||||
class: `${this.config.classNames.control} ${attributes.class ? attributes.class : ''}`.trim(),
|
||||
'aria-checked': checked,
|
||||
value,
|
||||
checked,
|
||||
class: 'plyr__sr-only',
|
||||
}),
|
||||
);
|
||||
|
||||
const faux = createElement('span', { hidden: '' });
|
||||
const flex = createElement('span');
|
||||
|
||||
label.appendChild(radio);
|
||||
label.appendChild(faux);
|
||||
label.insertAdjacentHTML('beforeend', title);
|
||||
// We have to set as HTML incase of special characters
|
||||
flex.innerHTML = title;
|
||||
|
||||
if (is.element(badge)) {
|
||||
label.appendChild(badge);
|
||||
flex.appendChild(badge);
|
||||
}
|
||||
|
||||
item.appendChild(label);
|
||||
list.appendChild(item);
|
||||
menuItem.appendChild(flex);
|
||||
|
||||
// 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
|
||||
@ -637,7 +748,7 @@ const controls = {
|
||||
// https://github.com/video-dev/hls.js/blob/5820d29d3c4c8a46e8b75f1e3afa3e68c1a9a2db/src/controller/buffer-controller.js#L415
|
||||
// https://github.com/google/shaka-player/blob/4d889054631f4e1cf0fbd80ddd2b71887c02e232/lib/media/streaming_engine.js#L1062
|
||||
// https://github.com/Dash-Industry-Forum/dash.js/blob/69859f51b969645b234666800d4cb596d89c602d/src/dash/models/DashManifestModel.js#L338
|
||||
if (this.duration >= 2**32) {
|
||||
if (this.duration >= 2 ** 32) {
|
||||
toggleHidden(this.elements.display.currentTime, true);
|
||||
toggleHidden(this.elements.progress, true);
|
||||
return;
|
||||
@ -666,19 +777,97 @@ const controls = {
|
||||
},
|
||||
|
||||
// Hide/show a tab
|
||||
toggleTab(setting, toggle) {
|
||||
toggleHidden(this.elements.settings.tabs[setting], !toggle);
|
||||
toggleMenuButton(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}×`;
|
||||
|
||||
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
|
||||
setQualityMenu(options) {
|
||||
// Menu required
|
||||
if (!is.element(this.elements.settings.panes.quality)) {
|
||||
if (!is.element(this.elements.settings.panels.quality)) {
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
if (is.array(options)) {
|
||||
@ -687,7 +876,10 @@ const controls = {
|
||||
|
||||
// Toggle the pane and tab
|
||||
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
|
||||
controls.checkMenu.call(this);
|
||||
@ -697,9 +889,6 @@ const controls = {
|
||||
return;
|
||||
}
|
||||
|
||||
// Empty the menu
|
||||
emptyElement(list);
|
||||
|
||||
// Get the badge HTML for HD, 4K etc
|
||||
const getBadge = quality => {
|
||||
const label = i18n.get(`qualityBadge.${quality}`, this.config);
|
||||
@ -730,101 +919,23 @@ const controls = {
|
||||
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}×`;
|
||||
|
||||
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
|
||||
/* setLoopMenu() {
|
||||
// Menu required
|
||||
if (!is.element(this.elements.settings.panes.loop)) {
|
||||
if (!is.element(this.elements.settings.panels.loop)) {
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
toggleHidden(this.elements.settings.tabs.loop, false);
|
||||
toggleHidden(this.elements.settings.panes.loop, false);
|
||||
toggleHidden(this.elements.settings.buttons.loop, false);
|
||||
toggleHidden(this.elements.settings.panels.loop, false);
|
||||
|
||||
// Toggle the pane and tab
|
||||
const toggle = !is.empty(this.loop.options);
|
||||
controls.toggleTab.call(this, 'loop', toggle);
|
||||
controls.toggleMenuButton.call(this, 'loop', toggle);
|
||||
|
||||
// Empty the menu
|
||||
emptyElement(list);
|
||||
@ -857,13 +968,19 @@ const controls = {
|
||||
|
||||
// Set a list of available captions languages
|
||||
setCaptionsMenu() {
|
||||
// Menu required
|
||||
if (!is.element(this.elements.settings.panels.captions)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Captions or language? Currently it's mixed
|
||||
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 toggle = Boolean(tracks.length);
|
||||
|
||||
// Toggle the pane and tab
|
||||
controls.toggleTab.call(this, type, tracks.length);
|
||||
controls.toggleMenuButton.call(this, type, toggle);
|
||||
|
||||
// Empty the menu
|
||||
emptyElement(list);
|
||||
@ -872,7 +989,7 @@ const controls = {
|
||||
controls.checkMenu.call(this);
|
||||
|
||||
// If there's no captions, bail
|
||||
if (!tracks.length) {
|
||||
if (!toggle) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -903,17 +1020,13 @@ const controls = {
|
||||
|
||||
// Set a list of available captions languages
|
||||
setSpeedMenu(options) {
|
||||
// Do nothing if not selected
|
||||
if (!this.config.controls.includes('settings') || !this.config.settings.includes('speed')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Menu required
|
||||
if (!is.element(this.elements.settings.panes.speed)) {
|
||||
if (!is.element(this.elements.settings.panels.speed)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const type = 'speed';
|
||||
const list = this.elements.settings.panels.speed.querySelector('[role="menu"]');
|
||||
|
||||
// Set the speed options
|
||||
if (is.array(options)) {
|
||||
@ -927,7 +1040,10 @@ const controls = {
|
||||
|
||||
// Toggle the pane and tab
|
||||
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
|
||||
controls.checkMenu.call(this);
|
||||
@ -937,12 +1053,6 @@ const controls = {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the list to populate
|
||||
const list = this.elements.settings.panes.speed.querySelector('ul');
|
||||
|
||||
// Empty the menu
|
||||
emptyElement(list);
|
||||
|
||||
// Create items
|
||||
this.options.speed.forEach(speed => {
|
||||
controls.createMenuItem.call(this, {
|
||||
@ -958,29 +1068,35 @@ const controls = {
|
||||
|
||||
// Check if we need to hide/show the settings menu
|
||||
checkMenu() {
|
||||
const { tabs } = this.elements.settings;
|
||||
const visible = !is.empty(tabs) && Object.values(tabs).some(tab => !tab.hidden);
|
||||
const { buttons } = this.elements.settings;
|
||||
const visible = !is.empty(buttons) && Object.values(buttons).some(button => !button.hidden);
|
||||
|
||||
toggleHidden(this.elements.settings.menu, !visible);
|
||||
},
|
||||
|
||||
// Show/hide menu
|
||||
toggleMenu(event) {
|
||||
const { form } = this.elements.settings;
|
||||
toggleMenu(input) {
|
||||
const { popup } = this.elements.settings;
|
||||
const button = this.elements.buttons.settings;
|
||||
|
||||
// Menu and button are required
|
||||
if (!is.element(form) || !is.element(button)) {
|
||||
if (!is.element(popup) || !is.element(button)) {
|
||||
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)) {
|
||||
const isMenuItem = is.element(form) && form.contains(event.target);
|
||||
const isButton = event.target === this.elements.buttons.settings;
|
||||
if (is.boolean(input)) {
|
||||
show = input;
|
||||
} else if (is.keyboardEvent(input) && input.which === 27) {
|
||||
show = false;
|
||||
} else if (is.event(input)) {
|
||||
const isMenuItem = popup.contains(input.target);
|
||||
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
|
||||
// show the menu (a doc click shouldn't show the menu)
|
||||
if (isMenuItem || (!isMenuItem && !isButton && show)) {
|
||||
@ -989,40 +1105,38 @@ const controls = {
|
||||
|
||||
// Prevent the toggle being caught by the doc listener
|
||||
if (isButton) {
|
||||
event.stopPropagation();
|
||||
input.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
// Set form and button attributes
|
||||
if (is.element(button)) {
|
||||
button.setAttribute('aria-expanded', show);
|
||||
// Set button attributes
|
||||
button.setAttribute('aria-expanded', show);
|
||||
|
||||
// Show the actual popup
|
||||
toggleHidden(popup, !show);
|
||||
|
||||
// Add class hook
|
||||
toggleClass(this.elements.container, this.config.classNames.menu.open, show);
|
||||
|
||||
// Focus the first item if key interaction
|
||||
if (show && is.keyboardEvent(input)) {
|
||||
const pane = Object.values(this.elements.settings.panels).find(pane => !pane.hidden);
|
||||
const firstItem = pane.querySelector('[role^="menuitem"]');
|
||||
setFocus.call(this, firstItem, true);
|
||||
}
|
||||
|
||||
if (is.element(form)) {
|
||||
toggleHidden(form, !show);
|
||||
toggleClass(this.elements.container, this.config.classNames.menu.open, show);
|
||||
|
||||
if (show) {
|
||||
form.removeAttribute('tabindex');
|
||||
} else {
|
||||
form.setAttribute('tabindex', -1);
|
||||
}
|
||||
// If closing, re-focus the button
|
||||
else if (!show && !hidden) {
|
||||
setFocus.call(this, button, is.keyboardEvent(input));
|
||||
}
|
||||
},
|
||||
|
||||
// Get the natural size of a tab
|
||||
getTabSize(tab) {
|
||||
// Get the natural size of a menu panel
|
||||
getMenuSize(tab) {
|
||||
const clone = tab.cloneNode(true);
|
||||
clone.style.position = 'absolute';
|
||||
clone.style.opacity = 0;
|
||||
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
|
||||
tab.parentNode.appendChild(clone);
|
||||
|
||||
@ -1039,31 +1153,18 @@ const controls = {
|
||||
};
|
||||
},
|
||||
|
||||
// Toggle Menu
|
||||
showTab(target = '') {
|
||||
const { menu } = this.elements.settings;
|
||||
const pane = document.getElementById(target);
|
||||
// Show a panel in the menu
|
||||
showMenuPanel(type = '', tabFocus = false) {
|
||||
const target = document.getElementById(`plyr-settings-${this.id}-${type}`);
|
||||
|
||||
// Nothing to show, bail
|
||||
if (!is.element(pane)) {
|
||||
if (!is.element(target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Are we targeting a tab? If not, bail
|
||||
const isTab = pane.getAttribute('role') === 'tabpanel';
|
||||
if (!isTab) {
|
||||
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);
|
||||
});
|
||||
// Hide all other panels
|
||||
const container = target.parentNode;
|
||||
const current = Array.from(container.children).find(node => !node.hidden);
|
||||
|
||||
// If we can do fancy animations, we'll animate the height/width
|
||||
if (support.transitions && !support.reducedMotion) {
|
||||
@ -1072,12 +1173,12 @@ const controls = {
|
||||
container.style.height = `${current.scrollHeight}px`;
|
||||
|
||||
// Get potential sizes
|
||||
const size = controls.getTabSize.call(this, pane);
|
||||
const size = controls.getMenuSize.call(this, target);
|
||||
|
||||
// Restore auto height/width
|
||||
const restore = e => {
|
||||
const restore = event => {
|
||||
// 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;
|
||||
}
|
||||
|
||||
@ -1099,19 +1200,13 @@ const controls = {
|
||||
|
||||
// Set attributes on current tab
|
||||
toggleHidden(current, true);
|
||||
current.setAttribute('tabindex', -1);
|
||||
|
||||
// Set attributes on target
|
||||
toggleHidden(pane, false);
|
||||
|
||||
const tabs = getElements.call(this, `[aria-controls="${target}"]`);
|
||||
Array.from(tabs).forEach(tab => {
|
||||
tab.setAttribute('aria-expanded', true);
|
||||
});
|
||||
pane.removeAttribute('tabindex');
|
||||
toggleHidden(target, false);
|
||||
|
||||
// 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
|
||||
@ -1225,12 +1320,12 @@ const controls = {
|
||||
|
||||
// Settings button / menu
|
||||
if (this.config.controls.includes('settings') && !is.empty(this.config.settings)) {
|
||||
const menu = createElement('div', {
|
||||
const control = createElement('div', {
|
||||
class: 'plyr__menu',
|
||||
hidden: '',
|
||||
});
|
||||
|
||||
menu.appendChild(
|
||||
control.appendChild(
|
||||
controls.createButton.call(this, 'settings', {
|
||||
id: `plyr-settings-toggle-${data.id}`,
|
||||
'aria-haspopup': true,
|
||||
@ -1239,48 +1334,52 @@ const controls = {
|
||||
}),
|
||||
);
|
||||
|
||||
const form = createElement('form', {
|
||||
const popup = createElement('div', {
|
||||
class: 'plyr__menu__container',
|
||||
id: `plyr-settings-${data.id}`,
|
||||
hidden: '',
|
||||
'aria-labelled-by': `plyr-settings-toggle-${data.id}`,
|
||||
role: 'tablist',
|
||||
tabindex: -1,
|
||||
});
|
||||
|
||||
const inner = createElement('div');
|
||||
|
||||
const home = createElement('div', {
|
||||
id: `plyr-settings-${data.id}-home`,
|
||||
'aria-labelled-by': `plyr-settings-toggle-${data.id}`,
|
||||
role: 'tabpanel',
|
||||
});
|
||||
|
||||
// Create the tab list
|
||||
const tabs = createElement('ul', {
|
||||
role: 'tablist',
|
||||
// Create the menu
|
||||
const menu = createElement('div', {
|
||||
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 => {
|
||||
const tab = createElement('li', {
|
||||
role: 'tab',
|
||||
hidden: '',
|
||||
});
|
||||
|
||||
const button = createElement(
|
||||
// TODO: bundle this with the createMenuItem helper and bindings
|
||||
const menuItem = createElement(
|
||||
'button',
|
||||
extend(getAttributesFromSelector(this.config.selectors.buttons.settings), {
|
||||
type: 'button',
|
||||
class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`,
|
||||
id: `plyr-settings-${data.id}-${type}-tab`,
|
||||
role: 'menuitem',
|
||||
'aria-haspopup': true,
|
||||
'aria-controls': `plyr-settings-${data.id}-${type}`,
|
||||
'aria-expanded': false,
|
||||
hidden: '',
|
||||
}),
|
||||
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', {
|
||||
class: this.config.classNames.menu.value,
|
||||
});
|
||||
@ -1288,54 +1387,91 @@ const controls = {
|
||||
// Speed contains HTML entities
|
||||
value.innerHTML = data[type];
|
||||
|
||||
button.appendChild(value);
|
||||
tab.appendChild(button);
|
||||
tabs.appendChild(tab);
|
||||
flex.appendChild(value);
|
||||
menuItem.appendChild(flex);
|
||||
menu.appendChild(menuItem);
|
||||
|
||||
this.elements.settings.tabs[type] = tab;
|
||||
});
|
||||
|
||||
home.appendChild(tabs);
|
||||
inner.appendChild(home);
|
||||
|
||||
// Build the panes
|
||||
this.config.settings.forEach(type => {
|
||||
// Build the panes
|
||||
const pane = createElement('div', {
|
||||
id: `plyr-settings-${data.id}-${type}`,
|
||||
hidden: '',
|
||||
'aria-labelled-by': `plyr-settings-${data.id}-${type}-tab`,
|
||||
role: 'tabpanel',
|
||||
tabindex: -1,
|
||||
});
|
||||
|
||||
const back = createElement(
|
||||
'button',
|
||||
{
|
||||
type: 'button',
|
||||
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),
|
||||
// Back button
|
||||
const backButton = createElement('button', {
|
||||
type: 'button',
|
||||
class: `${this.config.classNames.control} ${this.config.classNames.control}--back`,
|
||||
});
|
||||
|
||||
// Visible label
|
||||
backButton.appendChild(
|
||||
createElement(
|
||||
'span',
|
||||
{
|
||||
'aria-hidden': true,
|
||||
},
|
||||
i18n.get(type, this.config),
|
||||
),
|
||||
);
|
||||
|
||||
pane.appendChild(back);
|
||||
// Screen reader label
|
||||
backButton.appendChild(
|
||||
createElement(
|
||||
'span',
|
||||
{
|
||||
class: this.config.classNames.hidden,
|
||||
},
|
||||
i18n.get('menuBack', this.config),
|
||||
),
|
||||
);
|
||||
|
||||
const options = createElement('ul');
|
||||
// Go back via keyboard
|
||||
on(
|
||||
pane,
|
||||
'keydown',
|
||||
event => {
|
||||
// We only care about <-
|
||||
if (event.which !== 37) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent seek
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// Show the respective menu
|
||||
controls.showMenuPanel.call(this, 'home', true);
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
// Go back via button click
|
||||
on(backButton, 'click', () => {
|
||||
controls.showMenuPanel.call(this, 'home', false);
|
||||
});
|
||||
|
||||
// Add to pane
|
||||
pane.appendChild(backButton);
|
||||
|
||||
// Menu
|
||||
pane.appendChild(
|
||||
createElement('div', {
|
||||
role: 'menu',
|
||||
}),
|
||||
);
|
||||
|
||||
pane.appendChild(options);
|
||||
inner.appendChild(pane);
|
||||
|
||||
this.elements.settings.panes[type] = pane;
|
||||
this.elements.settings.buttons[type] = menuItem;
|
||||
this.elements.settings.panels[type] = pane;
|
||||
});
|
||||
|
||||
form.appendChild(inner);
|
||||
menu.appendChild(form);
|
||||
container.appendChild(menu);
|
||||
popup.appendChild(inner);
|
||||
control.appendChild(popup);
|
||||
container.appendChild(control);
|
||||
|
||||
this.elements.settings.form = form;
|
||||
this.elements.settings.menu = menu;
|
||||
this.elements.settings.popup = popup;
|
||||
this.elements.settings.menu = control;
|
||||
}
|
||||
|
||||
// Picture in picture button
|
||||
|
@ -4,8 +4,9 @@
|
||||
|
||||
import controls from './controls';
|
||||
import ui from './ui';
|
||||
import { repaint } from './utils/animation';
|
||||
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 is from './utils/is';
|
||||
|
||||
@ -13,14 +14,19 @@ class Listeners {
|
||||
constructor(player) {
|
||||
this.player = player;
|
||||
this.lastKey = null;
|
||||
this.focusTimer = null;
|
||||
this.lastKeyDown = null;
|
||||
|
||||
this.handleKey = this.handleKey.bind(this);
|
||||
this.toggleMenu = this.toggleMenu.bind(this);
|
||||
this.setTabFocus = this.setTabFocus.bind(this);
|
||||
this.firstTouch = this.firstTouch.bind(this);
|
||||
}
|
||||
|
||||
// Handle key presses
|
||||
handleKey(event) {
|
||||
const { player } = this;
|
||||
const { elements } = player;
|
||||
const code = event.keyCode ? event.keyCode : event.which;
|
||||
const pressed = event.type === 'keydown';
|
||||
const repeat = pressed && code === this.lastKey;
|
||||
@ -39,27 +45,32 @@ class Listeners {
|
||||
// Seek by the number keys
|
||||
const seekByKey = () => {
|
||||
// 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
|
||||
// Reset on keyup
|
||||
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
|
||||
// and if the focused element is not editable (e.g. text input)
|
||||
// and any that accept key input http://webaim.org/techniques/keyboard/
|
||||
const focused = getFocusElement();
|
||||
if (
|
||||
is.element(focused) &&
|
||||
(focused !== this.player.elements.inputs.seek &&
|
||||
matches(focused, this.player.config.selectors.editable))
|
||||
) {
|
||||
return;
|
||||
const focused = document.activeElement;
|
||||
if (is.element(focused)) {
|
||||
const { editable } = player.config.selectors;
|
||||
const { seek } = elements.inputs;
|
||||
|
||||
if (focused !== seek && matches(focused, editable)) {
|
||||
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 (preventDefault.includes(code)) {
|
||||
event.preventDefault();
|
||||
@ -87,52 +98,52 @@ class Listeners {
|
||||
case 75:
|
||||
// Space and K key
|
||||
if (!repeat) {
|
||||
this.player.togglePlay();
|
||||
player.togglePlay();
|
||||
}
|
||||
break;
|
||||
|
||||
case 38:
|
||||
// Arrow up
|
||||
this.player.increaseVolume(0.1);
|
||||
player.increaseVolume(0.1);
|
||||
break;
|
||||
|
||||
case 40:
|
||||
// Arrow down
|
||||
this.player.decreaseVolume(0.1);
|
||||
player.decreaseVolume(0.1);
|
||||
break;
|
||||
|
||||
case 77:
|
||||
// M key
|
||||
if (!repeat) {
|
||||
this.player.muted = !this.player.muted;
|
||||
player.muted = !player.muted;
|
||||
}
|
||||
break;
|
||||
|
||||
case 39:
|
||||
// Arrow forward
|
||||
this.player.forward();
|
||||
player.forward();
|
||||
break;
|
||||
|
||||
case 37:
|
||||
// Arrow back
|
||||
this.player.rewind();
|
||||
player.rewind();
|
||||
break;
|
||||
|
||||
case 70:
|
||||
// F key
|
||||
this.player.fullscreen.toggle();
|
||||
player.fullscreen.toggle();
|
||||
break;
|
||||
|
||||
case 67:
|
||||
// C key
|
||||
if (!repeat) {
|
||||
this.player.toggleCaptions();
|
||||
player.toggleCaptions();
|
||||
}
|
||||
break;
|
||||
|
||||
case 76:
|
||||
// L key
|
||||
this.player.loop = !this.player.loop;
|
||||
player.loop = !player.loop;
|
||||
break;
|
||||
|
||||
/* case 73:
|
||||
@ -153,8 +164,8 @@ class Listeners {
|
||||
|
||||
// Escape is handle natively when in full screen
|
||||
// So we only need to worry about non native
|
||||
if (!this.player.fullscreen.enabled && this.player.fullscreen.active && code === 27) {
|
||||
this.player.fullscreen.toggle();
|
||||
if (!player.fullscreen.enabled && player.fullscreen.active && code === 27) {
|
||||
player.fullscreen.toggle();
|
||||
}
|
||||
|
||||
// Store last code for next cycle
|
||||
@ -171,58 +182,99 @@ class Listeners {
|
||||
|
||||
// Device is touch enabled
|
||||
firstTouch() {
|
||||
this.player.touch = true;
|
||||
const { player } = this;
|
||||
const { elements } = player;
|
||||
|
||||
player.touch = true;
|
||||
|
||||
// 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(toggle = true) {
|
||||
const { player } = this;
|
||||
|
||||
// Keyboard shortcuts
|
||||
if (this.player.config.keyboard.global) {
|
||||
toggleListener.call(this.player, window, 'keydown keyup', this.handleKey, toggle, false);
|
||||
if (player.config.keyboard.global) {
|
||||
toggleListener.call(player, window, 'keydown keyup', this.handleKey, toggle, false);
|
||||
}
|
||||
|
||||
// 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
|
||||
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() {
|
||||
const { player } = this;
|
||||
const { elements } = player;
|
||||
|
||||
// Keyboard shortcuts
|
||||
if (!this.player.config.keyboard.global && this.player.config.keyboard.focused) {
|
||||
on.call(this.player, this.player.elements.container, 'keydown keyup', this.handleKey, false);
|
||||
if (!player.config.keyboard.global && player.config.keyboard.focused) {
|
||||
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
|
||||
on.call(
|
||||
this.player,
|
||||
this.player.elements.container,
|
||||
player,
|
||||
elements.container,
|
||||
'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen',
|
||||
event => {
|
||||
const { controls } = this.player.elements;
|
||||
const { controls } = elements;
|
||||
|
||||
// Remove button states for fullscreen
|
||||
if (event.type === 'enterfullscreen') {
|
||||
@ -236,85 +288,83 @@ class Listeners {
|
||||
let delay = 0;
|
||||
|
||||
if (show) {
|
||||
ui.toggleControls.call(this.player, true);
|
||||
ui.toggleControls.call(player, true);
|
||||
// Use longer timeout for touch devices
|
||||
delay = this.player.touch ? 3000 : 2000;
|
||||
delay = player.touch ? 3000 : 2000;
|
||||
}
|
||||
|
||||
// Clear timer
|
||||
clearTimeout(this.player.timers.controls);
|
||||
// Timer to prevent flicker when seeking
|
||||
this.player.timers.controls = setTimeout(() => ui.toggleControls.call(this.player, false), delay);
|
||||
clearTimeout(player.timers.controls);
|
||||
|
||||
// Set new timer to prevent flicker when seeking
|
||||
player.timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Listen for media events
|
||||
media() {
|
||||
const { player } = this;
|
||||
const { elements } = player;
|
||||
|
||||
// Time change on media
|
||||
on.call(this.player, this.player.media, 'timeupdate seeking seeked', event =>
|
||||
controls.timeUpdate.call(this.player, event),
|
||||
);
|
||||
on.call(player, player.media, 'timeupdate seeking seeked', event => controls.timeUpdate.call(player, event));
|
||||
|
||||
// Display duration
|
||||
on.call(this.player, this.player.media, 'durationchange loadeddata loadedmetadata', event =>
|
||||
controls.durationUpdate.call(this.player, event),
|
||||
on.call(player, player.media, 'durationchange loadeddata loadedmetadata', event =>
|
||||
controls.durationUpdate.call(player, event),
|
||||
);
|
||||
|
||||
// Check for audio tracks on load
|
||||
// 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', () => {
|
||||
toggleHidden(this.player.elements.volume, !this.player.hasAudio);
|
||||
toggleHidden(this.player.elements.buttons.mute, !this.player.hasAudio);
|
||||
on.call(player, player.media, 'canplay', () => {
|
||||
toggleHidden(elements.volume, !player.hasAudio);
|
||||
toggleHidden(elements.buttons.mute, !player.hasAudio);
|
||||
});
|
||||
|
||||
// Handle the media finishing
|
||||
on.call(this.player, this.player.media, 'ended', () => {
|
||||
on.call(player, player.media, 'ended', () => {
|
||||
// Show poster on end
|
||||
if (this.player.isHTML5 && this.player.isVideo && this.player.config.resetOnEnd) {
|
||||
if (player.isHTML5 && player.isVideo && player.config.resetOnEnd) {
|
||||
// Restart
|
||||
this.player.restart();
|
||||
player.restart();
|
||||
}
|
||||
});
|
||||
|
||||
// Check for buffer progress
|
||||
on.call(this.player, this.player.media, 'progress playing seeking seeked', event =>
|
||||
controls.updateProgress.call(this.player, event),
|
||||
on.call(player, player.media, 'progress playing seeking seeked', event =>
|
||||
controls.updateProgress.call(player, event),
|
||||
);
|
||||
|
||||
// Handle volume changes
|
||||
on.call(this.player, this.player.media, 'volumechange', event =>
|
||||
controls.updateVolume.call(this.player, event),
|
||||
);
|
||||
on.call(player, player.media, 'volumechange', event => controls.updateVolume.call(player, event));
|
||||
|
||||
// Handle play/pause
|
||||
on.call(this.player, this.player.media, 'playing play pause ended emptied timeupdate', event =>
|
||||
ui.checkPlaying.call(this.player, event),
|
||||
on.call(player, player.media, 'playing play pause ended emptied timeupdate', event =>
|
||||
ui.checkPlaying.call(player, event),
|
||||
);
|
||||
|
||||
// Loading state
|
||||
on.call(this.player, this.player.media, 'waiting canplay seeked playing', event =>
|
||||
ui.checkLoading.call(this.player, event),
|
||||
);
|
||||
on.call(player, player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(player, event));
|
||||
|
||||
// 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
|
||||
on.call(this.player, this.player.media, 'playing', () => {
|
||||
if (!this.player.ads) {
|
||||
on.call(player, player.media, 'playing', () => {
|
||||
if (!player.ads) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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
|
||||
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
|
||||
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
|
||||
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)
|
||||
if (!is.element(wrapper)) {
|
||||
@ -322,28 +372,38 @@ class Listeners {
|
||||
}
|
||||
|
||||
// On click play, pause ore restart
|
||||
on.call(this.player, wrapper, 'click', () => {
|
||||
// Touch devices will just show controls (if we're hiding controls)
|
||||
if (this.player.config.hideControls && this.player.touch && !this.player.paused) {
|
||||
on.call(player, elements.container, 'click touchstart', event => {
|
||||
const targets = [elements.container, wrapper];
|
||||
|
||||
// Ignore if click if not container or in video wrapper
|
||||
if (!targets.includes(event.target) && !wrapper.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.player.paused) {
|
||||
this.player.play();
|
||||
} else if (this.player.ended) {
|
||||
this.player.restart();
|
||||
this.player.play();
|
||||
// First touch on touch devices will just show controls (if we're hiding controls)
|
||||
// If controls are shown then it'll toggle like a pointer device
|
||||
if (
|
||||
player.config.hideControls &&
|
||||
player.touch &&
|
||||
hasClass(elements.container, player.config.classNames.hideControls)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (player.ended) {
|
||||
player.restart();
|
||||
player.play();
|
||||
} else {
|
||||
this.player.pause();
|
||||
player.togglePlay();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Disable right click
|
||||
if (this.player.supported.ui && this.player.config.disableContextMenu) {
|
||||
if (player.supported.ui && player.config.disableContextMenu) {
|
||||
on.call(
|
||||
this.player,
|
||||
this.player.elements.wrapper,
|
||||
player,
|
||||
elements.wrapper,
|
||||
'contextmenu',
|
||||
event => {
|
||||
event.preventDefault();
|
||||
@ -353,220 +413,227 @@ class Listeners {
|
||||
}
|
||||
|
||||
// Volume change
|
||||
on.call(this.player, this.player.media, 'volumechange', () => {
|
||||
on.call(player, player.media, 'volumechange', () => {
|
||||
// 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
|
||||
on.call(this.player, this.player.media, 'ratechange', () => {
|
||||
on.call(player, player.media, 'ratechange', () => {
|
||||
// Update UI
|
||||
controls.updateSetting.call(this.player, 'speed');
|
||||
controls.updateSetting.call(player, 'speed');
|
||||
|
||||
// Save to storage
|
||||
this.player.storage.set({ speed: this.player.speed });
|
||||
player.storage.set({ speed: player.speed });
|
||||
});
|
||||
|
||||
// Quality request
|
||||
on.call(this.player, this.player.media, 'qualityrequested', event => {
|
||||
on.call(player, player.media, 'qualityrequested', event => {
|
||||
// Save to storage
|
||||
this.player.storage.set({ quality: event.detail.quality });
|
||||
player.storage.set({ quality: event.detail.quality });
|
||||
});
|
||||
|
||||
// Quality change
|
||||
on.call(this.player, this.player.media, 'qualitychange', event => {
|
||||
on.call(player, player.media, 'qualitychange', event => {
|
||||
// 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
|
||||
// Bubble up key events for Edge
|
||||
const proxyEvents = this.player.config.events.concat(['keyup', 'keydown']).join(' ');
|
||||
on.call(this.player, this.player.media, proxyEvents, event => {
|
||||
const proxyEvents = player.config.events.concat(['keyup', 'keydown']).join(' ');
|
||||
|
||||
on.call(player, player.media, proxyEvents, event => {
|
||||
let { detail = {} } = event;
|
||||
|
||||
// Get error details from media
|
||||
if (event.type === 'error') {
|
||||
detail = this.player.media.error;
|
||||
detail = player.media.error;
|
||||
}
|
||||
|
||||
triggerEvent.call(this.player, this.player.elements.container, event.type, true, detail);
|
||||
triggerEvent.call(player, elements.container, event.type, true, detail);
|
||||
});
|
||||
}
|
||||
|
||||
// Run default and custom handlers
|
||||
proxy(event, defaultHandler, customHandlerKey) {
|
||||
const { player } = this;
|
||||
const customHandler = player.config.listeners[customHandlerKey];
|
||||
const hasCustomHandler = is.function(customHandler);
|
||||
let returned = true;
|
||||
|
||||
// Execute custom handler
|
||||
if (hasCustomHandler) {
|
||||
returned = customHandler.call(player, event);
|
||||
}
|
||||
|
||||
// Only call default handler if not prevented in custom handler
|
||||
if (returned && is.function(defaultHandler)) {
|
||||
defaultHandler.call(player, event);
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger custom and default handlers
|
||||
bind(element, type, defaultHandler, customHandlerKey, passive = true) {
|
||||
const { player } = this;
|
||||
const customHandler = player.config.listeners[customHandlerKey];
|
||||
const hasCustomHandler = is.function(customHandler);
|
||||
|
||||
on.call(
|
||||
player,
|
||||
element,
|
||||
type,
|
||||
event => this.proxy(event, defaultHandler, customHandlerKey),
|
||||
passive && !hasCustomHandler,
|
||||
);
|
||||
}
|
||||
|
||||
// Listen for control events
|
||||
controls() {
|
||||
const { player } = this;
|
||||
const { elements } = player;
|
||||
|
||||
// IE doesn't support input event, so we fallback to change
|
||||
const inputEvent = browser.isIE ? 'change' : 'input';
|
||||
|
||||
// Run default and custom handlers
|
||||
const proxy = (event, defaultHandler, customHandlerKey) => {
|
||||
const customHandler = this.player.config.listeners[customHandlerKey];
|
||||
const hasCustomHandler = is.function(customHandler);
|
||||
let returned = true;
|
||||
|
||||
// Execute custom handler
|
||||
if (hasCustomHandler) {
|
||||
returned = customHandler.call(this.player, event);
|
||||
}
|
||||
|
||||
// Only call default handler if not prevented in custom handler
|
||||
if (returned && is.function(defaultHandler)) {
|
||||
defaultHandler.call(this.player, event);
|
||||
}
|
||||
};
|
||||
|
||||
// Trigger custom and default handlers
|
||||
const bind = (element, type, defaultHandler, customHandlerKey, passive = true) => {
|
||||
const customHandler = this.player.config.listeners[customHandlerKey];
|
||||
const hasCustomHandler = is.function(customHandler);
|
||||
|
||||
on.call(
|
||||
this.player,
|
||||
element,
|
||||
type,
|
||||
event => proxy(event, defaultHandler, customHandlerKey),
|
||||
passive && !hasCustomHandler,
|
||||
);
|
||||
};
|
||||
|
||||
// Play/pause toggle
|
||||
if (this.player.elements.buttons.play) {
|
||||
Array.from(this.player.elements.buttons.play).forEach(button => {
|
||||
bind(button, 'click', this.player.togglePlay, 'play');
|
||||
if (elements.buttons.play) {
|
||||
Array.from(elements.buttons.play).forEach(button => {
|
||||
this.bind(button, 'click', player.togglePlay, 'play');
|
||||
});
|
||||
}
|
||||
|
||||
// Pause
|
||||
bind(this.player.elements.buttons.restart, 'click', this.player.restart, 'restart');
|
||||
this.bind(elements.buttons.restart, 'click', player.restart, 'restart');
|
||||
|
||||
// Rewind
|
||||
bind(this.player.elements.buttons.rewind, 'click', this.player.rewind, 'rewind');
|
||||
this.bind(elements.buttons.rewind, 'click', player.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
|
||||
bind(
|
||||
this.player.elements.buttons.mute,
|
||||
this.bind(
|
||||
elements.buttons.mute,
|
||||
'click',
|
||||
() => {
|
||||
this.player.muted = !this.player.muted;
|
||||
player.muted = !player.muted;
|
||||
},
|
||||
'mute',
|
||||
);
|
||||
|
||||
// Captions toggle
|
||||
bind(this.player.elements.buttons.captions, 'click', () => this.player.toggleCaptions());
|
||||
this.bind(elements.buttons.captions, 'click', () => player.toggleCaptions());
|
||||
|
||||
// Fullscreen toggle
|
||||
bind(
|
||||
this.player.elements.buttons.fullscreen,
|
||||
this.bind(
|
||||
elements.buttons.fullscreen,
|
||||
'click',
|
||||
() => {
|
||||
this.player.fullscreen.toggle();
|
||||
player.fullscreen.toggle();
|
||||
},
|
||||
'fullscreen',
|
||||
);
|
||||
|
||||
// Picture-in-Picture
|
||||
bind(
|
||||
this.player.elements.buttons.pip,
|
||||
this.bind(
|
||||
elements.buttons.pip,
|
||||
'click',
|
||||
() => {
|
||||
this.player.pip = 'toggle';
|
||||
player.pip = 'toggle';
|
||||
},
|
||||
'pip',
|
||||
);
|
||||
|
||||
// Airplay
|
||||
bind(this.player.elements.buttons.airplay, 'click', this.player.airplay, 'airplay');
|
||||
this.bind(elements.buttons.airplay, 'click', player.airplay, 'airplay');
|
||||
|
||||
// Settings menu
|
||||
bind(this.player.elements.buttons.settings, 'click', event => {
|
||||
controls.toggleMenu.call(this.player, event);
|
||||
// Settings menu - click toggle
|
||||
this.bind(elements.buttons.settings, 'click', event => {
|
||||
controls.toggleMenu.call(player, event);
|
||||
});
|
||||
|
||||
// Settings menu
|
||||
bind(this.player.elements.settings.form, 'click', event => {
|
||||
event.stopPropagation();
|
||||
// Settings menu - keyboard toggle
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Go back to home tab on click
|
||||
const showHomeTab = () => {
|
||||
const id = `plyr-settings-${this.player.id}-home`;
|
||||
controls.showTab.call(this.player, id);
|
||||
};
|
||||
// Prevent scroll
|
||||
event.preventDefault();
|
||||
|
||||
// Settings menu items - use event delegation as items are added/removed
|
||||
if (matches(event.target, this.player.config.selectors.inputs.language)) {
|
||||
proxy(
|
||||
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'));
|
||||
// Prevent playing video (Firefox)
|
||||
if (event.which === 32) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
// Toggle menu
|
||||
controls.toggleMenu.call(player, event);
|
||||
},
|
||||
null,
|
||||
false,
|
||||
);
|
||||
|
||||
// Escape closes menu
|
||||
this.bind(elements.settings.menu, 'keydown', event => {
|
||||
if (event.which === 27) {
|
||||
controls.toggleMenu.call(player, event);
|
||||
}
|
||||
});
|
||||
|
||||
// Set range input alternative "value", which matches the tooltip time (#954)
|
||||
bind(this.player.elements.inputs.seek, 'mousedown mousemove', event => {
|
||||
const clientRect = this.player.elements.progress.getBoundingClientRect();
|
||||
const percent = 100 / clientRect.width * (event.pageX - clientRect.left);
|
||||
this.bind(elements.inputs.seek, 'mousedown mousemove', event => {
|
||||
const rect = elements.progress.getBoundingClientRect();
|
||||
const percent = 100 / rect.width * (event.pageX - rect.left);
|
||||
event.currentTarget.setAttribute('seek-value', percent);
|
||||
});
|
||||
|
||||
// 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 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;
|
||||
}
|
||||
|
||||
// Was playing before?
|
||||
const play = seek.hasAttribute('play-on-seeked');
|
||||
const play = seek.hasAttribute(attribute);
|
||||
|
||||
// Done seeking
|
||||
const done = ['mouseup', 'touchend', 'keyup'].includes(event.type);
|
||||
|
||||
// If we're done seeking and it was playing, resume playback
|
||||
if (play && done) {
|
||||
seek.removeAttribute('play-on-seeked');
|
||||
this.player.play();
|
||||
} else if (!done && this.player.playing) {
|
||||
seek.setAttribute('play-on-seeked', '');
|
||||
this.player.pause();
|
||||
seek.removeAttribute(attribute);
|
||||
player.play();
|
||||
} else if (!done && player.playing) {
|
||||
seek.setAttribute(attribute, '');
|
||||
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
|
||||
bind(
|
||||
this.player.elements.inputs.seek,
|
||||
this.bind(
|
||||
elements.inputs.seek,
|
||||
inputEvent,
|
||||
event => {
|
||||
const seek = event.currentTarget;
|
||||
@ -580,70 +647,71 @@ class Listeners {
|
||||
|
||||
seek.removeAttribute('seek-value');
|
||||
|
||||
this.player.currentTime = seekTo / seek.max * this.player.duration;
|
||||
player.currentTime = seekTo / seek.max * player.duration;
|
||||
},
|
||||
'seek',
|
||||
);
|
||||
|
||||
// Current time invert
|
||||
// Only if one time element is used for both currentTime and duration
|
||||
if (this.player.config.toggleInvert && !is.element(this.player.elements.display.duration)) {
|
||||
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',
|
||||
// Seek tooltip
|
||||
this.bind(elements.progress, 'mouseenter mouseleave mousemove', event =>
|
||||
controls.updateSeekTooltip.call(player, event),
|
||||
);
|
||||
|
||||
// Polyfill for lower fill in <input type="range"> for webkit
|
||||
if (browser.isWebkit) {
|
||||
Array.from(getElements.call(this.player, 'input[type="range"]')).forEach(element => {
|
||||
bind(element, 'input', event => controls.updateRangeFill.call(this.player, event.target));
|
||||
Array.from(getElements.call(player, 'input[type="range"]')).forEach(element => {
|
||||
this.bind(element, 'input', event => controls.updateRangeFill.call(player, event.target));
|
||||
});
|
||||
}
|
||||
|
||||
// Seek tooltip
|
||||
bind(this.player.elements.progress, 'mouseenter mouseleave mousemove', event =>
|
||||
controls.updateSeekTooltip.call(this.player, event),
|
||||
// Current time invert
|
||||
// Only if one time element is used for both currentTime and duration
|
||||
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)
|
||||
bind(this.player.elements.controls, 'mouseenter mouseleave', event => {
|
||||
this.player.elements.controls.hover = !this.player.touch && event.type === 'mouseenter';
|
||||
this.bind(elements.controls, 'mouseenter mouseleave', event => {
|
||||
elements.controls.hover = !player.touch && event.type === 'mouseenter';
|
||||
});
|
||||
|
||||
// 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.player.elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);
|
||||
this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {
|
||||
elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);
|
||||
});
|
||||
|
||||
// Focus in/out on controls
|
||||
bind(this.player.elements.controls, 'focusin focusout', event => {
|
||||
const { config, elements, timers } = this.player;
|
||||
this.bind(elements.controls, 'focusin focusout', event => {
|
||||
const { config, elements, timers } = player;
|
||||
const isFocusIn = event.type === 'focusin';
|
||||
|
||||
// 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
|
||||
ui.toggleControls.call(this.player, event.type === 'focusin');
|
||||
ui.toggleControls.call(player, isFocusIn);
|
||||
|
||||
// If focusin, hide again after delay
|
||||
if (event.type === 'focusin') {
|
||||
if (isFocusIn) {
|
||||
// Restore transition
|
||||
setTimeout(() => {
|
||||
toggleClass(elements.controls, config.classNames.noTransition, false);
|
||||
@ -654,14 +722,15 @@ class Listeners {
|
||||
|
||||
// Clear timer
|
||||
clearTimeout(timers.controls);
|
||||
|
||||
// Hide
|
||||
timers.controls = setTimeout(() => ui.toggleControls.call(this.player, false), delay);
|
||||
timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);
|
||||
}
|
||||
});
|
||||
|
||||
// Mouse wheel for volume
|
||||
bind(
|
||||
this.player.elements.inputs.volume,
|
||||
this.bind(
|
||||
elements.inputs.volume,
|
||||
'wheel',
|
||||
event => {
|
||||
// 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);
|
||||
|
||||
// Change the volume by 2%
|
||||
this.player.increaseVolume(direction / 50);
|
||||
player.increaseVolume(direction / 50);
|
||||
|
||||
// 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)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
@ -207,6 +207,11 @@ class Ads {
|
||||
* @param {Event} adsManagerLoadedEvent
|
||||
*/
|
||||
onAdsManagerLoaded(event) {
|
||||
// Load could occur after a source change (race condition)
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the ads manager
|
||||
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
|
||||
this.manager.setVolume(this.player.volume);
|
||||
|
||||
|
@ -75,16 +75,17 @@ class Plyr {
|
||||
// Elements cache
|
||||
this.elements = {
|
||||
container: null,
|
||||
captions: null,
|
||||
buttons: {},
|
||||
display: {},
|
||||
progress: {},
|
||||
inputs: {},
|
||||
settings: {
|
||||
popup: null,
|
||||
menu: null,
|
||||
panes: {},
|
||||
tabs: {},
|
||||
panels: {},
|
||||
buttons: {},
|
||||
},
|
||||
captions: null,
|
||||
};
|
||||
|
||||
// Captions
|
||||
@ -185,7 +186,7 @@ class Plyr {
|
||||
// YouTube requires the playsinline in the URL
|
||||
if (this.isYouTube) {
|
||||
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 {
|
||||
this.config.playsinline = true;
|
||||
}
|
||||
@ -221,7 +222,7 @@ class Plyr {
|
||||
if (this.media.hasAttribute('autoplay')) {
|
||||
this.config.autoplay = true;
|
||||
}
|
||||
if (this.media.hasAttribute('playsinline')) {
|
||||
if (this.media.hasAttribute('playsinline') || this.media.hasAttribute('webkit-playsinline')) {
|
||||
this.config.playsinline = true;
|
||||
}
|
||||
if (this.media.hasAttribute('muted')) {
|
||||
@ -293,7 +294,9 @@ class Plyr {
|
||||
this.fullscreen = new Fullscreen(this);
|
||||
|
||||
// Setup ads if provided
|
||||
this.ads = new Ads(this);
|
||||
if (this.config.ads.enabled) {
|
||||
this.ads = new Ads(this);
|
||||
}
|
||||
|
||||
// Autoplay if required
|
||||
if (this.config.autoplay) {
|
||||
@ -696,7 +699,9 @@ class Plyr {
|
||||
}
|
||||
|
||||
// Trigger request event
|
||||
triggerEvent.call(this, this.media, 'qualityrequested', false, { quality });
|
||||
triggerEvent.call(this, this.media, 'qualityrequested', false, {
|
||||
quality,
|
||||
});
|
||||
|
||||
// Update config
|
||||
config.selected = quality;
|
||||
@ -933,13 +938,16 @@ class Plyr {
|
||||
if (hiding && this.config.controls.includes('settings') && !is.empty(this.config.settings)) {
|
||||
controls.toggleMenu.call(this, false);
|
||||
}
|
||||
|
||||
// Trigger event on change
|
||||
if (hiding !== isHidden) {
|
||||
const eventName = hiding ? 'controlshidden' : 'controlsshown';
|
||||
triggerEvent.call(this, this.media, eventName);
|
||||
}
|
||||
|
||||
return !hiding;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,9 @@ export const transitionEndEvent = (() => {
|
||||
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;
|
||||
})();
|
||||
@ -23,8 +25,12 @@ export const transitionEndEvent = (() => {
|
||||
// Force repaint of element
|
||||
export function repaint(element) {
|
||||
setTimeout(() => {
|
||||
toggleHidden(element, true);
|
||||
element.offsetHeight; // eslint-disable-line
|
||||
toggleHidden(element, false);
|
||||
try {
|
||||
toggleHidden(element, true);
|
||||
element.offsetHeight; // eslint-disable-line
|
||||
toggleHidden(element, false);
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
@ -70,12 +70,19 @@ export function createElement(type, attributes, text) {
|
||||
|
||||
// Inaert an element after another
|
||||
export function insertAfter(element, target) {
|
||||
if (!is.element(element) || !is.element(target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
target.parentNode.insertBefore(element, target.nextSibling);
|
||||
}
|
||||
|
||||
// Insert a DocumentFragment
|
||||
export function insertElement(type, parent, attributes, text) {
|
||||
// Inject the new <element>
|
||||
if (!is.element(parent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
parent.appendChild(createElement(type, attributes, text));
|
||||
}
|
||||
|
||||
@ -95,6 +102,10 @@ export function removeElement(element) {
|
||||
|
||||
// Remove all child elements
|
||||
export function emptyElement(element) {
|
||||
if (!is.element(element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let { length } = element.childNodes;
|
||||
|
||||
while (length > 0) {
|
||||
@ -105,7 +116,11 @@ export function emptyElement(element) {
|
||||
|
||||
// Replace element
|
||||
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;
|
||||
}
|
||||
|
||||
@ -192,6 +207,10 @@ export function toggleHidden(element, hidden) {
|
||||
|
||||
// Mirror Element.classList.toggle, with IE compatibility for "force" argument
|
||||
export function toggleClass(element, className, force) {
|
||||
if (is.nodeList(element)) {
|
||||
return Array.from(element).map(e => toggleClass(e, className, force));
|
||||
}
|
||||
|
||||
if (is.element(element)) {
|
||||
let method = 'toggle';
|
||||
if (typeof force !== 'undefined') {
|
||||
@ -202,7 +221,7 @@ export function toggleClass(element, className, force) {
|
||||
return element.classList.contains(className);
|
||||
}
|
||||
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Has class name
|
||||
@ -238,26 +257,16 @@ export function getElement(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
|
||||
export function trapFocus(element = null, toggle = false) {
|
||||
if (!is.element(element)) {
|
||||
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 last = focusable[focusable.length - 1];
|
||||
|
||||
@ -268,7 +277,7 @@ export function trapFocus(element = null, toggle = false) {
|
||||
}
|
||||
|
||||
// Get the current focused element
|
||||
const focused = getFocusElement();
|
||||
const focused = document.activeElement;
|
||||
|
||||
if (focused === last && !event.shiftKey) {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ const isNodeList = input => instanceOf(input, NodeList);
|
||||
const isElement = input => instanceOf(input, Element);
|
||||
const isTextNode = input => getConstructor(input) === Text;
|
||||
const isEvent = input => instanceOf(input, Event);
|
||||
const isKeyboardEvent = input => instanceOf(input, KeyboardEvent);
|
||||
const isCue = input => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue);
|
||||
const isTrack = input => instanceOf(input, TextTrack) || (!isNullOrUndefined(input) && isString(input.kind));
|
||||
|
||||
@ -56,6 +57,7 @@ export default {
|
||||
element: isElement,
|
||||
textNode: isTextNode,
|
||||
event: isEvent,
|
||||
keyboardEvent: isKeyboardEvent,
|
||||
cue: isCue,
|
||||
track: isTrack,
|
||||
url: isUrl,
|
||||
|
@ -41,7 +41,7 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Audio styles
|
||||
// Audio control
|
||||
.plyr--audio .plyr__control {
|
||||
&.plyr__tab-focus,
|
||||
&: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)
|
||||
.plyr__control--overlaid {
|
||||
background: rgba($plyr-video-control-bg-hover, 0.8);
|
||||
|
@ -2,11 +2,6 @@
|
||||
// Controls
|
||||
// --------------------------------------------------------------
|
||||
|
||||
// Hide empty controls
|
||||
.plyr__controls:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Hide native controls
|
||||
.plyr--full-ui ::-webkit-media-controls {
|
||||
display: none;
|
||||
@ -37,6 +32,11 @@
|
||||
margin-left: ($plyr-control-spacing / 2);
|
||||
}
|
||||
|
||||
// Hide empty controls
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: $plyr-bp-sm) {
|
||||
> .plyr__control,
|
||||
.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
|
||||
.plyr--video .plyr__controls {
|
||||
background: linear-gradient(
|
||||
@ -69,32 +77,10 @@
|
||||
position: absolute;
|
||||
right: 0;
|
||||
transition: opacity 0.4s ease-in-out, transform 0.4s ease-in-out;
|
||||
z-index: 2;
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
// 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
|
||||
// Hide video controls
|
||||
.plyr--video.plyr--hide-controls .plyr__controls {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
@ -39,7 +39,8 @@
|
||||
|
||||
> div {
|
||||
overflow: hidden;
|
||||
transition: height 0.35s cubic-bezier(0.4, 0, 0.2, 1), width 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition: height 0.35s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
width 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
// Arrow
|
||||
@ -54,18 +55,16 @@
|
||||
width: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
[role='menu'] {
|
||||
padding: $plyr-control-padding;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-top: 2px;
|
||||
[role='menuitem'],
|
||||
[role='menuitemradio'] {
|
||||
margin-top: 2px;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,10 +74,17 @@
|
||||
color: $plyr-menu-color;
|
||||
display: flex;
|
||||
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;
|
||||
width: 100%;
|
||||
|
||||
> span {
|
||||
align-items: inherit;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&::after {
|
||||
border: 4px solid transparent;
|
||||
content: '';
|
||||
@ -135,50 +141,49 @@
|
||||
}
|
||||
}
|
||||
|
||||
label.plyr__control {
|
||||
.plyr__control[role='menuitemradio'] {
|
||||
padding-left: $plyr-control-padding;
|
||||
|
||||
input[type='radio'] + span {
|
||||
background: rgba(#000, 0.1);
|
||||
&::before,
|
||||
&::after {
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
&::before {
|
||||
background: rgba(#000, 0.1);
|
||||
content: '';
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
height: 16px;
|
||||
margin-right: $plyr-control-spacing;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
width: 16px;
|
||||
|
||||
&::after {
|
||||
background: #fff;
|
||||
border-radius: 100%;
|
||||
content: '';
|
||||
height: 6px;
|
||||
left: 5px;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
transform: scale(0);
|
||||
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||
width: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='radio']:checked + span {
|
||||
background: $plyr-color-main;
|
||||
&::after {
|
||||
background: #fff;
|
||||
border: 0;
|
||||
height: 6px;
|
||||
left: 12px;
|
||||
opacity: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%) scale(0);
|
||||
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&[aria-checked='true'] {
|
||||
&::before {
|
||||
background: $plyr-color-main;
|
||||
}
|
||||
&::after {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
transform: translateY(-50%) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
input[type='radio']:focus + span {
|
||||
@include plyr-tab-focus();
|
||||
}
|
||||
|
||||
&.plyr__tab-focus input[type='radio'] + span,
|
||||
&:hover input[type='radio'] + span {
|
||||
&.plyr__tab-focus::before,
|
||||
&:hover::before {
|
||||
background: rgba(#000, 0.1);
|
||||
}
|
||||
}
|
||||
@ -188,7 +193,7 @@
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin-left: auto;
|
||||
margin-right: -$plyr-control-padding;
|
||||
margin-right: -($plyr-control-padding - 2);
|
||||
overflow: hidden;
|
||||
padding-left: ceil($plyr-control-padding * 3.5);
|
||||
pointer-events: none;
|
||||
|
@ -12,12 +12,11 @@
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
transition: opacity 0.2s ease;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.plyr--stopped.plyr__poster-enabled .plyr__poster {
|
||||
opacity: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
// --------------------------------------------------------------
|
||||
|
||||
.plyr__progress {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
left: $plyr-range-thumb-height / 2;
|
||||
margin-right: $plyr-range-thumb-height;
|
||||
|
@ -19,7 +19,11 @@
|
||||
|
||||
&::-webkit-slider-runnable-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 {
|
||||
@ -140,15 +144,21 @@
|
||||
// Pressed styles
|
||||
&:active {
|
||||
&::-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 {
|
||||
@include plyr-range-thumb-active($plyr-audio-range-thumb-shadow-color);
|
||||
@include plyr-range-thumb-active(
|
||||
$plyr-audio-range-thumb-shadow-color
|
||||
);
|
||||
}
|
||||
|
||||
&::-ms-thumb {
|
||||
@include plyr-range-thumb-active($plyr-audio-range-thumb-shadow-color);
|
||||
@include plyr-range-thumb-active(
|
||||
$plyr-audio-range-thumb-shadow-color
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
// Nicer focus styles
|
||||
// ---------------------------------------
|
||||
@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;
|
||||
}
|
||||
|
||||
@ -28,6 +28,7 @@
|
||||
border: 0;
|
||||
border-radius: ($plyr-range-track-height / 2);
|
||||
height: $plyr-range-track-height;
|
||||
transition: box-shadow 0.3s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@ -36,7 +37,6 @@
|
||||
border: 0;
|
||||
border-radius: 100%;
|
||||
box-shadow: $plyr-range-thumb-shadow;
|
||||
box-sizing: border-box;
|
||||
height: $plyr-range-thumb-height;
|
||||
position: relative;
|
||||
transition: all 0.2s ease;
|
||||
|
@ -22,3 +22,7 @@
|
||||
width: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.plyr [hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
146
yarn.lock
146
yarn.lock
@ -407,6 +407,17 @@ autoprefixer@^8.0.0:
|
||||
postcss "^6.0.19"
|
||||
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:
|
||||
version "0.6.0"
|
||||
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"
|
||||
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:
|
||||
version "1.1.0"
|
||||
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"
|
||||
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:
|
||||
version "1.0.0"
|
||||
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:
|
||||
array-find-index "^1.0.1"
|
||||
|
||||
custom-event-polyfill@^0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/custom-event-polyfill/-/custom-event-polyfill-0.3.0.tgz#99807839be62edb446b645832e0d80ead6fa1888"
|
||||
custom-event-polyfill@^1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/custom-event-polyfill/-/custom-event-polyfill-1.0.6.tgz#6b026e81cd9f7bc896bd6b016a427407bb068db1"
|
||||
|
||||
d@1:
|
||||
version "1.0.0"
|
||||
@ -1859,6 +1882,10 @@ electron-to-chromium@^1.3.47:
|
||||
version "1.3.48"
|
||||
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":
|
||||
version "6.1.1"
|
||||
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"
|
||||
text-table "~0.2.0"
|
||||
|
||||
eslint@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.1.0.tgz#2ed611f1ce163c0fb99e1e0cda5af8f662dff645"
|
||||
eslint@^5.2.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.2.0.tgz#3901ae249195d473e633c4acbc370068b1c964dc"
|
||||
dependencies:
|
||||
ajv "^6.5.0"
|
||||
babel-code-frame "^6.26.0"
|
||||
@ -2064,7 +2091,7 @@ eslint@^5.1.0:
|
||||
functional-red-black-tree "^1.0.1"
|
||||
glob "^7.1.2"
|
||||
globals "^11.7.0"
|
||||
ignore "^3.3.3"
|
||||
ignore "^4.0.2"
|
||||
imurmurhash "^0.1.4"
|
||||
inquirer "^5.2.0"
|
||||
is-resolvable "^1.1.0"
|
||||
@ -2889,9 +2916,9 @@ gulp-postcss@^7.0.1:
|
||||
postcss-load-config "^1.2.0"
|
||||
vinyl-sourcemaps-apply "^0.2.1"
|
||||
|
||||
gulp-rename@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/gulp-rename/-/gulp-rename-1.3.0.tgz#2e789d8f563ab0c924eeb62967576f37ff4cb826"
|
||||
gulp-rename@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/gulp-rename/-/gulp-rename-1.4.0.tgz#de1c718e7c4095ae861f7296ef4f3248648240bd"
|
||||
|
||||
gulp-replace@^1.0.0:
|
||||
version "1.0.0"
|
||||
@ -3238,6 +3265,10 @@ ignore@^3.3.3, ignore@^3.3.5:
|
||||
version "3.3.7"
|
||||
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:
|
||||
version "2.1.0"
|
||||
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"
|
||||
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:
|
||||
version "4.8.3"
|
||||
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"
|
||||
unist-util-find-all-after "^1.0.1"
|
||||
|
||||
postcss-html@^0.28.0:
|
||||
version "0.28.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-html/-/postcss-html-0.28.0.tgz#3dd0f5b5d7f886b8181bf844396d43a7898162cb"
|
||||
postcss-html@^0.31.0:
|
||||
version "0.31.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-html/-/postcss-html-0.31.0.tgz#ea6ae2e95df60a03032e9ab5aba72143d8ca0325"
|
||||
dependencies:
|
||||
htmlparser2 "^3.9.2"
|
||||
|
||||
@ -5185,9 +5222,9 @@ postcss-load-plugins@^2.3.0:
|
||||
cosmiconfig "^2.1.1"
|
||||
object-assign "^4.1.0"
|
||||
|
||||
postcss-markdown@^0.28.0:
|
||||
version "0.28.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-markdown/-/postcss-markdown-0.28.0.tgz#99d1c4e74967af9e9c98acb2e2b66df4b3c6ed86"
|
||||
postcss-markdown@^0.31.0:
|
||||
version "0.31.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-markdown/-/postcss-markdown-0.31.0.tgz#e4c699ad34b14a29ad5d47132bb1b3100b60ef75"
|
||||
dependencies:
|
||||
remark "^9.0.0"
|
||||
unist-util-find-all-after "^1.0.2"
|
||||
@ -5215,6 +5252,12 @@ postcss-safe-parser@^3.0.1:
|
||||
dependencies:
|
||||
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:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-sass/-/postcss-sass-0.2.0.tgz#e55516441e9526ba4b380a730d3a02e9eaa78c7a"
|
||||
@ -5235,6 +5278,12 @@ postcss-scss@^1.0.2:
|
||||
dependencies:
|
||||
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:
|
||||
version "3.1.1"
|
||||
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"
|
||||
postcss "^6.0.13"
|
||||
|
||||
postcss-syntax@^0.28.0:
|
||||
version "0.28.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-syntax/-/postcss-syntax-0.28.0.tgz#e17572a7dcf5388f0c9b68232d2dad48fa7f0b12"
|
||||
postcss-styled@^0.31.0:
|
||||
version "0.31.0"
|
||||
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:
|
||||
version "3.3.0"
|
||||
@ -5299,6 +5352,14 @@ postcss@^6.0.17:
|
||||
source-map "^0.6.1"
|
||||
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:
|
||||
version "1.1.2"
|
||||
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"
|
||||
kind-of "^4.0.0"
|
||||
|
||||
raven-js@^3.26.3:
|
||||
version "3.26.3"
|
||||
resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.26.3.tgz#0efb49969b5b11ab965f7b0d6da4ca102b763cb0"
|
||||
raven-js@^3.26.4:
|
||||
version "3.26.4"
|
||||
resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.26.4.tgz#32aae3a63a9314467a453c94c89a364ea43707be"
|
||||
|
||||
rc@^1.0.1, rc@^1.1.6:
|
||||
version "1.2.6"
|
||||
@ -5948,9 +6009,9 @@ rollup-plugin-babel@^3.0.7:
|
||||
dependencies:
|
||||
rollup-pluginutils "^1.5.0"
|
||||
|
||||
rollup-plugin-commonjs@^9.1.3:
|
||||
version "9.1.3"
|
||||
resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-9.1.3.tgz#37bfbf341292ea14f512438a56df8f9ca3ba4d67"
|
||||
rollup-plugin-commonjs@^9.1.4:
|
||||
version "9.1.4"
|
||||
resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-9.1.4.tgz#525b701adfd40e314b5bb6888d88edc28e10442f"
|
||||
dependencies:
|
||||
estree-walker "^0.5.1"
|
||||
magic-string "^0.22.4"
|
||||
@ -6256,6 +6317,10 @@ specificity@^0.3.1:
|
||||
version "0.3.2"
|
||||
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:
|
||||
version "3.1.0"
|
||||
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-value-parser "^3.3.0"
|
||||
|
||||
stylelint-scss@^3.1.3:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-3.1.3.tgz#28f881ae298c3f5db667b10b6cf94a1a219001d6"
|
||||
stylelint-scss@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-3.2.0.tgz#13545a1be5ab5435ea94e761b2d4824eb32033b3"
|
||||
dependencies:
|
||||
lodash "^4.17.10"
|
||||
postcss-media-query-parser "^0.2.3"
|
||||
@ -6575,11 +6640,11 @@ stylelint@^8.1.1:
|
||||
svg-tags "^1.0.0"
|
||||
table "^4.0.1"
|
||||
|
||||
stylelint@^9.3.0:
|
||||
version "9.3.0"
|
||||
resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-9.3.0.tgz#fe176e4e421ac10eac1a6b6d9f28e908eb58c5db"
|
||||
stylelint@^9.4.0:
|
||||
version "9.4.0"
|
||||
resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-9.4.0.tgz#2f2b82ae9db53a06735ae0724f41b134fdb84a10"
|
||||
dependencies:
|
||||
autoprefixer "^8.0.0"
|
||||
autoprefixer "^9.0.0"
|
||||
balanced-match "^1.0.0"
|
||||
chalk "^2.4.1"
|
||||
cosmiconfig "^5.0.0"
|
||||
@ -6590,7 +6655,7 @@ stylelint@^9.3.0:
|
||||
globby "^8.0.0"
|
||||
globjoin "^0.1.4"
|
||||
html-tags "^2.0.0"
|
||||
ignore "^3.3.3"
|
||||
ignore "^4.0.0"
|
||||
import-lazy "^3.1.0"
|
||||
imurmurhash "^0.1.4"
|
||||
known-css-properties "^0.6.0"
|
||||
@ -6601,22 +6666,23 @@ stylelint@^9.3.0:
|
||||
micromatch "^2.3.11"
|
||||
normalize-selector "^0.2.0"
|
||||
pify "^3.0.0"
|
||||
postcss "^6.0.16"
|
||||
postcss-html "^0.28.0"
|
||||
postcss "^7.0.0"
|
||||
postcss-html "^0.31.0"
|
||||
postcss-less "^2.0.0"
|
||||
postcss-markdown "^0.28.0"
|
||||
postcss-markdown "^0.31.0"
|
||||
postcss-media-query-parser "^0.2.3"
|
||||
postcss-reporter "^5.0.0"
|
||||
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-scss "^1.0.2"
|
||||
postcss-scss "^2.0.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"
|
||||
resolve-from "^4.0.0"
|
||||
signal-exit "^3.0.2"
|
||||
specificity "^0.3.1"
|
||||
specificity "^0.4.0"
|
||||
string-width "^2.1.0"
|
||||
style-search "^0.1.0"
|
||||
sugarss "^1.0.0"
|
||||
|
Loading…
x
Reference in New Issue
Block a user