Merge branch 'master' into beta

This commit is contained in:
Sam Potts 2018-05-08 22:22:09 +10:00
commit e1ff516219
39 changed files with 5016 additions and 4490 deletions

View File

@ -1,3 +1,39 @@
# v3.3.6
* Vimeo fixes for mute state
* Vimeo ID fix (fixes #945)
* Use `<div>` for poster container
* Tooltip fixes for unicode languages (fixes #943)
# v3.3.5
* Removed `.load()` call as it breaks HLS (see #870)
# v3.3.4
* Fix for controls sometimes not showing while video is playing
* Fixed logic for show home tab on option select
# v3.3.3
* Reverted change to show home tab on option select due to usability regression
# v3.3.2
* Fix for ads running in audio
* Fix for setting poster on source change
## v3.3.0
* Now using a custom poster image element to hide the YouTube play button and give more control over when the poster image shows
* Renamed `showPosterOnEnd` to `resetOnEnd` as it makes more sense and now works for all players and does not reload media
* Fix for same domain SVG URLs (raised by Jochem in Slack)
* [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/Window/URL) is polyfill now required
* Added pause className (fixes #941)
* Button height set in CSS (auto) (fixes #928)
* Don't autoplay cloned original media (fixes #936)
* Return to the home menu pane after selecting an option
## v3.2.4
* Fix issue wher player never reports as ready if controls is empty array

2
demo/dist/demo.css vendored

File diff suppressed because one or more lines are too long

33
demo/dist/demo.js vendored
View File

@ -3948,6 +3948,39 @@ singleton.Client = Client;
'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
},

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -171,7 +171,7 @@
</aside>
<!-- Polyfills -->
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent,Object.entries,Object.values"
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent,Object.entries,Object.values,URL"
crossorigin="anonymous"></script>
<!-- Plyr core script -->

View File

@ -74,6 +74,39 @@ import Raven from 'raven-js';
'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,
},

View File

@ -29,6 +29,7 @@ video {
position: absolute;
right: 0;
top: 0;
z-index: 3;
}
// Style full supported player

2
dist/plyr.css vendored

File diff suppressed because one or more lines are too long

4325
dist/plyr.js vendored

File diff suppressed because it is too large Load Diff

2
dist/plyr.js.map vendored

File diff suppressed because one or more lines are too long

2
dist/plyr.min.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4329
dist/plyr.polyfilled.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -129,7 +129,7 @@ const build = {
tasks.js.push(name);
const { output } = paths[bundle];
gulp.task(name, () =>
return gulp.task(name, () =>
gulp
.src(bundles[bundle].js[key])
.pipe(sourcemaps.init())
@ -162,7 +162,7 @@ const build = {
const name = `sass:${key}`;
tasks.sass.push(name);
gulp.task(name, () =>
return gulp.task(name, () =>
gulp
.src(bundles[bundle].sass[key])
.pipe(sass())
@ -180,7 +180,7 @@ const build = {
tasks.sprite.push(name);
// Process Icons
gulp.task(name, () =>
return gulp.task(name, () =>
gulp
.src(paths[bundle].src.sprite)
.pipe(
@ -287,7 +287,8 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
'plyr.polyfilled.js',
'defaults.js',
];
gulp
return gulp
.src(files.map(file => path.join(root, `src/js/${file}`)))
.pipe(replace(semver, `v${version}`))
.pipe(replace(cdnpath, `${aws.cdn.domain}/${version}/`))
@ -406,7 +407,7 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
});
// Do everything
gulp.task('publish', () => {
run('version', tasks.clean, tasks.js, tasks.sass, tasks.sprite, 'cdn', 'demo');
gulp.task('publish', callback => {
run('version', tasks.clean, tasks.js, tasks.sass, tasks.sprite, 'cdn', 'demo', callback);
});
}

View File

@ -1,6 +1,6 @@
{
"name": "plyr",
"version": "3.2.4",
"version": "3.3.6",
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
"homepage": "https://plyr.io",
"main": "./dist/plyr.js",
@ -8,7 +8,7 @@
"sass": "./src/sass/plyr.scss",
"style": "./dist/plyr.css",
"devDependencies": {
"babel-core": "^6.26.0",
"babel-core": "^6.26.3",
"babel-eslint": "^8.2.3",
"babel-plugin-external-helpers": "^6.22.0",
"babel-preset-env": "^1.6.1",
@ -21,7 +21,7 @@
"gulp": "^3.9.1",
"gulp-autoprefixer": "^5.0.0",
"gulp-better-rollup": "^3.1.0",
"gulp-clean-css": "^3.9.3",
"gulp-clean-css": "^3.9.4",
"gulp-concat": "^2.6.1",
"gulp-filter": "^5.1.0",
"gulp-open": "^3.0.1",
@ -38,7 +38,7 @@
"prettier-eslint": "^8.8.1",
"prettier-stylelint": "^0.4.2",
"rollup-plugin-babel": "^3.0.4",
"rollup-plugin-commonjs": "^9.1.0",
"rollup-plugin-commonjs": "^9.1.3",
"rollup-plugin-node-resolve": "^3.3.0",
"run-sequence": "^2.2.1",
"stylelint": "^9.2.0",
@ -46,7 +46,7 @@
"stylelint-config-recommended": "^2.1.0",
"stylelint-config-sass-guidelines": "^5.0.0",
"stylelint-order": "^0.8.1",
"stylelint-scss": "^3.0.1",
"stylelint-scss": "^3.1.0",
"stylelint-selector-bem-pattern": "^2.0.0"
},
"keywords": ["HTML5 Video", "HTML5 Audio", "Media Player", "DASH", "Shaka", "WordPress", "HLS"],

View File

@ -128,7 +128,7 @@ See [initialising](#initialising) for more information on advanced setups.
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript, you can use the following:
```html
<script src="https://cdn.plyr.io/3.2.4/plyr.js"></script>
<script src="https://cdn.plyr.io/3.3.6/plyr.js"></script>
```
_Note_: Be sure to read the [polyfills](#polyfills) section below about browser compatibility
@ -144,13 +144,13 @@ Include the `plyr.css` stylsheet into your `<head>`
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the default CSS, you can use the following:
```html
<link rel="stylesheet" href="https://cdn.plyr.io/3.2.4/plyr.css">
<link rel="stylesheet" href="https://cdn.plyr.io/3.3.6/plyr.css">
```
### SVG Sprite
The SVG sprite is loaded automatically from our CDN (provided by [Fastly](https://www.fastly.com/)). To change this, see the [options](#options) below. For
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.2.4/plyr.svg`.
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.3.6/plyr.svg`.
## Ads
@ -210,7 +210,7 @@ You can specify a range of arguments for the constructor to use:
* A [`NodeList]`(https://developer.mozilla.org/en-US/docs/Web/API/NodeList)
* A [jQuery](https://jquery.com) object
_Note_: If a `NodeList`, `Array`, or jQuery object are passed, the first element will be used for setup.
_Note_: If a `NodeList`, `Array`, or jQuery object are passed, the first element will be used for setup. To setup multiple players, see [setting up multiple players](#setting-up-multiple-players) below.
Here's some examples
@ -226,20 +226,32 @@ Passing a [HTMLElement](https://developer.mozilla.org/en/docs/Web/API/HTMLElemen
const player = new Plyr(document.getElementById('player'));
```
Passing a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList):
Passing a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList) (see note below):
```javascript
const player = new Plyr(document.querySelectorAll('.js-player'));
```
The NodeList, HTMLElement or string selector can be the target `<video>`, `<audio>`, or `<div>` wrapper for embeds
The NodeList, HTMLElement or string selector can be the target `<video>`, `<audio>`, or `<div>` wrapper for embeds.
##### Setting up multiple players
You have two choices here. You can either use a simple array loop to map the constructor:
```javascript
const players = Array.from(document.querySelectorAll('.js-player')).map(player => new Plyr(player));
const players = Array.from(document.querySelectorAll('.js-player')).map(p => new Plyr(p));
```
...or use a static method where you can pass a [string selector](https://developer.mozilla.org/en-US/docs/Web/API/NodeList), a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList) or an [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) of elements:
```javascript
const players = Plyr.setup('.js-player');
```
Both options will also return an array of instances in the order of they were in the DOM for the string selector or the source NodeList or Array.
##### Passing options
The second argument for the constructor is the [options](#options) object:
```javascript
@ -248,7 +260,7 @@ const player = new Plyr('#player', {
});
```
The constructor will return a Plyr object that can be used with the [API](#api) methods. See the [API](#api) section for more info.
In all cases, the constructor will return a Plyr object that can be used with the [API](#api) methods. See the [API](#api) section for more info.
#### Options
@ -279,7 +291,7 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
| `clickToPlay` | Boolean | `true` | Click (or tap) of the video container will toggle play/pause. |
| `disableContextMenu` | Boolean | `true` | Disable right click menu on video to <em>help</em> as very primitive obfuscation to prevent downloads of content. |
| `hideControls` | Boolean | `true` | Hide video controls automatically after 2s of no mouse or focus movement, on control element blur (tab out), on playback start or entering fullscreen. As soon as the mouse is moved, a control element is focused or playback is paused, the controls reappear instantly. |
| `showPosterOnEnd` | Boolean | false | This will restore and _reload_ HTML5 video once playback is complete. Note: depending on the browser caching, this may result in the video downloading again (or parts of it). Use with caution. |
| `resetOnEnd` | Boolean | false | Reset the playback to the start once playback is complete. |
| `keyboard` | Object | `{ focused: true, global: false }` | Enable [keyboard shortcuts](#shortcuts) for focused players only or globally |
| `tooltips` | Object | `{ controls: false, seek: true }` | `controls`: Display control labels as tooltips on `:hover` & `:focus` (by default, the labels are screen reader only). `seek`: Display a seek tooltip to indicate on click where the media would seek to. |
| `duration` | Number | `null` | Specify a custom duration for media. |
@ -374,8 +386,9 @@ player.fullscreen.active; // false;
| -------------------- | ------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `isHTML5` | ✓ | - | Returns a boolean indicating if the current player is HTML5. |
| `isEmbed` | ✓ | - | Returns a boolean indicating if the current player is an embedded player. |
| `paused` | ✓ | - | Returns a boolean indicating if the current player is paused. |
| `playing` | ✓ | - | Returns a boolean indicating if the current player is playing. |
| `paused` | ✓ | - | Returns a boolean indicating if the current player is paused. |
| `stopped` | ✓ | - | Returns a boolean indicating if the current player is stopped. |
| `ended` | ✓ | - | Returns a boolean indicating if the current player has finished playback. |
| `buffered` | ✓ | - | Returns a float between 0 and 1 indicating how much of the media is buffered |
| `currentTime` | ✓ | ✓ | Gets or sets the currentTime for the player. The setter accepts a float in seconds. |
@ -388,7 +401,7 @@ player.fullscreen.active; // false;
| `quality`&sup1; | ✓ | ✓ | Gets or sets the quality for the player. The setter accepts a value from the options specified in your config. |
| `loop` | ✓ | ✓ | Gets or sets the current loop state of the player. The setter accepts a boolean. |
| `source` | ✓ | ✓ | Gets or sets the current source for the player. The setter accepts an object. See [source setter](#source-setter) below for examples. |
| `poster`&sup2; | ✓ | ✓ | Gets or sets the current poster image for the player. The setter accepts a string; the URL for the updated poster image. |
| `poster` | ✓ | ✓ | Gets or sets the current poster image for the player. The setter accepts a string; the URL for the updated poster image. |
| `autoplay` | ✓ | ✓ | Gets or sets the autoplay state of the player. The setter accepts a boolean. |
| `language` | ✓ | ✓ | Gets or sets the preferred captions language for the player. The setter accepts an ISO two-letter language code. Support for the languages is dependent on the captions you include. |
| `fullscreen.active` | ✓ | - | Returns a boolean indicating if the current player is in fullscreen mode. |
@ -599,6 +612,8 @@ Russitto ([@russitto](https://github.com/russitto)) for working on this. Here's
* Using [Shaka](https://github.com/google/shaka-player) - [Demo](http://codepen.io/sampotts/pen/zBNpVR)
* Using [dash.js](https://github.com/Dash-Industry-Forum/dash.js) - [Demo](http://codepen.io/sampotts/pen/BzpJXN)
_Note_: These need updating to use the new v3 syntax but would still work.
## Fullscreen
Fullscreen in Plyr is supported by all browsers that [currently support it](http://caniuse.com/#feat=fullscreen).
@ -608,18 +623,19 @@ Fullscreen in Plyr is supported by all browsers that [currently support it](http
Plyr supports the last 2 versions of most _modern_ browsers.
| Browser | Supported |
| ------------- | --------- |
| ------------- | ------------- |
| Safari | ✓ |
| Mobile Safari | ✓&sup1; |
| Firefox | ✓ |
| Chrome | ✓ |
| Opera | ✓ |
| Edge | ✓ |
| IE11 | ✓ |
| IE10 | ✓&sup2; |
| IE11 | ✓&sup3; |
| IE10 | ✓&sup2;&sup3; |
1. Mobile Safari on the iPhone forces the native player for `<video>` unless the `playsinline` attribute is present. Volume controls are also disabled as they are handled device wide.
2. Native player used (no support for `<progress>` or `<input type="range">`) but the API is supported. No native fullscreen support, fallback can be used (see [options](#options))
2. Native player used (no support for `<progress>` or `<input type="range">`) but the API is supported. No native fullscreen support, fallback can be used (see [options](#options)).
3. Polyfills required. See below.
### Polyfills

View File

@ -3,10 +3,10 @@
// TODO: Create as class
// ==========================================================================
import support from './support';
import utils from './utils';
import controls from './controls';
import i18n from './i18n';
import support from './support';
import utils from './utils';
const captions = {
// Setup captions

90
src/js/controls.js vendored
View File

@ -2,12 +2,12 @@
// Plyr controls
// ==========================================================================
import support from './support';
import utils from './utils';
import ui from './ui';
import i18n from './i18n';
import captions from './captions';
import html5 from './html5';
import i18n from './i18n';
import support from './support';
import ui from './ui';
import utils from './utils';
// Sniff out the browser
const browser = utils.getBrowser();
@ -37,17 +37,74 @@ const controls = {
// Get icon URL
getIconUrl() {
const url = new URL(this.config.iconUrl, window.location);
const cors = url.host !== window.location.host || (browser.isIE && !window.svg4everybody);
return {
url: this.config.iconUrl,
absolute: this.config.iconUrl.indexOf('http') === 0 || (browser.isIE && !window.svg4everybody),
cors,
};
},
// Find the UI controls and store references in custom controls
// TODO: Allow settings menus with custom controls
findElements() {
try {
this.elements.controls = utils.getElement.call(this, this.config.selectors.controls.wrapper);
// Buttons
this.elements.buttons = {
play: utils.getElements.call(this, this.config.selectors.buttons.play),
pause: utils.getElement.call(this, this.config.selectors.buttons.pause),
restart: utils.getElement.call(this, this.config.selectors.buttons.restart),
rewind: utils.getElement.call(this, this.config.selectors.buttons.rewind),
fastForward: utils.getElement.call(this, this.config.selectors.buttons.fastForward),
mute: utils.getElement.call(this, this.config.selectors.buttons.mute),
pip: utils.getElement.call(this, this.config.selectors.buttons.pip),
airplay: utils.getElement.call(this, this.config.selectors.buttons.airplay),
settings: utils.getElement.call(this, this.config.selectors.buttons.settings),
captions: utils.getElement.call(this, this.config.selectors.buttons.captions),
fullscreen: utils.getElement.call(this, this.config.selectors.buttons.fullscreen),
};
// Progress
this.elements.progress = utils.getElement.call(this, this.config.selectors.progress);
// Inputs
this.elements.inputs = {
seek: utils.getElement.call(this, this.config.selectors.inputs.seek),
volume: utils.getElement.call(this, this.config.selectors.inputs.volume),
};
// Display
this.elements.display = {
buffer: utils.getElement.call(this, this.config.selectors.display.buffer),
currentTime: utils.getElement.call(this, this.config.selectors.display.currentTime),
duration: utils.getElement.call(this, this.config.selectors.display.duration),
};
// Seek tooltip
if (utils.is.element(this.elements.progress)) {
this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`);
}
return true;
} catch (error) {
// Log it
this.debug.warn('It looks like there is a problem with your custom controls HTML', error);
// Restore native video controls
this.toggleNativeControls(true);
return false;
}
},
// Create <svg> icon
createIcon(type, attributes) {
const namespace = 'http://www.w3.org/2000/svg';
const iconUrl = controls.getIconUrl.call(this);
const iconPath = `${!iconUrl.absolute ? iconUrl.url : ''}#${this.config.iconPrefix}`;
const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`;
// Create <svg>
const icon = document.createElementNS(namespace, 'svg');
@ -331,7 +388,7 @@ const controls = {
const container = utils.createElement('div', utils.extend(attributes, {
class: `plyr__time ${attributes.class}`,
'aria-label': i18n.get(type, this.config),
}), '0:00');
}), '00:00');
// Reference for updates
this.elements.display[type] = container;
@ -483,6 +540,7 @@ const controls = {
break;
case 576:
case 480:
label = 'SD';
break;
@ -840,11 +898,9 @@ const controls = {
},
// Toggle Menu
showTab(event) {
showTab(target = '') {
const { menu } = this.elements.settings;
const tab = event.target;
const show = tab.getAttribute('aria-expanded') === 'false';
const pane = document.getElementById(tab.getAttribute('aria-controls'));
const pane = document.getElementById(target);
// Nothing to show, bail
if (!utils.is.element(pane)) {
@ -907,8 +963,12 @@ const controls = {
current.setAttribute('tabindex', -1);
// Set attributes on target
utils.toggleHidden(pane, !show);
tab.setAttribute('aria-expanded', show);
utils.toggleHidden(pane, false);
const tabs = utils.getElements.call(this, `[aria-controls="${target}"]`);
Array.from(tabs).forEach(tab => {
tab.setAttribute('aria-expanded', true);
});
pane.removeAttribute('tabindex');
// Focus the first item
@ -1183,7 +1243,7 @@ const controls = {
const icon = controls.getIconUrl.call(this);
// Only load external sprite using AJAX
if (icon.absolute) {
if (icon.cors) {
utils.loadSprite(icon.url, 'sprite-plyr');
}
}
@ -1269,7 +1329,7 @@ const controls = {
// Find the elements if need be
if (!utils.is.element(this.elements.controls)) {
utils.findElements.call(this);
controls.findElements.call(this);
}
// Edge sometimes doesn't finish the paint so force a redraw

View File

@ -47,8 +47,8 @@ const defaults = {
// Auto hide the controls
hideControls: true,
// Revert to poster on finish (HTML5 - will cause reload)
showPosterOnEnd: false,
// Reset to start when playback ended
resetOnEnd: false,
// Disable the standard context menu
disableContextMenu: true,
@ -56,7 +56,7 @@ const defaults = {
// Sprite (for icons)
loadSprite: true,
iconPrefix: 'plyr',
iconUrl: 'https://cdn.plyr.io/3.2.4/plyr.svg',
iconUrl: 'https://cdn.plyr.io/3.3.6/plyr.svg',
// Blank video (used to prevent errors on source change)
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
@ -157,10 +157,10 @@ const defaults = {
// Localisation
i18n: {
restart: 'Restart',
rewind: 'Rewind {seektime} secs',
rewind: 'Rewind {seektime}s',
play: 'Play',
pause: 'Pause',
fastForward: 'Forward {seektime} secs',
fastForward: 'Forward {seektime}s',
seek: 'Seek',
played: 'Played',
buffered: 'Buffered',
@ -192,13 +192,17 @@ const defaults = {
// URLs
urls: {
vimeo: {
api: 'https://player.vimeo.com/api/player.js',
sdk: 'https://player.vimeo.com/api/player.js',
iframe: 'https://player.vimeo.com/video/{0}?{1}',
api: 'https://vimeo.com/api/v2/video/{0}.json',
},
youtube: {
api: 'https://www.youtube.com/iframe_api',
sdk: 'https://www.youtube.com/iframe_api',
api: 'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title))&part=snippet',
poster: 'https://img.youtube.com/vi/{0}/maxresdefault.jpg,https://img.youtube.com/vi/{0}/hqdefault.jpg',
},
googleIMA: {
api: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',
},
},
@ -322,14 +326,17 @@ const defaults = {
// Class hooks added to the player in different states
classNames: {
video: 'plyr__video-wrapper',
embed: 'plyr__video-embed',
ads: 'plyr__ads',
control: 'plyr__control',
type: 'plyr--{0}',
provider: 'plyr--{0}',
stopped: 'plyr--stopped',
video: 'plyr__video-wrapper',
embed: 'plyr__video-embed',
embedContainer: 'plyr__video-embed__container',
poster: 'plyr__poster',
ads: 'plyr__ads',
control: 'plyr__control',
playing: 'plyr--playing',
paused: 'plyr--paused',
stopped: 'plyr--stopped',
loading: 'plyr--loading',
error: 'plyr--has-error',
hover: 'plyr--hover',

View File

@ -2,9 +2,9 @@
// Plyr Event Listeners
// ==========================================================================
import utils from './utils';
import controls from './controls';
import ui from './ui';
import utils from './utils';
// Sniff out the browser
const browser = utils.getBrowser();
@ -265,12 +265,9 @@ class Listeners {
// Handle the media finishing
utils.on(this.player.media, 'ended', () => {
// Show poster on end
if (this.player.isHTML5 && this.player.isVideo && this.player.config.showPosterOnEnd) {
if (this.player.isHTML5 && this.player.isVideo && this.player.config.resetOnEnd) {
// Restart
this.player.restart();
// Re-load media
this.player.media.load();
}
});
@ -281,7 +278,7 @@ class Listeners {
utils.on(this.player.media, 'volumechange', event => ui.updateVolume.call(this.player, event));
// Handle play/pause
utils.on(this.player.media, 'playing play pause ended emptied', event => ui.checkPlaying.call(this.player, event));
utils.on(this.player.media, 'playing play pause ended emptied timeupdate', event => ui.checkPlaying.call(this.player, event));
// Loading state
utils.on(this.player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(this.player, event));
@ -492,12 +489,19 @@ class Listeners {
on(this.player.elements.settings.form, 'click', event => {
event.stopPropagation();
// Go back to home tab on click
const showHomeTab = () => {
const id = `plyr-settings-${this.player.id}-home`;
controls.showTab.call(this.player, id);
};
// Settings menu items - use event delegation as items are added/removed
if (utils.matches(event.target, this.player.config.selectors.inputs.language)) {
proxy(
event,
() => {
this.player.language = event.target.value;
showHomeTab();
},
'language',
);
@ -506,6 +510,7 @@ class Listeners {
event,
() => {
this.player.quality = event.target.value;
showHomeTab();
},
'quality',
);
@ -514,11 +519,13 @@ class Listeners {
event,
() => {
this.player.speed = parseFloat(event.target.value);
showHomeTab();
},
'speed',
);
} else {
controls.showTab.call(this.player, event);
const tab = event.target;
controls.showTab.call(this.player, tab.getAttribute('aria-controls'));
}
});

View File

@ -2,15 +2,10 @@
// Plyr Media
// ==========================================================================
import support from './support';
import utils from './utils';
import youtube from './plugins/youtube';
import vimeo from './plugins/vimeo';
import html5 from './html5';
import ui from './ui';
// Sniff out the browser
const browser = utils.getBrowser();
import vimeo from './plugins/vimeo';
import youtube from './plugins/youtube';
import utils from './utils';
const media = {
// Setup media
@ -33,23 +28,6 @@ const media = {
utils.toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);
}
if (this.supported.ui) {
// Check for picture-in-picture support
utils.toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo);
// Check for airplay support
utils.toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
// If there's no autoplay attribute, assume the video is stopped and add state class
utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.config.autoplay);
// Add iOS class
utils.toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
// Add touch class
utils.toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
}
// Inject the player wrapper
if (this.isVideo) {
// Create the wrapper div
@ -59,6 +37,13 @@ const media = {
// Wrap the video in a container
utils.wrap(this.media, this.elements.wrapper);
// Faux poster container
this.elements.poster = utils.createElement('div', {
class: this.config.classNames.poster,
});
this.elements.wrapper.appendChild(this.elements.poster);
}
if (this.isEmbed) {
@ -75,8 +60,6 @@ const media = {
break;
}
} else if (this.isHTML5) {
ui.setTitle.call(this);
html5.extend.call(this);
}
},

View File

@ -6,8 +6,8 @@
/* global google */
import utils from '../utils';
import i18n from '../i18n';
import utils from '../utils';
class Ads {
/**
@ -18,7 +18,6 @@ class Ads {
constructor(player) {
this.player = player;
this.publisherId = player.config.ads.publisherId;
this.enabled = player.isHTML5 && player.isVideo && player.config.ads.enabled && utils.is.string(this.publisherId) && this.publisherId.length;
this.playing = false;
this.initialized = false;
this.elements = {
@ -44,6 +43,10 @@ class Ads {
this.load();
}
get enabled() {
return this.player.isVideo && this.player.config.ads.enabled && !utils.is.empty(this.publisherId);
}
/**
* Load the IMA SDK
*/
@ -52,7 +55,7 @@ class Ads {
// Check if the Google IMA3 SDK is loaded or load it ourselves
if (!utils.is.object(window.google) || !utils.is.object(window.google.ima)) {
utils
.loadScript(this.player.config.urls.googleIMA.api)
.loadScript(this.player.config.urls.googleIMA.sdk)
.then(() => {
this.ready();
})
@ -160,6 +163,9 @@ class Ads {
// We only overlay ads as we only support video.
request.forceNonLinearFullSlot = false;
// Mute based on current state
request.setAdWillPlayMuted(!this.player.muted);
this.loader.requestAds(request);
} catch (e) {
this.onAdError(e);
@ -226,7 +232,7 @@ class Ads {
// Get skippable state
// TODO: Skip button
// this.manager.getAdSkippableState();
// this.player.debug.warn(this.manager.getAdSkippableState());
// Set volume to match player
this.manager.setVolume(this.player.volume);

View File

@ -2,10 +2,10 @@
// Vimeo plugin
// ==========================================================================
import utils from './../utils';
import captions from './../captions';
import controls from './../controls';
import ui from './../ui';
import utils from './../utils';
const vimeo = {
setup() {
@ -18,7 +18,7 @@ const vimeo = {
// Load the API if not already
if (!utils.is.object(window.Vimeo)) {
utils
.loadScript(this.config.urls.vimeo.api)
.loadScript(this.config.urls.vimeo.sdk)
.then(() => {
vimeo.ready.call(this);
})
@ -53,6 +53,7 @@ const vimeo = {
const options = {
loop: player.config.loop.active,
autoplay: player.autoplay,
// muted: player.muted,
byline: false,
portrait: false,
title: false,
@ -68,27 +69,49 @@ const vimeo = {
// Get from <div> if needed
if (utils.is.empty(source)) {
source = player.media.getAttribute(this.config.attributes.embed.id);
source = player.media.getAttribute(player.config.attributes.embed.id);
}
const id = utils.parseVimeoId(source);
// Build an iframe
const iframe = utils.createElement('iframe');
const src = `https://player.vimeo.com/video/${id}?${params}`;
const src = utils.format(player.config.urls.vimeo.iframe, id, params);
iframe.setAttribute('src', src);
iframe.setAttribute('allowfullscreen', '');
iframe.setAttribute('allowtransparency', '');
iframe.setAttribute('allow', 'autoplay');
// Inject the package
const wrapper = utils.createElement('div');
const wrapper = utils.createElement('div', { class: player.config.classNames.embedContainer });
wrapper.appendChild(iframe);
player.media = utils.replaceElement(wrapper, player.media);
// Get poster image
utils.fetch(utils.format(player.config.urls.vimeo.api, id), 'json').then(response => {
if (utils.is.empty(response)) {
return;
}
// Get the URL for thumbnail
const url = new URL(response[0].thumbnail_large);
// Get original image
url.pathname = `${url.pathname.split('_')[0]}.jpg`;
// Set attribute
player.media.setAttribute('poster', url.href);
// Update
ui.setPoster.call(player);
});
// Setup instance
// https://github.com/vimeo/player.js
player.embed = new window.Vimeo.Player(iframe);
player.embed = new window.Vimeo.Player(iframe, {
autopause: player.config.autopause,
muted: player.muted,
});
player.media.paused = true;
player.media.currentTime = 0;

View File

@ -2,9 +2,9 @@
// YouTube plugin
// ==========================================================================
import utils from './../utils';
import controls from './../controls';
import ui from './../ui';
import utils from './../utils';
// Standardise YouTube quality unit
function mapQualityUnit(input) {
@ -77,7 +77,7 @@ const youtube = {
youtube.ready.call(this);
} else {
// Load the API
utils.loadScript(this.config.urls.youtube.api).catch(error => {
utils.loadScript(this.config.urls.youtube.sdk).catch(error => {
this.debug.warn('YouTube API failed to load', error);
});
@ -117,7 +117,7 @@ const youtube = {
// Or via Google API
const key = this.config.keys.google;
if (utils.is.string(key) && !utils.is.empty(key)) {
const url = `https://www.googleapis.com/youtube/v3/videos?id=${videoId}&key=${key}&fields=items(snippet(title))&part=snippet`;
const url = utils.format(this.config.urls.youtube.api, videoId, key);
utils
.fetch(url)
@ -161,6 +161,9 @@ const youtube = {
const container = utils.createElement('div', { id });
player.media = utils.replaceElement(container, player.media);
// Set poster image
player.media.setAttribute('poster', utils.format(player.config.urls.youtube.poster, videoId));
// Setup instance
// https://developers.google.com/youtube/iframe_api_reference
player.embed = new window.YT.Player(id, {

View File

@ -1,26 +1,24 @@
// ==========================================================================
// Plyr
// plyr.js v3.2.4
// plyr.js v3.3.6
// https://github.com/sampotts/plyr
// License: The MIT License (MIT)
// ==========================================================================
import { providers, types } from './types';
import defaults from './defaults';
import support from './support';
import utils from './utils';
import captions from './captions';
import Console from './console';
import controls from './controls';
import defaults from './defaults';
import Fullscreen from './fullscreen';
import Listeners from './listeners';
import Storage from './storage';
import Ads from './plugins/ads';
import captions from './captions';
import controls from './controls';
import media from './media';
import Ads from './plugins/ads';
import source from './source';
import Storage from './storage';
import support from './support';
import { providers, types } from './types';
import ui from './ui';
import utils from './utils';
// Private properties
// TODO: Use a WeakMap for private globals
@ -134,17 +132,9 @@ class Plyr {
}
// Cache original element state for .destroy()
// TODO: Investigate a better solution as I suspect this causes reported double load issues?
setTimeout(() => {
const clone = this.media.cloneNode(true);
// Prevent the clone autoplaying
if (clone.getAttribute('autoplay')) {
clone.pause();
}
clone.autoplay = false;
this.elements.original = clone;
}, 0);
// Set media type based on tag or data attribute
// Supported: video, audio, vimeo, youtube
@ -343,11 +333,6 @@ class Plyr {
return null;
}
// If ads are enabled, wait for them first
/* if (this.ads.enabled && !this.ads.initialized) {
return this.ads.managerPromise.then(() => this.ads.play()).catch(() => this.media.play());
} */
// Return the promise (for HTML5)
return this.media.play();
}
@ -363,6 +348,13 @@ class Plyr {
this.media.pause();
}
/**
* Get playing state
*/
get playing() {
return Boolean(this.ready && !this.paused && !this.ended);
}
/**
* Get paused state
*/
@ -371,10 +363,10 @@ class Plyr {
}
/**
* Get playing state
* Get stopped state
*/
get playing() {
return Boolean(this.ready && !this.paused && !this.ended && (this.isHTML5 ? this.media.readyState > 2 : true));
get stopped() {
return Boolean(this.paused && this.currentTime === 0);
}
/**
@ -404,7 +396,8 @@ class Plyr {
*/
stop() {
if (this.isHTML5) {
this.media.load();
this.pause();
this.restart();
} else if (utils.is.function(this.media.stop)) {
this.media.stop();
}
@ -799,17 +792,18 @@ class Plyr {
}
/**
* Set the poster image for a HTML5 video
* Set the poster image for a video
* @param {input} - the URL for the new poster image
*/
set poster(input) {
if (!this.isHTML5 || !this.isVideo) {
this.debug.warn('Poster can only be set on HTML5 video');
if (!this.isVideo) {
this.debug.warn('Poster can only be set for video');
return;
}
if (utils.is.string(input)) {
this.media.setAttribute('poster', input);
ui.setPoster.call(this);
}
}
@ -817,7 +811,7 @@ class Plyr {
* Get the current poster image
*/
get poster() {
if (!this.isHTML5 || !this.isVideo) {
if (!this.isVideo) {
return null;
}
@ -1076,8 +1070,8 @@ class Plyr {
utils.toggleClass(this.elements.controls, this.config.classNames.noTransition, false);
}
// Check if controls toggled
const toggled = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, true);
// Set hideControls class
const toggled = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, this.config.hideControls);
// Trigger event and close menu
if (toggled) {
@ -1250,6 +1244,29 @@ class Plyr {
static loadSprite(url, id) {
return utils.loadSprite(url, id);
}
/**
* Setup multiple instances
* @param {*} selector
* @param {object} options
*/
static setup(selector, options = {}) {
let targets = null;
if (utils.is.string(selector)) {
targets = Array.from(document.querySelectorAll(selector));
} else if (utils.is.nodeList(selector)) {
targets = Array.from(selector);
} else if (utils.is.array(selector)) {
targets = selector.filter(i => utils.is.element(i));
}
if (utils.is.empty(targets)) {
return null;
}
return targets.map(t => new Plyr(t, options));
}
}
export default Plyr;

View File

@ -1,14 +1,12 @@
// ==========================================================================
// Plyr Polyfilled Build
// plyr.js v3.2.4
// plyr.js v3.3.6
// https://github.com/sampotts/plyr
// License: The MIT License (MIT)
// ==========================================================================
import 'babel-polyfill';
import 'custom-event-polyfill';
import Plyr from './plyr';
export default Plyr;

View File

@ -2,12 +2,12 @@
// Plyr source update
// ==========================================================================
import { providers } from './types';
import utils from './utils';
import html5 from './html5';
import media from './media';
import ui from './ui';
import support from './support';
import { providers } from './types';
import ui from './ui';
import utils from './utils';
const source = {
// Add elements to HTML5 media (source, tracks, etc)
@ -94,8 +94,8 @@ const source = {
if (this.config.autoplay) {
this.media.setAttribute('autoplay', '');
}
if ('poster' in input) {
this.media.setAttribute('poster', input.poster);
if (!utils.is.empty(input.poster)) {
this.poster = input.poster;
}
if (this.config.loop.active) {
this.media.setAttribute('loop', '');

View File

@ -2,10 +2,14 @@
// Plyr UI
// ==========================================================================
import utils from './utils';
import captions from './captions';
import controls from './controls';
import i18n from './i18n';
import support from './support';
import utils from './utils';
// Sniff out the browser
const browser = utils.getBrowser();
const ui = {
addStyleHook() {
@ -78,6 +82,18 @@ const ui = {
// Update the UI
ui.checkPlaying.call(this);
// Check for picture-in-picture support
utils.toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo);
// Check for airplay support
utils.toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
// Add iOS class
utils.toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
// Add touch class
utils.toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
// Ready for API calls
this.ready = true;
@ -88,6 +104,9 @@ const ui = {
// Set the title
ui.setTitle.call(this);
// Set the poster image
ui.setPoster.call(this);
},
// Setup aria attribute for play and iframe title
@ -121,20 +140,38 @@ const ui = {
// Default to media type
const title = !utils.is.empty(this.config.title) ? this.config.title : 'video';
const format = i18n.get('frameTitle', this.config);
iframe.setAttribute('title', i18n.get('frameTitle', this.config));
iframe.setAttribute('title', format.replace('{title}', title));
}
},
// Set the poster image
setPoster() {
if (!utils.is.element(this.elements.poster) || utils.is.empty(this.poster)) {
return;
}
// Set the inline style
const posters = this.poster.split(',');
this.elements.poster.style.backgroundImage = posters.map(p => `url('${p}')`).join(',');
},
// Check playing state
checkPlaying() {
checkPlaying(event) {
// Class hooks
utils.toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.paused);
utils.toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);
// Set ARIA state
utils.toggleState(this.elements.buttons.play, this.playing);
// Only update controls on non timeupdate events
if (utils.is.event(event) && event.type === 'timeupdate') {
return;
}
// Toggle controls
this.toggleControls(!this.playing);
},

View File

@ -3,7 +3,6 @@
// ==========================================================================
import loadjs from 'loadjs';
import support from './support';
import { providers } from './types';
@ -269,14 +268,14 @@ const utils = {
parent.appendChild(utils.createElement(type, attributes, text));
},
// Remove an element
// Remove element(s)
removeElement(element) {
if (!utils.is.element(element) || !utils.is.element(element.parentNode)) {
if (utils.is.nodeList(element) || utils.is.array(element)) {
Array.from(element).forEach(utils.removeElement);
return;
}
if (utils.is.nodeList(element) || utils.is.array(element)) {
Array.from(element).forEach(utils.removeElement);
if (!utils.is.element(element) || !utils.is.element(element.parentNode)) {
return;
}
@ -435,60 +434,6 @@ const utils = {
return this.elements.container.querySelector(selector);
},
// Find the UI controls and store references in custom controls
// TODO: Allow settings menus with custom controls
findElements() {
try {
this.elements.controls = utils.getElement.call(this, this.config.selectors.controls.wrapper);
// Buttons
this.elements.buttons = {
play: utils.getElements.call(this, this.config.selectors.buttons.play),
pause: utils.getElement.call(this, this.config.selectors.buttons.pause),
restart: utils.getElement.call(this, this.config.selectors.buttons.restart),
rewind: utils.getElement.call(this, this.config.selectors.buttons.rewind),
fastForward: utils.getElement.call(this, this.config.selectors.buttons.fastForward),
mute: utils.getElement.call(this, this.config.selectors.buttons.mute),
pip: utils.getElement.call(this, this.config.selectors.buttons.pip),
airplay: utils.getElement.call(this, this.config.selectors.buttons.airplay),
settings: utils.getElement.call(this, this.config.selectors.buttons.settings),
captions: utils.getElement.call(this, this.config.selectors.buttons.captions),
fullscreen: utils.getElement.call(this, this.config.selectors.buttons.fullscreen),
};
// Progress
this.elements.progress = utils.getElement.call(this, this.config.selectors.progress);
// Inputs
this.elements.inputs = {
seek: utils.getElement.call(this, this.config.selectors.inputs.seek),
volume: utils.getElement.call(this, this.config.selectors.inputs.volume),
};
// Display
this.elements.display = {
buffer: utils.getElement.call(this, this.config.selectors.display.buffer),
currentTime: utils.getElement.call(this, this.config.selectors.display.currentTime),
duration: utils.getElement.call(this, this.config.selectors.display.duration),
};
// Seek tooltip
if (utils.is.element(this.elements.progress)) {
this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`);
}
return true;
} catch (error) {
// Log it
this.debug.warn('It looks like there is a problem with your custom controls HTML', error);
// Restore native video controls
this.toggleNativeControls(true);
return false;
}
},
// Get the focused element
getFocusElement() {
let focused = document.activeElement;
@ -632,6 +577,15 @@ const utils = {
element.setAttribute('aria-pressed', state);
},
// Format string
format(input, ...args) {
if (utils.is.empty(input)) {
return input;
}
return input.toString().replace(/{(\d+)}/g, (match, i) => utils.is.string(args[i]) ? args[i] : '');
},
// Get percentage
getPercentage(current, max) {
if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {
@ -769,7 +723,7 @@ const utils = {
}
// Vimeo
if (/^https?:\/\/player.vimeo.com\/video\/\d{8,}(?=\b|\/)/.test(url)) {
if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) {
return providers.vimeo;
}

View File

@ -29,6 +29,7 @@
button {
font: inherit;
line-height: inherit;
width: auto;
}
// Ignore focus

View File

@ -32,8 +32,8 @@ $embed-padding: ((100 / 16) * 9);
pointer-events: none;
}
// Vimeo hack
> div {
// Only used for Vimeo
> .plyr__video-embed__container {
padding-bottom: to-percentage($height);
position: relative;
transform: translateY(-$offset);

View File

@ -0,0 +1,22 @@
// --------------------------------------------------------------
// Faux poster overlay
// --------------------------------------------------------------
.plyr__poster {
background-color: #000;
background-position: 50% 50%;
background-repeat: no-repeat;
background-size: 100% 100%;
height: 100%;
left: 0;
opacity: 0;
position: absolute;
top: 0;
transition: opacity 0.3s ease;
width: 100%;
z-index: 1;
}
.plyr--stopped .plyr__poster {
opacity: 1;
}

View File

@ -19,6 +19,7 @@
transform: translate(-50%, 10px) scale(0.8);
transform-origin: 50% 100%;
transition: transform 0.2s 0.1s ease, opacity 0.2s 0.1s ease;
white-space: nowrap;
z-index: 2;
// The background triangle

View File

@ -32,6 +32,7 @@
@import 'components/embed';
@import 'components/menus';
@import 'components/progress';
@import 'components/poster';
@import 'components/sliders';
@import 'components/times';
@import 'components/tooltips';

View File

@ -548,6 +548,30 @@ babel-core@^6.26.0:
slash "^1.0.0"
source-map "^0.5.6"
babel-core@^6.26.3:
version "6.26.3"
resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.3.tgz#b2e2f09e342d0f0c88e2f02e067794125e75c207"
dependencies:
babel-code-frame "^6.26.0"
babel-generator "^6.26.0"
babel-helpers "^6.24.1"
babel-messages "^6.23.0"
babel-register "^6.26.0"
babel-runtime "^6.26.0"
babel-template "^6.26.0"
babel-traverse "^6.26.0"
babel-types "^6.26.0"
babylon "^6.18.0"
convert-source-map "^1.5.1"
debug "^2.6.9"
json5 "^0.5.1"
lodash "^4.17.4"
minimatch "^3.0.4"
path-is-absolute "^1.0.1"
private "^0.1.8"
slash "^1.0.0"
source-map "^0.5.7"
babel-eslint@^8.2.3:
version "8.2.3"
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-8.2.3.tgz#1a2e6681cc9bc4473c32899e59915e19cd6733cf"
@ -1612,7 +1636,7 @@ contains-path@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a"
convert-source-map@1.X, convert-source-map@^1.1.0, convert-source-map@^1.5.0:
convert-source-map@1.X, convert-source-map@^1.1.0, convert-source-map@^1.5.0, convert-source-map@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5"
@ -1716,6 +1740,10 @@ css@2.X, css@^2.2.1:
source-map-resolve "^0.3.0"
urix "^0.1.0"
cssesc@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-1.0.1.tgz#ef7bd8d0229ed6a3a7051ff7771265fe7330e0a8"
csso@~2.3.1:
version "2.3.2"
resolved "https://registry.yarnpkg.com/csso/-/csso-2.3.2.tgz#ddd52c587033f49e94b71fc55569f252e8ff5f85"
@ -2971,9 +2999,9 @@ gulp-better-rollup@^3.1.0:
vinyl "^2.1.0"
vinyl-sourcemaps-apply "^0.2.1"
gulp-clean-css@^3.9.3:
version "3.9.3"
resolved "https://registry.yarnpkg.com/gulp-clean-css/-/gulp-clean-css-3.9.3.tgz#47bf7ad62f44970f86e4ac4bdeed68ad904e65c5"
gulp-clean-css@^3.9.4:
version "3.9.4"
resolved "https://registry.yarnpkg.com/gulp-clean-css/-/gulp-clean-css-3.9.4.tgz#c6d3f8bb7a600fbe661962a72348a330954d343b"
dependencies:
clean-css "4.1.11"
plugin-error "1.0.1"
@ -4829,8 +4857,8 @@ nanomatch@^1.2.9:
to-regex "^3.0.1"
natives@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/natives/-/natives-1.1.1.tgz#011acce1f7cbd87f7ba6b3093d6cd9392be1c574"
version "1.1.3"
resolved "https://registry.yarnpkg.com/natives/-/natives-1.1.3.tgz#44a579be64507ea2d6ed1ca04a9415915cf75558"
natural-compare@^1.4.0:
version "1.4.0"
@ -5715,6 +5743,14 @@ postcss-selector-parser@^3.1.0, postcss-selector-parser@^3.1.1:
indexes-of "^1.0.1"
uniq "^1.0.1"
postcss-selector-parser@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-4.0.0.tgz#50c6570f40579036d8e63f23e6c0626fe5743527"
dependencies:
cssesc "^1.0.1"
indexes-of "^1.0.1"
uniq "^1.0.1"
postcss-sorting@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/postcss-sorting/-/postcss-sorting-3.1.0.tgz#af7c90ee73ad12569a57664eaf06735c2e25bec0"
@ -5818,7 +5854,7 @@ pretty-hrtime@^1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
private@^0.1.6, private@^0.1.7:
private@^0.1.6, private@^0.1.7, private@^0.1.8:
version "0.1.8"
resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
@ -6460,9 +6496,9 @@ rollup-plugin-babel@^3.0.4:
dependencies:
rollup-pluginutils "^1.5.0"
rollup-plugin-commonjs@^9.1.0:
version "9.1.0"
resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-9.1.0.tgz#468341aab32499123ee9a04b22f51d9bf26fdd94"
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"
dependencies:
estree-walker "^0.5.1"
magic-string "^0.22.4"
@ -7058,14 +7094,14 @@ stylelint-scss@^2.0.0:
postcss-selector-parser "^3.1.1"
postcss-value-parser "^3.3.0"
stylelint-scss@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-3.0.1.tgz#bc062e818add985f19dee98f7f5b4bff4f38706e"
stylelint-scss@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-3.1.0.tgz#aa46503014d1a6edb2fb4c5fefb73a7d0d5bc644"
dependencies:
lodash "^4.17.4"
postcss-media-query-parser "^0.2.3"
postcss-resolve-nested-selector "^0.1.1"
postcss-selector-parser "^3.1.1"
postcss-selector-parser "^4.0.0"
postcss-value-parser "^3.3.0"
stylelint-selector-bem-pattern@^2.0.0: