Compare commits
65 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
0c9759455c | ||
|
ae59c117b5 | ||
|
8da1a78342 | ||
|
747c5f5f31 | ||
|
39c6049850 | ||
|
0202e8efb0 | ||
|
e17d0220c0 | ||
|
aa4eaec742 | ||
|
851c39d44e | ||
|
f012a7df36 | ||
|
b8c40ead0a | ||
|
874bb8d3e8 | ||
|
2b3e483b36 | ||
|
69ef4ec827 | ||
|
d6cc19f2d8 | ||
|
d67eea5942 | ||
|
c4a566dbff | ||
|
185bb4c7cc | ||
|
d9161212ff | ||
|
ed0b901d11 | ||
|
62436d8e8e | ||
|
5731245f4f | ||
|
41e2454bd2 | ||
|
9248e322fc | ||
|
fa634a1bbb | ||
|
f7d2938f42 | ||
|
7c88b58a1a | ||
|
20bf5a8833 | ||
|
de0402bebf | ||
|
db1b89b1c1 | ||
|
ea3675fcdc | ||
|
e8beabd6a8 | ||
|
433acd6f41 | ||
|
69ced1313a | ||
|
511a7db7c2 | ||
|
6a62e3d085 | ||
|
aba00d0ab6 | ||
|
c115dfc6a6 | ||
|
6030b300f6 | ||
|
fff26351c9 | ||
|
e0fb524382 | ||
|
ebda039395 | ||
|
ed456197f5 | ||
|
66187c348a | ||
|
cacaef7def | ||
|
42b8f7bdab | ||
|
c90f044dac | ||
|
5f2cb90bc6 | ||
|
537ad2fe7d | ||
|
e7621bec0f | ||
|
e3acba5859 | ||
|
d7086074e8 | ||
|
04e6c43da7 | ||
|
73bfb5211b | ||
|
4a78b656da | ||
|
776fd099f3 | ||
|
bdcd98f4a7 | ||
|
01417f958a | ||
|
412d5df06e | ||
|
3e35461e04 | ||
|
1edec28d86 | ||
|
99a7e9e1d7 | ||
|
24eef153cb | ||
|
3f09cf566b | ||
|
29318591c0 |
1
.node-version
Normal file
1
.node-version
Normal file
@ -0,0 +1 @@
|
||||
19.7.0
|
67
CHANGELOG.md
67
CHANGELOG.md
@ -1,3 +1,58 @@
|
||||
# Changelog
|
||||
|
||||
### v3.7.8
|
||||
|
||||
- Feat: Minor demo style tweaks
|
||||
- Fix: Minor style fixes related to backgrounds and border radii (🚨 Requires a SCSS/CSS update 🚨)
|
||||
|
||||
### v3.7.7
|
||||
|
||||
- Fix (Accessibility): Don’t set tabindex on parent container
|
||||
- Fix (Accessibility): Add `role="timer"` to time elements
|
||||
- Fix (Accessibility): Leverage native `:focus-visible` in CSS, instead of a custom solution (🚨 Requires a SCSS/CSS update 🚨)
|
||||
|
||||
### v3.7.6
|
||||
|
||||
- Fix: Revert postinstall script
|
||||
|
||||
### v3.7.5
|
||||
|
||||
- Fix: Replace `pnpm` with `npm` in scripts to fix build issues
|
||||
|
||||
### v3.7.4
|
||||
|
||||
- Fix: Fixed event key with space (thanks @royeden!)
|
||||
- Fix: Changing Vimeo function call from `setVolume` to `setMuted` to fix iOS issue (issue #2624) (thanks @HandreMelo and Andre Fernandes Cristofolini Melo!)
|
||||
- Fix: Call preview-thumbnails listeners() function on load (thanks @mogzol!)
|
||||
- Fix: Fullscreen improvements for iOS & iPadOS
|
||||
- Feat: Remove need for iOS-specific styling (please update [volume.scss](https://github.com/sampotts/plyr/blob/master/src/sass/components/volume.scss))
|
||||
|
||||
### v3.7.3
|
||||
|
||||
- Fix: force nowrap in progress tooltips (related: #2549) (thanks @raad-altaie!)
|
||||
- Feat(i18n): Make captions autodetect text direction (#2540) (thanks @ebraminio!)
|
||||
- Fix: fixed menu border radius bug (#2548) (thanks @raad-altaie!)
|
||||
- Chore: navigator.platform is deprecated (#2530) (thanks @stamat!)
|
||||
- Feat: Added configurable property to elements for re-use (#2489) (thanks @NoirHusky!)
|
||||
- Docs: Replace example video ID with one that still works (#2518) (thanks @luvejo!)
|
||||
- Fix: Improve accessibility on control buttons with aria-pressed (#2523) (thanks @emilkarl!)
|
||||
- Fix: Fix for calc() in newer Dart Sass versions (#2519) (thanks @ckhicks!)
|
||||
- Fix: simplify logic for isFunction assertion method
|
||||
- Chore: update types to include string for controls
|
||||
- Chore: upgrade packages
|
||||
- Chore: use `.node-version` instead of `.nvmrc`
|
||||
|
||||
### v3.7.2
|
||||
|
||||
- Fix: Add `@babel/plugin-proposal-optional-chaining` to transform optional chaining in build output
|
||||
|
||||
### v3.7.1
|
||||
|
||||
- Feat: Minor styling improvements to the preview thumbnails (🚨 Requires a SCSS/CSS update 🚨)
|
||||
- Fix: Fix invalid CSS @charset rule in Sass files (thanks @Hashen110!)
|
||||
- Chore: Replace deprecated KeyboardEvent `keyCode` references to use `key` instead (thanks @Hashen110!)
|
||||
- Various other code clean up and typo fixes (thanks @Hashen110!)
|
||||
|
||||
## v3.7.0
|
||||
|
||||
- Feat: Add markers support (🚨 Requires a SCSS/CSS update 🚨) (thanks @ForeverSc and @fengshuo!)
|
||||
@ -604,10 +659,10 @@ Because we're using the fancy new ES6 syntax, you will need to polyfill for vint
|
||||
|
||||
- Vimeo controls fix (fixes #697)
|
||||
- SVG4everybody compatibility fix
|
||||
- Allow Plyr.setup event listeners to be set up as separate event listeners (https://github.com/sampotts/plyr/pull/703)
|
||||
- Added title to the layer html template (for custom controls) (https://github.com/sampotts/plyr/pull/649)
|
||||
- Target is null bug fix (https://github.com/sampotts/plyr/pull/617)
|
||||
- fix #684 memory leaks issues after destroy (https://github.com/sampotts/plyr/pull/700)
|
||||
- Allow Plyr.setup event listeners to be set up as separate event listeners (<https://github.com/sampotts/plyr/pull/703>)
|
||||
- Added title to the layer html template (for custom controls) (<https://github.com/sampotts/plyr/pull/649>)
|
||||
- Target is null bug fix (<https://github.com/sampotts/plyr/pull/617>)
|
||||
- fix #684 memory leaks issues after destroy (<https://github.com/sampotts/plyr/pull/700>)
|
||||
|
||||
### v2.0.16
|
||||
|
||||
@ -627,8 +682,8 @@ Because we're using the fancy new ES6 syntax, you will need to polyfill for vint
|
||||
|
||||
### v2.0.12
|
||||
|
||||
- Ability to set custom `blankUrl` for source changes (https://github.com/sampotts/plyr/pull/504)
|
||||
- Ability to set caption button listener (https://github.com/sampotts/plyr/pull/468)
|
||||
- Ability to set custom `blankUrl` for source changes (<https://github.com/sampotts/plyr/pull/504>)
|
||||
- Ability to set caption button listener (<https://github.com/sampotts/plyr/pull/468>)
|
||||
|
||||
### v2.0.11
|
||||
|
||||
|
107
README.md
107
README.md
@ -7,7 +7,7 @@ Plyr is a simple, lightweight, accessible and customizable HTML5, YouTube and Vi
|
||||
|
||||
[](https://badge.fury.io/js/plyr) [](https://gitpod.io/#https://github.com/sampotts/plyr) [](https://opencollective.com/plyr)
|
||||
|
||||
[](https://plyr.io)
|
||||
[](https://plyr.io)
|
||||
|
||||
# Features
|
||||
|
||||
@ -32,7 +32,7 @@ Plyr is a simple, lightweight, accessible and customizable HTML5, YouTube and Vi
|
||||
- 🤟 **No frameworks** - written in "vanilla" ES6 JavaScript, no jQuery required
|
||||
- 💁♀️ **Sass** - to include in your build processes
|
||||
|
||||
### Demos
|
||||
## Demos
|
||||
|
||||
You can try Plyr in Codepen using our minimal templates: [HTML5 video](https://codepen.io/pen?template=bKeqpr), [HTML5 audio](https://codepen.io/pen?template=rKLywR), [YouTube](https://codepen.io/pen?template=GGqbbJ), [Vimeo](https://codepen.io/pen?template=bKeXNq). For Streaming we also have example integrations with: [Dash.js](https://codepen.io/pen?template=GRoogML), [Hls.js](https://codepen.io/pen?template=oyLKQb) and [Shaka Player](https://codepen.io/pen?template=ZRpzZO)
|
||||
|
||||
@ -117,7 +117,7 @@ Or the `<div>` non progressively enhanced method:
|
||||
|
||||
You can use Plyr as an ES6 module as follows:
|
||||
|
||||
```javascript
|
||||
```js
|
||||
import Plyr from 'plyr';
|
||||
|
||||
const player = new Plyr('#player');
|
||||
@ -132,18 +132,18 @@ Alternatively you can include the `plyr.js` script before the closing `</body>`
|
||||
</script>
|
||||
```
|
||||
|
||||
See [initialising](#initialising) for more information on advanced setups.
|
||||
See [initialising](#initializing) for more information on advanced setups.
|
||||
|
||||
You can use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript. There's 2 versions; one with and one without [polyfills](#polyfills). My recommendation would be to manage polyfills separately as part of your application but to make life easier you can use the polyfilled build.
|
||||
You can use our CDN (provided by [Cloudflare](https://www.cloudflare.com/)) for the JavaScript. There's 2 versions; one with and one without [polyfills](#polyfills). My recommendation would be to manage polyfills separately as part of your application but to make life easier you can use the polyfilled build.
|
||||
|
||||
```html
|
||||
<script src="https://cdn.plyr.io/3.7.0/plyr.js"></script>
|
||||
<script src="https://cdn.plyr.io/3.7.8/plyr.js"></script>
|
||||
```
|
||||
|
||||
...or...
|
||||
|
||||
```html
|
||||
<script src="https://cdn.plyr.io/3.7.0/plyr.polyfilled.js"></script>
|
||||
<script src="https://cdn.plyr.io/3.7.8/plyr.polyfilled.js"></script>
|
||||
```
|
||||
|
||||
## CSS
|
||||
@ -154,16 +154,24 @@ Include the `plyr.css` stylesheet into your `<head>`.
|
||||
<link rel="stylesheet" href="path/to/plyr.css" />
|
||||
```
|
||||
|
||||
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the default CSS, you can use the following:
|
||||
If you want to use our CDN (provided by [Cloudflare](https://www.cloudflare.com/)) for the default CSS, you can use the following:
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="https://cdn.plyr.io/3.7.0/plyr.css" />
|
||||
<link rel="stylesheet" href="https://cdn.plyr.io/3.7.8/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.7.0/plyr.svg`.
|
||||
The SVG sprite is loaded automatically from our CDN (provided by [Cloudflare](https://www.cloudflare.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.7.8/plyr.svg`.
|
||||
|
||||
### Self hosting
|
||||
|
||||
If you don't want to create a build system to include Plyr as an npm module, you can use the pre-built files. You have a few options:
|
||||
|
||||
- Download the files from the CDN links above, they're already minified.
|
||||
- Download the files from [unpkg](https://unpkg.com/browse/plyr/dist/) or similar services.
|
||||
- Build the project yourself using `npm i && npm run build`, which installs the dependencies and spits out a build to `dist`.
|
||||
|
||||
# Ads
|
||||
|
||||
@ -189,11 +197,10 @@ Here's a list of the properties and what they are used for:
|
||||
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
|
||||
| `--plyr-color-main` | The primary UI color. |  `#00b3ff` |
|
||||
| `--plyr-video-background` | The background color of video and poster wrappers for using alpha channel videos and poster images. | `rgba(0, 0, 0, 1)` |
|
||||
| `--plyr-tab-focus-color` | The color used for the dotted outline when an element is `:focus-visible` (equivalent) keyboard focus. | `--plyr-color-main` |
|
||||
| `--plyr-focus-visible-color` | The color used for the focus styles when an element is `:focus-visible` (keyboard focused). | `--plyr-color-main` |
|
||||
| `--plyr-badge-background` | The background color for badges in the menu. |  `#4a5464` |
|
||||
| `--plyr-badge-text-color` | The text color for badges. |  `#ffffff` |
|
||||
| `--plyr-badge-border-radius` | The border radius used for badges. | `2px` |
|
||||
| `--plyr-tab-focus-color` | The color used to highlight tab (keyboard) focus. | `--plyr-color-main` |
|
||||
| `--plyr-captions-background` | The color for the background of captions. | `rgba(0, 0, 0, 0.8)` |
|
||||
| `--plyr-captions-text-color` | The color used for the captions text. |  `#ffffff` |
|
||||
| `--plyr-control-icon-size` | The size of the icons used in the controls. | `18px` |
|
||||
@ -310,7 +317,7 @@ WebVTT captions are supported. To add a caption track, check the HTML example ab
|
||||
|
||||
## JavaScript
|
||||
|
||||
### Initialising
|
||||
### Initializing
|
||||
|
||||
You can specify a range of arguments for the constructor to use:
|
||||
|
||||
@ -324,17 +331,17 @@ _Note_: If a `NodeList`, `Array`, or jQuery object are passed, the first element
|
||||
|
||||
Passing a CSS string selector that's compatible with [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector):
|
||||
|
||||
```javascript
|
||||
```js
|
||||
const player = new Plyr('#player');
|
||||
```
|
||||
|
||||
Passing a [HTMLElement](https://developer.mozilla.org/en/docs/Web/API/HTMLElement):
|
||||
|
||||
```javascript
|
||||
```js
|
||||
const player = new Plyr(document.getElementById('player'));
|
||||
```
|
||||
|
||||
```javascript
|
||||
```js
|
||||
const player = new Plyr(document.querySelector('.js-player'));
|
||||
```
|
||||
|
||||
@ -344,13 +351,13 @@ The HTMLElement or string selector can be the target `<video>`, `<audio>`, or `<
|
||||
|
||||
You have two choices here. You can either use a simple array loop to map the constructor:
|
||||
|
||||
```javascript
|
||||
```js
|
||||
const players = Array.from(document.querySelectorAll('.js-player')).map((p) => new Plyr(p));
|
||||
```
|
||||
|
||||
...or use a static method where you can pass a [CSS string selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors), a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList), an [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) of [HTMLElement](https://developer.mozilla.org/en/docs/Web/API/HTMLElement), or a [JQuery](https://jquery.com) object:
|
||||
|
||||
```javascript
|
||||
```js
|
||||
const players = Plyr.setup('.js-player');
|
||||
```
|
||||
|
||||
@ -360,7 +367,7 @@ Both options will also return an array of instances in the order of they were in
|
||||
|
||||
The second argument for the constructor is the [options](#options) object:
|
||||
|
||||
```javascript
|
||||
```js
|
||||
const player = new Plyr('#player', {
|
||||
title: 'Example Title',
|
||||
});
|
||||
@ -375,7 +382,7 @@ Options can be passed as an object to the constructor as above or as JSON in `da
|
||||
Note the single quotes encapsulating the JSON and double quotes on the object keys. Only string values need double quotes.
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
| -------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| -------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `enabled` | Boolean | `true` | Completely disable Plyr. This would allow you to do a User Agent check or similar to programmatically enable or disable Plyr for a certain UA. Example below. |
|
||||
| `debug` | Boolean | `false` | Display debugging information in the console |
|
||||
| `controls` | Array, Function or Element | `['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen']` | If a function is passed, it is assumed your method will return either an element or HTML string for the controls. Three arguments will be passed to your function; `id` (the unique id for the player), `seektime` (the seektime step in seconds), and `title` (the media title). See [CONTROLS.md](CONTROLS.md) for more info on how the html needs to be structured. |
|
||||
@ -387,6 +394,7 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
|
||||
| `blankVideo` | String | `https://cdn.plyr.io/static/blank.mp4` | Specify a URL or path to a blank video file used to properly cancel network requests. |
|
||||
| `autoplay`² | Boolean | `false` | Autoplay the media on load. If the `autoplay` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true. |
|
||||
| `autopause`¹ | Boolean | `true` | Only allow one player playing at once. |
|
||||
| `playsinline`³ | Boolean | `true` | Allow inline playback on iOS. Note this has no effect on iPadOS. |
|
||||
| `seekTime` | Number | `10` | The time, in seconds, to seek when a user hits fast forward or rewind. |
|
||||
| `volume` | Number | `1` | A number, between 0 and 1, representing the initial volume of the player. |
|
||||
| `muted` | Boolean | `false` | Whether to start playback muted. If the `muted` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true. |
|
||||
@ -402,7 +410,7 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
|
||||
| `toggleInvert` | Boolean | `true` | Allow users to click to toggle the above. |
|
||||
| `listeners` | Object | `null` | Allows binding of event listeners to the controls before the default handlers. See the `defaults.js` for available listeners. If your handler prevents default on the event (`event.preventDefault()`), the default handler will not fire. |
|
||||
| `captions` | Object | `{ active: false, language: 'auto', update: false }` | `active`: Toggles if captions should be active by default. `language`: Sets the default language to load (if available). 'auto' uses the browser language. `update`: Listen to changes to tracks and update menu. This is needed for some streaming libraries, but can result in non-selectable language options). |
|
||||
| `fullscreen` | Object | `{ enabled: true, fallback: true, iosNative: false, container: null }` | `enabled`: Toggles whether fullscreen should be enabled. `fallback`: Allow fallback to a full-window solution (`true`/`false`/`'force'`). `iosNative`: whether to use native iOS fullscreen when entering fullscreen (no custom controls). `container`: A selector for an ancestor of the player element, allows contextual content to remain visual in fullscreen mode. Non-ancestors are ignored. |
|
||||
| `fullscreen` | Object | `{ enabled: true, fallback: true, iosNative: false, container: null }` | `enabled`: Toggles whether fullscreen should be enabled. `fallback`: Allow fallback to a full-window solution (`true`/`false`/`'force'`). `iosNative`: whether to use native iOS fullscreen when entering fullscreen (no custom controls) - note this has no effect on iPadOS. `container`: A selector for an ancestor of the player element, allows contextual content to remain visual in fullscreen mode. Non-ancestors are ignored. |
|
||||
| `ratio` | String | `null` | Force an aspect ratio for all videos. The format is `'w:h'` - e.g. `'16:9'` or `'4:3'`. If this is not specified then the default for HTML5 and Vimeo is to use the native resolution of the video. As dimensions are not available from YouTube via SDK, 16:9 is forced as a sensible default. |
|
||||
| `storage` | Object | `{ enabled: true, key: 'plyr' }` | `enabled`: Allow use of local storage to store user settings. `key`: The key name to use. |
|
||||
| `speed` | Object | `{ selected: 1, options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4] }` | `selected`: The default speed for playback. `options`: The speed options to display in the UI. YouTube and Vimeo will ignore any options outside of the 0.5-2 range, so options outside of this range will be hidden automatically. |
|
||||
@ -419,9 +427,14 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
|
||||
1. Vimeo only
|
||||
2. Autoplay is generally not recommended as it is seen as a negative user experience. It is also disabled in many browsers. Before raising issues, do your homework. More info can be found here:
|
||||
|
||||
- https://webkit.org/blog/6784/new-video-policies-for-ios/
|
||||
- https://developers.google.com/web/updates/2017/09/autoplay-policy-changes
|
||||
- https://hacks.mozilla.org/2019/02/firefox-66-to-block-automatically-playing-audible-video-and-audio/
|
||||
- <https://webkit.org/blog/6784/new-video-policies-for-ios/>
|
||||
- <https://developers.google.com/web/updates/2017/09/autoplay-policy-changes>
|
||||
- <https://hacks.mozilla.org/2019/02/firefox-66-to-block-automatically-playing-audible-video-and-audio/>
|
||||
|
||||
3. YouTube does not support programatically toggling the native fullscreen player via it's API. This means on iOS you have two options, neither being perfect:
|
||||
|
||||
- Use the fallback/faux fullscreen option which covers the whole viewport (this is the default)
|
||||
- Set `playsinline` to `false` and/or `fullscreen.iosNative` to `true` - either option hides the fullscreen toggle in the UI (because of the above API issue) and means iOS will play the video in it's native player.
|
||||
|
||||
# API
|
||||
|
||||
@ -431,7 +444,7 @@ There are methods, setters and getters on a Plyr object.
|
||||
|
||||
The easiest way to access the Plyr object is to set the return value from your call to the constructor to a variable. For example:
|
||||
|
||||
```javascript
|
||||
```js
|
||||
const player = new Plyr('#player', {
|
||||
/* options */
|
||||
});
|
||||
@ -439,7 +452,7 @@ const player = new Plyr('#player', {
|
||||
|
||||
You can also access the object through any events:
|
||||
|
||||
```javascript
|
||||
```js
|
||||
element.addEventListener('ready', (event) => {
|
||||
const player = event.detail.plyr;
|
||||
});
|
||||
@ -449,7 +462,7 @@ element.addEventListener('ready', (event) => {
|
||||
|
||||
Example method use:
|
||||
|
||||
```javascript
|
||||
```js
|
||||
player.play(); // Start playback
|
||||
player.fullscreen.enter(); // Enter fullscreen
|
||||
```
|
||||
@ -484,14 +497,14 @@ player.fullscreen.enter(); // Enter fullscreen
|
||||
|
||||
Example setters:
|
||||
|
||||
```javascript
|
||||
```js
|
||||
player.volume = 0.5; // Sets volume at 50%
|
||||
player.currentTime = 10; // Seeks to 10 seconds
|
||||
```
|
||||
|
||||
Example getters:
|
||||
|
||||
```javascript
|
||||
```js
|
||||
player.volume; // 0.5;
|
||||
player.currentTime; // 10
|
||||
player.fullscreen.active; // false;
|
||||
@ -535,7 +548,7 @@ This allows changing the player source and type on the fly.
|
||||
|
||||
Video example:
|
||||
|
||||
```javascript
|
||||
```js
|
||||
player.source = {
|
||||
type: 'video',
|
||||
title: 'Example title',
|
||||
@ -575,7 +588,7 @@ player.source = {
|
||||
|
||||
Audio example:
|
||||
|
||||
```javascript
|
||||
```js
|
||||
player.source = {
|
||||
type: 'audio',
|
||||
title: 'Example title',
|
||||
@ -594,7 +607,7 @@ player.source = {
|
||||
|
||||
YouTube example:
|
||||
|
||||
```javascript
|
||||
```js
|
||||
player.source = {
|
||||
type: 'video',
|
||||
sources: [
|
||||
@ -608,12 +621,12 @@ player.source = {
|
||||
|
||||
Vimeo example
|
||||
|
||||
```javascript
|
||||
```js
|
||||
player.source = {
|
||||
type: 'video',
|
||||
sources: [
|
||||
{
|
||||
src: '143418951',
|
||||
src: '76979871',
|
||||
provider: 'vimeo',
|
||||
},
|
||||
],
|
||||
@ -639,7 +652,7 @@ You can listen for events on the target element you setup Plyr on (see example u
|
||||
reference to the instance, you can use the `on()` API method or `addEventListener()`. Access to the API can be obtained this way through the `event.detail.plyr`
|
||||
property. Here's an example:
|
||||
|
||||
```javascript
|
||||
```js
|
||||
player.on('ready', (event) => {
|
||||
const instance = event.detail.plyr;
|
||||
});
|
||||
@ -761,21 +774,20 @@ Plyr uses ES6 which isn't supported in all browsers quite yet. This means some f
|
||||
|
||||
You can use the static method to check for support. For example
|
||||
|
||||
```javascript
|
||||
const supported = Plyr.supported('video', 'html5', true);
|
||||
```js
|
||||
const supported = Plyr.supported('video', 'html5');
|
||||
```
|
||||
|
||||
The arguments are:
|
||||
|
||||
- Media type (`audio` or `video`)
|
||||
- Provider (`html5`, `youtube` or `vimeo`)
|
||||
- Whether the player has the `playsinline` attribute (only applicable to iOS 10+)
|
||||
- Media type (`'audio' | 'video'`)
|
||||
- Provider (`'html5' | 'youtube' | 'vimeo'`)
|
||||
|
||||
## Disable support programmatically
|
||||
|
||||
The `enabled` option can be used to disable certain User Agents. For example, if you don't want to use Plyr for smartphones, you could use:
|
||||
|
||||
```javascript
|
||||
```js
|
||||
{
|
||||
enabled: !/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
|
||||
}
|
||||
@ -845,6 +857,8 @@ Plyr costs money to run, not only my time. I donate my time for free as I enjoy
|
||||
- [pressakey.com | Blog-Magazin für Videospiele](https://pressakey.com)
|
||||
- [STROLLÿN: Work with a View](https://strollyn.com)
|
||||
- [CFDA Runway360](https://runway360.cfda.com/)
|
||||
- [NKLAV | Filmmaker](https://nklav.com)
|
||||
- [GDI.JS.ORG - Google Drive Index](https://gitlab.com/GoogleDriveIndex/Google-Drive-Index)
|
||||
|
||||
If you want to be added to the list, open a pull request. It'd be awesome to see how you're using Plyr 😎
|
||||
|
||||
@ -855,13 +869,8 @@ If you want to be added to the list, open a pull request. It'd be awesome to see
|
||||
|
||||
# Thanks
|
||||
|
||||
[](https://www.fastly.com/)
|
||||
|
||||
Massive thanks to [Fastly](https://www.fastly.com/) for providing the CDN services.
|
||||
|
||||
[](https://sentry.io/)
|
||||
|
||||
Massive thanks to [Sentry](https://sentry.io/) for providing the logging services for the demo site.
|
||||
- [Cloudflare](https://www.cloudflare.com/) and [Fastly](https://www.fastly.com/) for providing the CDN services.
|
||||
- [Sentry](https://sentry.io/) for error logging service on the demo website.
|
||||
|
||||
## Contributors
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
"version": "0.2",
|
||||
"ignorePaths": ["package.json", "dist/*", "demo/node_modules/*"],
|
||||
"dictionaryDefinitions": [],
|
||||
"dictionaries": [],
|
||||
"dictionaries": ["en-gb", "softwareTerms", "html", "css", "typescript"],
|
||||
"words": [
|
||||
"autopause",
|
||||
"autoplay",
|
||||
@ -13,9 +13,11 @@
|
||||
"fastly",
|
||||
"fullscreen",
|
||||
"gordita",
|
||||
"loadjs",
|
||||
"magazin",
|
||||
"menuitemradio",
|
||||
"noupe",
|
||||
"otransitionend",
|
||||
"playsinline",
|
||||
"plyr",
|
||||
"rutheneum",
|
||||
|
@ -66,42 +66,46 @@
|
||||
<h1>Pl<span>a</span>y<span>e</span>r</h1>
|
||||
<p>
|
||||
A simple, accessible and customisable media player for
|
||||
<button type="button" class="faux-link" data-source="video">
|
||||
<button type="button" class="link" data-source="video">
|
||||
<svg class="icon">
|
||||
<title>HTML5</title>
|
||||
<path
|
||||
d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"
|
||||
></path></svg
|
||||
>Video</button
|
||||
></path>
|
||||
</svg>
|
||||
Video</button
|
||||
>,
|
||||
<button type="button" class="faux-link" data-source="audio">
|
||||
<button type="button" class="link" data-source="audio">
|
||||
<svg class="icon">
|
||||
<title>HTML5</title>
|
||||
<path
|
||||
d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"
|
||||
></path></svg
|
||||
>Audio</button
|
||||
></path>
|
||||
</svg>
|
||||
Audio</button
|
||||
>,
|
||||
<button type="button" class="faux-link" data-source="youtube">
|
||||
<button type="button" class="link" data-source="youtube">
|
||||
<svg class="icon" role="presentation">
|
||||
<title>YouTube</title>
|
||||
<path
|
||||
d="M15.8,4.8c-0.2-1.3-0.8-2.2-2.2-2.4C11.4,2,8,2,8,2S4.6,2,2.4,2.4C1,2.6,0.3,3.5,0.2,4.8C0,6.1,0,8,0,8
|
||||
s0,1.9,0.2,3.2c0.2,1.3,0.8,2.2,2.2,2.4C4.6,14,8,14,8,14s3.4,0,5.6-0.4c1.4-0.3,2-1.1,2.2-2.4C16,9.9,16,8,16,8S16,6.1,15.8,4.8z
|
||||
M6,11V5l5,3L6,11z"
|
||||
></path></svg
|
||||
>YouTube
|
||||
></path>
|
||||
</svg>
|
||||
YouTube
|
||||
</button>
|
||||
and
|
||||
<button type="button" class="faux-link" data-source="vimeo">
|
||||
<button type="button" class="link" data-source="vimeo">
|
||||
<svg class="icon" role="presentation">
|
||||
<title>Vimeo</title>
|
||||
<path
|
||||
d="M16,4.3c-0.1,1.6-1.2,3.7-3.3,6.4c-2.2,2.8-4,4.2-5.5,4.2c-0.9,0-1.7-0.9-2.4-2.6C4,9.9,3.4,5,2,5
|
||||
C1.9,5,1.5,5.3,0.8,5.8L0,4.8c0.8-0.7,3.5-3.4,4.7-3.5C5.9,1.2,6.7,2,7,3.8c0.3,2,0.8,6.1,1.8,6.1c0.9,0,2.5-3.4,2.6-4
|
||||
c0.1-0.9-0.3-1.9-2.3-1.1c0.8-2.6,2.3-3.8,4.5-3.8C15.3,1.1,16.1,2.2,16,4.3z"
|
||||
></path></svg
|
||||
>Vimeo
|
||||
></path>
|
||||
</svg>
|
||||
Vimeo
|
||||
</button>
|
||||
</p>
|
||||
|
||||
@ -177,7 +181,10 @@
|
||||
d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"
|
||||
></path>
|
||||
</svg>
|
||||
<a href="https://itunes.apple.com/au/movie/view-from-a-blue-moon/id1041586323" target="_blank"
|
||||
<a
|
||||
href="https://itunes.apple.com/au/movie/view-from-a-blue-moon/id1041586323"
|
||||
target="_blank"
|
||||
class="link"
|
||||
>View From A Blue Moon</a
|
||||
>
|
||||
© Brainfarm
|
||||
@ -191,7 +198,7 @@
|
||||
d="M14.738.326C14.548.118 14.28 0 14 0H2c-.28 0-.55.118-.738.326S.98.81 1.004 1.09l1 11c.03.317.208.603.48.767l5 3c.16.095.338.143.516.143s.356-.048.515-.143l5-3c.273-.164.452-.45.48-.767l1-11c.026-.28-.067-.557-.257-.764zM12 4H6v2h6v5.72l-4 1.334-4-1.333V9h2v1.28l2 .666 2-.667V8H4V2h8v2z"
|
||||
></path>
|
||||
</svg>
|
||||
<a href="http://www.kishibashi.com/" target="_blank"
|
||||
<a href="http://www.kishibashi.com/" target="_blank" class="link"
|
||||
>Kishi Bashi – “It All Began With A Burst”</a
|
||||
>
|
||||
© Kishi Bashi
|
||||
@ -215,7 +222,7 @@
|
||||
</li>
|
||||
<li class="plyr__cite plyr__cite--vimeo" hidden>
|
||||
<small>
|
||||
<a href="https://vimeo.com/40648169" target="_blank">Toob “Wavaphon” Music Video</a>
|
||||
<a href="https://vimeo.com/40648169" target="_blank" class="link">Toob “Wavaphon” Music Video</a>
|
||||
on
|
||||
<span class="color--vimeo">
|
||||
<svg class="icon" role="presentation">
|
||||
@ -248,7 +255,7 @@
|
||||
<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"
|
||||
class="js-shr"
|
||||
class="link js-shr"
|
||||
>tweet it</a
|
||||
>
|
||||
👍
|
||||
|
@ -4,7 +4,6 @@
|
||||
// Please see README.md in the root or github.com/sampotts/plyr
|
||||
// ==========================================================================
|
||||
|
||||
import './tab-focus';
|
||||
import 'custom-event-polyfill';
|
||||
import 'url-polyfill';
|
||||
|
||||
@ -13,13 +12,13 @@ import Shr from 'shr-buttons';
|
||||
|
||||
import Plyr from '../../../src/js/plyr';
|
||||
import sources from './sources';
|
||||
import toggleClass from './toggle-class';
|
||||
|
||||
(() => {
|
||||
const production = 'plyr.io';
|
||||
const isProduction = window.location.host.includes(production);
|
||||
|
||||
// Sentry for demo site (https://plyr.io) only
|
||||
if (window.location.host === production) {
|
||||
if (isProduction) {
|
||||
Sentry.init({
|
||||
dsn: 'https://d4ad9866ad834437a4754e23937071e4@sentry.io/305555',
|
||||
whitelistUrls: [production].map((d) => new RegExp(`https://(([a-z0-9])+(.))*${d}`)),
|
||||
@ -53,10 +52,10 @@ import toggleClass from './toggle-class';
|
||||
captions: {
|
||||
active: true,
|
||||
},
|
||||
ads: {
|
||||
enabled: window.location.host.includes(production),
|
||||
/* ads: {
|
||||
enabled: isProduction,
|
||||
publisherId: '918848828995742',
|
||||
},
|
||||
}, */
|
||||
previewThumbnails: {
|
||||
enabled: true,
|
||||
src: ['https://cdn.plyr.io/static/demo/thumbs/100p.vtt', 'https://cdn.plyr.io/static/demo/thumbs/240p.vtt'],
|
||||
@ -107,10 +106,10 @@ import toggleClass from './toggle-class';
|
||||
|
||||
function render(type) {
|
||||
// Remove active classes
|
||||
Array.from(buttons).forEach((button) => toggleClass(button.parentElement, 'active', false));
|
||||
Array.from(buttons).forEach((button) => button.parentElement.classList.toggle('active', false));
|
||||
|
||||
// Set active on parent
|
||||
toggleClass(document.querySelector(`[data-source="${type}"]`), 'active', true);
|
||||
document.querySelector(`[data-source="${type}"]`).classList.toggle('active', true);
|
||||
|
||||
// Show cite
|
||||
Array.from(document.querySelectorAll('.plyr__cite')).forEach((cite) => {
|
||||
|
@ -1,31 +0,0 @@
|
||||
// Setup tab focus
|
||||
const container = document.getElementById('container');
|
||||
const tabClassName = 'tab-focus';
|
||||
|
||||
// Remove class on blur
|
||||
document.addEventListener('focusout', (event) => {
|
||||
if (!event.target.classList || container.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.target.classList.remove(tabClassName);
|
||||
});
|
||||
|
||||
// Add classname to tabbed elements
|
||||
document.addEventListener('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(() => {
|
||||
const focused = document.activeElement;
|
||||
|
||||
if (!focused || !focused.classList || container.contains(focused)) {
|
||||
return;
|
||||
}
|
||||
|
||||
focused.classList.add(tabClassName);
|
||||
}, 10);
|
||||
});
|
@ -1,5 +0,0 @@
|
||||
// Toggle class on an element
|
||||
const toggleClass = (element, className = '', toggle = false) =>
|
||||
element && element.classList[toggle ? 'add' : 'remove'](className);
|
||||
|
||||
export default toggleClass;
|
@ -1,7 +1,9 @@
|
||||
@charset "UTF-8";
|
||||
|
||||
// ==========================================================================
|
||||
// Plyr.io Demo Page
|
||||
// ==========================================================================
|
||||
@charset 'UTF-8';
|
||||
|
||||
@import '../../../../src/sass/lib/css-vars';
|
||||
|
||||
$css-vars-use-native: true;
|
||||
|
@ -1,7 +1,8 @@
|
||||
@charset "UTF-8";
|
||||
|
||||
// ==========================================================================
|
||||
// Plyr.io Error Page
|
||||
// ==========================================================================
|
||||
@charset 'UTF-8';
|
||||
|
||||
// Settings
|
||||
@import '../settings/colors';
|
||||
|
@ -7,8 +7,7 @@
|
||||
.button__count {
|
||||
align-items: center;
|
||||
border: 0;
|
||||
border-radius: $border-radius-base;
|
||||
box-shadow: 0 1px 1px rgba(#000, 0.1);
|
||||
border-radius: $border-radius-medium;
|
||||
display: inline-flex;
|
||||
padding: ($spacing-base * 0.75);
|
||||
position: relative;
|
||||
@ -19,45 +18,49 @@
|
||||
|
||||
// Buttons
|
||||
.button {
|
||||
background: $color-button-background;
|
||||
--shadow-color: 0deg 0% 20%;
|
||||
align-items: center;
|
||||
background-color: $color-button-background;
|
||||
background-image: linear-gradient(0deg, transparent, rgba(255, 255, 255, 0.05));
|
||||
border: 1px solid darken($color-button-background, 5);
|
||||
box-shadow: 0 0.8px 1px hsl(var(--shadow-color) / 0.05), 0 1.3px 1.6px -1px hsl(var(--shadow-color) / 0.06),
|
||||
0 2.8px 3.4px -2px hsl(var(--shadow-color) / 0.07);
|
||||
color: $color-button-text;
|
||||
display: inline-flex;
|
||||
font-weight: $font-weight-bold;
|
||||
gap: 0.25rem;
|
||||
padding-left: ($spacing-base * 1.25);
|
||||
padding-right: ($spacing-base * 1.25);
|
||||
text-decoration: none;
|
||||
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: $color-button-background-hover;
|
||||
border-color: darken($color-button-background, 7);
|
||||
|
||||
// Remove the underline/border
|
||||
&::after {
|
||||
display: none;
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 2px 2px rgba(#000, 0.1);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&.tab-focus {
|
||||
@include tab-focus;
|
||||
&:focus-visible {
|
||||
@include focus-visible($color-button-background);
|
||||
}
|
||||
|
||||
&:active {
|
||||
box-shadow: none;
|
||||
top: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
// Button group
|
||||
.button--with-count {
|
||||
display: inline-flex;
|
||||
|
||||
.button .icon {
|
||||
.icon {
|
||||
filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.1));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
@ -66,19 +69,21 @@
|
||||
.button__count {
|
||||
animation: fade-in 0.2s ease;
|
||||
background: $color-button-count-background;
|
||||
border: 1px solid $color-gray-100;
|
||||
color: $color-button-count-text;
|
||||
margin-left: ($spacing-base * 0.75);
|
||||
|
||||
&::before {
|
||||
border: $arrow-size solid transparent;
|
||||
border-left-width: 0;
|
||||
border-right-color: $color-button-count-background;
|
||||
background-color: $color-button-count-background;
|
||||
border: inherit;
|
||||
border-width: 0 0 1px 1px;
|
||||
content: '';
|
||||
height: 0;
|
||||
display: block;
|
||||
height: 8px;
|
||||
position: absolute;
|
||||
right: 100%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 0;
|
||||
transform: translateY(-50%) translateX(50%) translateX(-1px) rotate(45deg);
|
||||
width: 8px;
|
||||
}
|
||||
}
|
||||
|
@ -2,15 +2,9 @@
|
||||
// Links
|
||||
// ==========================================================================
|
||||
|
||||
// Make a <button> look like an <a>
|
||||
button.faux-link {
|
||||
@extend a; // stylelint-disable-line
|
||||
@include cancel-button-styles;
|
||||
}
|
||||
|
||||
// Links
|
||||
a {
|
||||
border-bottom: 1px dotted currentColor;
|
||||
.link {
|
||||
align-items: center;
|
||||
border-bottom: 1px dashed currentColor;
|
||||
color: $color-link;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
@ -38,8 +32,8 @@ a {
|
||||
}
|
||||
}
|
||||
|
||||
&.tab-focus {
|
||||
@include tab-focus;
|
||||
&:focus-visible {
|
||||
@include focus-visible($color-link);
|
||||
}
|
||||
|
||||
&.no-border::after {
|
||||
|
@ -6,8 +6,10 @@
|
||||
|
||||
// Example players
|
||||
.plyr {
|
||||
border-radius: $border-radius-large;
|
||||
box-shadow: 0 2px 15px rgba(#000, 0.1);
|
||||
--shadow-color: 197deg 32% 65%;
|
||||
border-radius: $border-radius-2x-large;
|
||||
box-shadow: 0 0.5px 0.6px hsl(var(--shadow-color) / 0.36), 0 1.7px 1.9px -0.8px hsl(var(--shadow-color) / 0.36),
|
||||
0 4.3px 4.8px -1.7px hsl(var(--shadow-color) / 0.36), -0.1px 10.6px 11.9px -2.5px hsl(var(--shadow-color) / 0.36);
|
||||
margin: $spacing-base auto;
|
||||
|
||||
&.plyr--audio {
|
||||
@ -17,6 +19,7 @@
|
||||
|
||||
.plyr__video-wrapper::after {
|
||||
border: 1px solid rgba(#000, 0.15);
|
||||
border-bottom-color: rgba(#000, 0.25);
|
||||
border-radius: inherit;
|
||||
bottom: 0;
|
||||
content: '';
|
||||
|
@ -37,6 +37,7 @@ main {
|
||||
aside {
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
box-shadow: 0 -1px 0 rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
justify-content: center;
|
||||
@ -58,8 +59,8 @@ aside {
|
||||
a {
|
||||
color: $color-twitter;
|
||||
|
||||
&.tab-focus {
|
||||
@include tab-focus($color-twitter);
|
||||
&:focus-visible {
|
||||
@include focus-visible($color-twitter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,30 +4,11 @@
|
||||
|
||||
@use 'sass:math';
|
||||
|
||||
// Convert a <button> into an <a>
|
||||
// ---------------------------------------
|
||||
@mixin cancel-button-styles() {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
line-height: $line-height-base;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
text-align: inherit;
|
||||
text-shadow: inherit;
|
||||
user-select: text;
|
||||
vertical-align: baseline;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
// Nicer focus styles
|
||||
// ---------------------------------------
|
||||
@mixin tab-focus($color: $tab-focus-default-color) {
|
||||
box-shadow: 0 0 0 3px rgba($color, 0.35);
|
||||
outline: 0;
|
||||
@mixin focus-visible($color: $focus-default-color) {
|
||||
outline: 2px dashed $color;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
// Use rems for font sizing
|
||||
|
@ -9,3 +9,7 @@
|
||||
*::before {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
button {
|
||||
all: unset;
|
||||
}
|
||||
|
@ -39,4 +39,4 @@ $color-button-count-background: #fff;
|
||||
$color-button-count-text: $color-gray-600;
|
||||
|
||||
// Focus
|
||||
$tab-focus-default-color: #fff;
|
||||
$focus-default-color: $color-brand-primary;
|
||||
|
@ -6,8 +6,9 @@
|
||||
$arrow-size: 5px;
|
||||
|
||||
// Radii
|
||||
$border-radius-base: 4px;
|
||||
$border-radius-large: 8px;
|
||||
$border-radius-small: 4px;
|
||||
$border-radius-medium: 6px;
|
||||
$border-radius-2x-large: 12px;
|
||||
|
||||
// Background
|
||||
$page-background: linear-gradient(to left top, $color-background-from, $color-background-to);
|
||||
|
4
demo/src/sass/utilities/focus.scss
Normal file
4
demo/src/sass/utilities/focus.scss
Normal file
@ -0,0 +1,4 @@
|
||||
*:focus-visible {
|
||||
outline: 2px dotted $color-brand-primary;
|
||||
outline-offset: 2px;
|
||||
}
|
45
package.json
45
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "plyr",
|
||||
"version": "3.7.0",
|
||||
"version": "3.7.8",
|
||||
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
|
||||
"homepage": "https://plyr.io",
|
||||
"author": "Sam Potts <sam@potts.es>",
|
||||
@ -34,24 +34,25 @@
|
||||
"lint": "eslint src/js && npm run remark && stylelint **/*.scss",
|
||||
"lint:fix": "eslint --fix src/js && stylelint **/*.scss --fix",
|
||||
"remark": "remark -f --use 'validate-links=repository:\"sampotts/plyr\"' '{,!(node_modules),.?**/}*.md'",
|
||||
"deploy": "yarn lint && gulp version && gulp build && gulp deploy",
|
||||
"deploy": "npm run lint && gulp version && gulp build && gulp deploy",
|
||||
"format": "prettier --write \"./{src,demo/src}/**/*.{js,scss}\"",
|
||||
"spellcheck": "cspell \"**/*.{js,md,scss,json}\" --no-must-find-files",
|
||||
"start": "gulp"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.17.9",
|
||||
"@babel/plugin-proposal-class-properties": "^7.16.7",
|
||||
"@babel/preset-env": "^7.16.11",
|
||||
"@babel/core": "^7.20.2",
|
||||
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.18.9",
|
||||
"@babel/preset-env": "^7.20.2",
|
||||
"@sampotts/eslint-config": "1.1.7",
|
||||
"autoprefixer": "^10.4.4",
|
||||
"aws-sdk": "^2.1116.0",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"aws-sdk": "^2.1256.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"browser-sync": "^2.27.9",
|
||||
"colorette": "2.0.16",
|
||||
"cspell": "^5.19.7",
|
||||
"cssnano": "^5.1.7",
|
||||
"del": "^6.0.0",
|
||||
"browser-sync": "^2.27.10",
|
||||
"colorette": "2.0.19",
|
||||
"cspell": "^6.14.2",
|
||||
"cssnano": "^5.1.14",
|
||||
"del": "^6.1.1",
|
||||
"eslint": "^7.23.0",
|
||||
"fancy-log": "^2.0.0",
|
||||
"git-branch": "^2.0.1",
|
||||
@ -73,25 +74,25 @@
|
||||
"gulp-sourcemaps": "^3.0.0",
|
||||
"gulp-svgstore": "^9.0.0",
|
||||
"gulp-terser": "^2.1.0",
|
||||
"postcss": "^8.4.12",
|
||||
"postcss-custom-properties": "^12.1.7",
|
||||
"postcss-scss": "^4.0.3",
|
||||
"postcss": "^8.4.19",
|
||||
"postcss-custom-properties": "^12.1.9",
|
||||
"postcss-scss": "^4.0.5",
|
||||
"prettier-eslint": "^12.0.0",
|
||||
"prettier-stylelint": "^0.4.2",
|
||||
"remark-cli": "^10.0.1",
|
||||
"remark-validate-links": "^11.0.2",
|
||||
"rollup": "^2.70.2",
|
||||
"remark-cli": "^11.0.0",
|
||||
"remark-validate-links": "^12.1.0",
|
||||
"rollup": "^3.3.0",
|
||||
"rollup-plugin-babel": "^4.4.0",
|
||||
"rollup-plugin-commonjs": "^10.1.0",
|
||||
"rollup-plugin-node-resolve": "^5.2.0",
|
||||
"sass": "^1.50.0",
|
||||
"stylelint": "^14.7.1",
|
||||
"stylelint-config-prettier": "^9.0.3",
|
||||
"sass": "^1.56.1",
|
||||
"stylelint": "^14.15.0",
|
||||
"stylelint-config-prettier": "^9.0.4",
|
||||
"stylelint-config-sass-guidelines": "^9.0.1",
|
||||
"stylelint-selector-bem-pattern": "^2.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "^3.22.0",
|
||||
"core-js": "^3.26.1",
|
||||
"custom-event-polyfill": "^1.0.7",
|
||||
"loadjs": "^4.2.0",
|
||||
"rangetouch": "^2.0.1",
|
||||
|
13882
pnpm-lock.yaml
generated
Normal file
13882
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -47,6 +47,7 @@ const captions = {
|
||||
// Inject the container
|
||||
if (!is.element(this.elements.captions)) {
|
||||
this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions));
|
||||
this.elements.captions.setAttribute('dir', 'auto');
|
||||
|
||||
insertAfter(this.elements.captions, this.elements.wrapper);
|
||||
}
|
||||
|
@ -18,8 +18,7 @@ const defaults = {
|
||||
// Only allow one media playing at once (vimeo only)
|
||||
autopause: true,
|
||||
|
||||
// Allow inline playback on iOS (this effects YouTube/Vimeo - HTML5 requires the attribute present)
|
||||
// TODO: Remove iosNative fullscreen option in favour of this (logic needs work)
|
||||
// Allow inline playback on iOS
|
||||
playsinline: true,
|
||||
|
||||
// Default time to skip when rewind/fast forward
|
||||
@ -61,7 +60,7 @@ const defaults = {
|
||||
// Sprite (for icons)
|
||||
loadSprite: true,
|
||||
iconPrefix: 'plyr',
|
||||
iconUrl: 'https://cdn.plyr.io/3.7.0/plyr.svg',
|
||||
iconUrl: 'https://cdn.plyr.io/3.7.8/plyr.svg',
|
||||
|
||||
// Blank video (used to prevent errors on source change)
|
||||
blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
|
||||
@ -353,7 +352,6 @@ const defaults = {
|
||||
marker: 'plyr__progress__marker',
|
||||
hidden: 'plyr__sr-only',
|
||||
hideControls: 'plyr--hide-controls',
|
||||
isIos: 'plyr--is-ios',
|
||||
isTouch: 'plyr--is-touch',
|
||||
uiSupported: 'plyr--full-ui',
|
||||
noTransition: 'plyr--no-transition',
|
||||
@ -381,7 +379,6 @@ const defaults = {
|
||||
supported: 'plyr--airplay-supported',
|
||||
active: 'plyr--airplay-active',
|
||||
},
|
||||
tabFocus: 'plyr__tab-focus',
|
||||
previewThumbnails: {
|
||||
// Tooltip thumbs
|
||||
thumbContainer: 'plyr__preview-thumb',
|
||||
|
38
src/js/controls.js
vendored
38
src/js/controls.js
vendored
@ -383,6 +383,7 @@ const controls = {
|
||||
extend(attributes, {
|
||||
class: `${attributes.class ? attributes.class : ''} ${this.config.classNames.display.time} `.trim(),
|
||||
'aria-label': i18n.get(type, this.config),
|
||||
role: 'timer',
|
||||
}),
|
||||
'00:00',
|
||||
);
|
||||
@ -404,7 +405,7 @@ const controls = {
|
||||
'keydown keyup',
|
||||
(event) => {
|
||||
// We only care about space and ⬆️ ⬇️️ ➡️
|
||||
if (![32, 38, 39, 40].includes(event.which)) {
|
||||
if (![' ', 'ArrowUp', 'ArrowDown', 'ArrowRight'].includes(event.key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -420,13 +421,13 @@ const controls = {
|
||||
const isRadioButton = matches(menuItem, '[role="menuitemradio"]');
|
||||
|
||||
// Show the respective menu
|
||||
if (!isRadioButton && [32, 39].includes(event.which)) {
|
||||
if (!isRadioButton && [' ', 'ArrowRight'].includes(event.key)) {
|
||||
controls.showMenuPanel.call(this, type, true);
|
||||
} else {
|
||||
let target;
|
||||
|
||||
if (event.which !== 32) {
|
||||
if (event.which === 40 || (isRadioButton && event.which === 39)) {
|
||||
if (event.key !== ' ') {
|
||||
if (event.key === 'ArrowDown' || (isRadioButton && event.key === 'ArrowRight')) {
|
||||
target = menuItem.nextElementSibling;
|
||||
|
||||
if (!is.element(target)) {
|
||||
@ -450,9 +451,7 @@ const controls = {
|
||||
// Enter will fire a `click` event but we still need to manage focus
|
||||
// So we bind to keyup which fires after and set focus here
|
||||
on.call(this, menuItem, 'keyup', (event) => {
|
||||
if (event.which !== 13) {
|
||||
return;
|
||||
}
|
||||
if (event.key !== 'Return') return;
|
||||
|
||||
controls.focusFirstMenuItem.call(this, null, true);
|
||||
});
|
||||
@ -506,7 +505,7 @@ const controls = {
|
||||
menuItem,
|
||||
'click keyup',
|
||||
(event) => {
|
||||
if (is.keyboardEvent(event) && event.which !== 32) {
|
||||
if (is.keyboardEvent(event) && event.key !== ' ') {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -678,7 +677,7 @@ const controls = {
|
||||
}
|
||||
|
||||
// WebKit only
|
||||
if (!browser.isWebkit) {
|
||||
if (!browser.isWebKit && !browser.isIPadOS) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1107,7 +1106,7 @@ const controls = {
|
||||
},
|
||||
|
||||
// Focus the first menu item in a given (or visible) menu
|
||||
focusFirstMenuItem(pane, tabFocus = false) {
|
||||
focusFirstMenuItem(pane, focusVisible = false) {
|
||||
if (this.elements.settings.popup.hidden) {
|
||||
return;
|
||||
}
|
||||
@ -1120,7 +1119,7 @@ const controls = {
|
||||
|
||||
const firstItem = target.querySelector('[role^="menuitem"]');
|
||||
|
||||
setFocus.call(this, firstItem, tabFocus);
|
||||
setFocus.call(this, firstItem, focusVisible);
|
||||
},
|
||||
|
||||
// Show/hide menu
|
||||
@ -1139,7 +1138,7 @@ const controls = {
|
||||
|
||||
if (is.boolean(input)) {
|
||||
show = input;
|
||||
} else if (is.keyboardEvent(input) && input.which === 27) {
|
||||
} else if (is.keyboardEvent(input) && input.key === 'Escape') {
|
||||
show = false;
|
||||
} else if (is.event(input)) {
|
||||
// If Plyr is in a shadowDOM, the event target is set to the component, instead of the
|
||||
@ -1197,7 +1196,7 @@ const controls = {
|
||||
},
|
||||
|
||||
// Show a panel in the menu
|
||||
showMenuPanel(type = '', tabFocus = false) {
|
||||
showMenuPanel(type = '', focusVisible = false) {
|
||||
const target = this.elements.container.querySelector(`#plyr-settings-${this.id}-${type}`);
|
||||
|
||||
// Nothing to show, bail
|
||||
@ -1248,7 +1247,7 @@ const controls = {
|
||||
toggleHidden(target, false);
|
||||
|
||||
// Focus the first item
|
||||
controls.focusFirstMenuItem.call(this, target, tabFocus);
|
||||
controls.focusFirstMenuItem.call(this, target, focusVisible);
|
||||
},
|
||||
|
||||
// Set the download URL
|
||||
@ -1387,7 +1386,7 @@ const controls = {
|
||||
// Volume range control
|
||||
// Ignored on iOS as it's handled globally
|
||||
// https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html
|
||||
if (control === 'volume' && !browser.isIos) {
|
||||
if (control === 'volume' && !browser.isIos && !browser.isIPadOS) {
|
||||
// Set the attributes
|
||||
const attributes = {
|
||||
max: 1,
|
||||
@ -1527,10 +1526,7 @@ const controls = {
|
||||
pane,
|
||||
'keydown',
|
||||
(event) => {
|
||||
// We only care about <-
|
||||
if (event.which !== 37) {
|
||||
return;
|
||||
}
|
||||
if (event.key !== 'ArrowLeft') return;
|
||||
|
||||
// Prevent seek
|
||||
event.preventDefault();
|
||||
@ -1720,13 +1716,17 @@ const controls = {
|
||||
if (!is.empty(this.elements.buttons)) {
|
||||
const addProperty = (button) => {
|
||||
const className = this.config.classNames.controlPressed;
|
||||
button.setAttribute('aria-pressed', 'false');
|
||||
|
||||
Object.defineProperty(button, 'pressed', {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get() {
|
||||
return hasClass(button, className);
|
||||
},
|
||||
set(pressed = false) {
|
||||
toggleClass(button, className, pressed);
|
||||
button.setAttribute('aria-pressed', pressed ? 'true' : 'false');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -57,12 +57,10 @@ class Fullscreen {
|
||||
|
||||
// Update the UI
|
||||
this.update();
|
||||
|
||||
// this.toggle = this.toggle.bind(this);
|
||||
}
|
||||
|
||||
// Determine if native supported
|
||||
static get native() {
|
||||
static get nativeSupported() {
|
||||
return !!(
|
||||
document.fullscreenEnabled ||
|
||||
document.webkitFullscreenEnabled ||
|
||||
@ -72,16 +70,14 @@ class Fullscreen {
|
||||
}
|
||||
|
||||
// If we're actually using native
|
||||
get usingNative() {
|
||||
return Fullscreen.native && !this.forceFallback;
|
||||
get useNative() {
|
||||
return Fullscreen.nativeSupported && !this.forceFallback;
|
||||
}
|
||||
|
||||
// Get the prefix for handlers
|
||||
static get prefix() {
|
||||
// No prefix
|
||||
if (is.function(document.exitFullscreen)) {
|
||||
return '';
|
||||
}
|
||||
if (is.function(document.exitFullscreen)) return '';
|
||||
|
||||
// Check for fullscreen support by vendor prefix
|
||||
let value = '';
|
||||
@ -103,24 +99,30 @@ class Fullscreen {
|
||||
return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';
|
||||
}
|
||||
|
||||
// Determine if fullscreen is enabled
|
||||
get enabled() {
|
||||
return (
|
||||
(Fullscreen.native || this.player.config.fullscreen.fallback) &&
|
||||
this.player.config.fullscreen.enabled &&
|
||||
this.player.supported.ui &&
|
||||
this.player.isVideo
|
||||
);
|
||||
// Determine if fullscreen is supported
|
||||
get supported() {
|
||||
return [
|
||||
// Fullscreen is enabled in config
|
||||
this.player.config.fullscreen.enabled,
|
||||
// Must be a video
|
||||
this.player.isVideo,
|
||||
// Either native is supported or fallback enabled
|
||||
Fullscreen.nativeSupported || this.player.config.fullscreen.fallback,
|
||||
// YouTube has no way to trigger fullscreen, so on devices with no native support, playsinline
|
||||
// must be enabled and iosNative fullscreen must be disabled to offer the fullscreen fallback
|
||||
!this.player.isYouTube ||
|
||||
Fullscreen.nativeSupported ||
|
||||
!browser.isIos ||
|
||||
(this.player.config.playsinline && !this.player.config.fullscreen.iosNative),
|
||||
].every(Boolean);
|
||||
}
|
||||
|
||||
// Get active state
|
||||
get active() {
|
||||
if (!this.enabled) {
|
||||
return false;
|
||||
}
|
||||
if (!this.supported) return false;
|
||||
|
||||
// Fallback using classname
|
||||
if (!Fullscreen.native || this.forceFallback) {
|
||||
if (!Fullscreen.nativeSupported || this.forceFallback) {
|
||||
return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
|
||||
}
|
||||
|
||||
@ -135,13 +137,11 @@ class Fullscreen {
|
||||
get target() {
|
||||
return browser.isIos && this.player.config.fullscreen.iosNative
|
||||
? this.player.media
|
||||
: this.player.elements.fullscreen || this.player.elements.container;
|
||||
: this.player.elements.fullscreen ?? this.player.elements.container;
|
||||
}
|
||||
|
||||
onChange = () => {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
if (!this.supported) return;
|
||||
|
||||
// Update toggle button
|
||||
const button = this.player.elements.buttons.fullscreen;
|
||||
@ -159,8 +159,8 @@ class Fullscreen {
|
||||
// Store or restore scroll position
|
||||
if (toggle) {
|
||||
this.scrollPosition = {
|
||||
x: window.scrollX || 0,
|
||||
y: window.scrollY || 0,
|
||||
x: window.scrollX ?? 0,
|
||||
y: window.scrollY ?? 0,
|
||||
};
|
||||
} else {
|
||||
window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);
|
||||
@ -188,10 +188,7 @@ class Fullscreen {
|
||||
|
||||
if (toggle) {
|
||||
this.cleanupViewport = !hasProperty;
|
||||
|
||||
if (!hasProperty) {
|
||||
viewport.content += `,${property}`;
|
||||
}
|
||||
if (!hasProperty) viewport.content += `,${property}`;
|
||||
} else if (this.cleanupViewport) {
|
||||
viewport.content = viewport.content
|
||||
.split(',')
|
||||
@ -206,10 +203,8 @@ class Fullscreen {
|
||||
|
||||
// Trap focus inside container
|
||||
trapFocus = (event) => {
|
||||
// Bail if iOS, not active, not the tab key
|
||||
if (browser.isIos || !this.active || event.key !== 'Tab' || event.keyCode !== 9) {
|
||||
return;
|
||||
}
|
||||
// Bail if iOS/iPadOS, not active, not the tab key
|
||||
if (browser.isIos || browser.isIPadOS || !this.active || event.key !== 'Tab') return;
|
||||
|
||||
// Get the current focused element
|
||||
const focused = document.activeElement;
|
||||
@ -230,16 +225,12 @@ class Fullscreen {
|
||||
|
||||
// Update UI
|
||||
update = () => {
|
||||
if (this.enabled) {
|
||||
if (this.supported) {
|
||||
let mode;
|
||||
|
||||
if (this.forceFallback) {
|
||||
mode = 'Fallback (forced)';
|
||||
} else if (Fullscreen.native) {
|
||||
mode = 'Native';
|
||||
} else {
|
||||
mode = 'Fallback';
|
||||
}
|
||||
if (this.forceFallback) mode = 'Fallback (forced)';
|
||||
else if (Fullscreen.nativeSupported) mode = 'Native';
|
||||
else mode = 'Fallback';
|
||||
|
||||
this.player.debug.log(`${mode} fullscreen enabled`);
|
||||
} else {
|
||||
@ -247,14 +238,12 @@ class Fullscreen {
|
||||
}
|
||||
|
||||
// Add styling hook to show button
|
||||
toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.enabled);
|
||||
toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.supported);
|
||||
};
|
||||
|
||||
// Make an element fullscreen
|
||||
enter = () => {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
if (!this.supported) return;
|
||||
|
||||
// iOS native fullscreen doesn't need the request step
|
||||
if (browser.isIos && this.player.config.fullscreen.iosNative) {
|
||||
@ -263,7 +252,7 @@ class Fullscreen {
|
||||
} else {
|
||||
this.target.webkitEnterFullscreen();
|
||||
}
|
||||
} else if (!Fullscreen.native || this.forceFallback) {
|
||||
} else if (!Fullscreen.nativeSupported || this.forceFallback) {
|
||||
this.toggleFallback(true);
|
||||
} else if (!this.prefix) {
|
||||
this.target.requestFullscreen({ navigationUI: 'hide' });
|
||||
@ -274,15 +263,17 @@ class Fullscreen {
|
||||
|
||||
// Bail from fullscreen
|
||||
exit = () => {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
if (!this.supported) return;
|
||||
|
||||
// iOS native fullscreen
|
||||
if (browser.isIos && this.player.config.fullscreen.iosNative) {
|
||||
this.target.webkitExitFullscreen();
|
||||
if (this.player.isVimeo) {
|
||||
this.player.embed.exitFullscreen();
|
||||
} else {
|
||||
this.target.webkitEnterFullscreen();
|
||||
}
|
||||
silencePromise(this.player.play());
|
||||
} else if (!Fullscreen.native || this.forceFallback) {
|
||||
} else if (!Fullscreen.nativeSupported || this.forceFallback) {
|
||||
this.toggleFallback(false);
|
||||
} else if (!this.prefix) {
|
||||
(document.cancelFullScreen || document.exitFullscreen).call(document);
|
||||
@ -294,11 +285,8 @@ class Fullscreen {
|
||||
|
||||
// Toggle state
|
||||
toggle = () => {
|
||||
if (!this.active) {
|
||||
this.enter();
|
||||
} else {
|
||||
this.exit();
|
||||
}
|
||||
if (!this.active) this.enter();
|
||||
else this.exit();
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,6 @@ class Listeners {
|
||||
|
||||
this.handleKey = this.handleKey.bind(this);
|
||||
this.toggleMenu = this.toggleMenu.bind(this);
|
||||
this.setTabFocus = this.setTabFocus.bind(this);
|
||||
this.firstTouch = this.firstTouch.bind(this);
|
||||
}
|
||||
|
||||
@ -29,25 +28,25 @@ class Listeners {
|
||||
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;
|
||||
const { key, type, altKey, ctrlKey, metaKey, shiftKey } = event;
|
||||
const pressed = type === 'keydown';
|
||||
const repeat = pressed && key === this.lastKey;
|
||||
|
||||
// Bail if a modifier key is set
|
||||
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
|
||||
if (altKey || ctrlKey || metaKey || shiftKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the event is bubbled from the media element
|
||||
// Firefox doesn't get the keycode for whatever reason
|
||||
if (!is.number(code)) {
|
||||
// Firefox doesn't get the key for whatever reason
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Seek by the number keys
|
||||
const seekByKey = () => {
|
||||
// Seek by increment
|
||||
const seekByIncrement = (increment) => {
|
||||
// Divide the max duration into 10th's and times by the number value
|
||||
player.currentTime = (player.duration / 10) * (code - 48);
|
||||
player.currentTime = (player.duration / 10) * increment;
|
||||
};
|
||||
|
||||
// Handle the key on keydown
|
||||
@ -65,113 +64,114 @@ class Listeners {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.which === 32 && matches(focused, 'button, [role^="menuitem"]')) {
|
||||
if (event.key === ' ' && 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];
|
||||
// Which keys should we prevent default
|
||||
const preventDefault = [
|
||||
' ',
|
||||
'ArrowLeft',
|
||||
'ArrowUp',
|
||||
'ArrowRight',
|
||||
'ArrowDown',
|
||||
|
||||
// If the code is found prevent default (e.g. prevent scrolling for arrows)
|
||||
if (preventDefault.includes(code)) {
|
||||
'0',
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
'4',
|
||||
'5',
|
||||
'6',
|
||||
'7',
|
||||
'8',
|
||||
'9',
|
||||
|
||||
'c',
|
||||
'f',
|
||||
'k',
|
||||
'l',
|
||||
'm',
|
||||
];
|
||||
|
||||
// If the key is found prevent default (e.g. prevent scrolling for arrows)
|
||||
if (preventDefault.includes(key)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
switch (code) {
|
||||
case 48:
|
||||
case 49:
|
||||
case 50:
|
||||
case 51:
|
||||
case 52:
|
||||
case 53:
|
||||
case 54:
|
||||
case 55:
|
||||
case 56:
|
||||
case 57:
|
||||
// 0-9
|
||||
switch (key) {
|
||||
case '0':
|
||||
case '1':
|
||||
case '2':
|
||||
case '3':
|
||||
case '4':
|
||||
case '5':
|
||||
case '6':
|
||||
case '7':
|
||||
case '8':
|
||||
case '9':
|
||||
if (!repeat) {
|
||||
seekByKey();
|
||||
seekByIncrement(parseInt(key, 10));
|
||||
}
|
||||
break;
|
||||
|
||||
case 32:
|
||||
case 75:
|
||||
// Space and K key
|
||||
case ' ':
|
||||
case 'k':
|
||||
if (!repeat) {
|
||||
silencePromise(player.togglePlay());
|
||||
}
|
||||
break;
|
||||
|
||||
case 38:
|
||||
// Arrow up
|
||||
case 'ArrowUp':
|
||||
player.increaseVolume(0.1);
|
||||
break;
|
||||
|
||||
case 40:
|
||||
// Arrow down
|
||||
case 'ArrowDown':
|
||||
player.decreaseVolume(0.1);
|
||||
break;
|
||||
|
||||
case 77:
|
||||
// M key
|
||||
case 'm':
|
||||
if (!repeat) {
|
||||
player.muted = !player.muted;
|
||||
}
|
||||
break;
|
||||
|
||||
case 39:
|
||||
// Arrow forward
|
||||
case 'ArrowRight':
|
||||
player.forward();
|
||||
break;
|
||||
|
||||
case 37:
|
||||
// Arrow back
|
||||
case 'ArrowLeft':
|
||||
player.rewind();
|
||||
break;
|
||||
|
||||
case 70:
|
||||
// F key
|
||||
case 'f':
|
||||
player.fullscreen.toggle();
|
||||
break;
|
||||
|
||||
case 67:
|
||||
// C key
|
||||
case 'c':
|
||||
if (!repeat) {
|
||||
player.toggleCaptions();
|
||||
}
|
||||
break;
|
||||
|
||||
case 76:
|
||||
// L key
|
||||
case 'l':
|
||||
player.loop = !player.loop;
|
||||
break;
|
||||
|
||||
/* case 73:
|
||||
this.setLoop('start');
|
||||
break;
|
||||
|
||||
case 76:
|
||||
this.setLoop();
|
||||
break;
|
||||
|
||||
case 79:
|
||||
this.setLoop('end');
|
||||
break; */
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Escape is handle natively when in full screen
|
||||
// So we only need to worry about non native
|
||||
if (code === 27 && !player.fullscreen.usingNative && player.fullscreen.active) {
|
||||
if (key === 'Escape' && !player.fullscreen.usingNative && player.fullscreen.active) {
|
||||
player.fullscreen.toggle();
|
||||
}
|
||||
|
||||
// Store last code for next cycle
|
||||
this.lastKey = code;
|
||||
// Store last key for next cycle
|
||||
this.lastKey = key;
|
||||
} else {
|
||||
this.lastKey = null;
|
||||
}
|
||||
@ -193,56 +193,6 @@ class Listeners {
|
||||
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
|
||||
if (event.type !== 'focusout') {
|
||||
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;
|
||||
@ -257,9 +207,6 @@ class Listeners {
|
||||
|
||||
// Detect touch by events
|
||||
once.call(player, document.body, 'touchstart', this.firstTouch);
|
||||
|
||||
// Tab focus detection
|
||||
toggleListener.call(player, document.body, 'keydown focus blur focusout', this.setTabFocus, toggle, false, true);
|
||||
};
|
||||
|
||||
// Container listeners
|
||||
@ -661,15 +608,12 @@ class Listeners {
|
||||
elements.buttons.settings,
|
||||
'keyup',
|
||||
(event) => {
|
||||
const code = event.which;
|
||||
|
||||
// We only care about space and return
|
||||
if (![13, 32].includes(code)) {
|
||||
if (![' ', 'Enter'].includes(event.key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Because return triggers a click anyway, all we need to do is set focus
|
||||
if (code === 13) {
|
||||
if (event.key === 'Enter') {
|
||||
controls.focusFirstMenuItem.call(player, null, true);
|
||||
return;
|
||||
}
|
||||
@ -689,7 +633,7 @@ class Listeners {
|
||||
|
||||
// Escape closes menu
|
||||
this.bind(elements.settings.menu, 'keydown', (event) => {
|
||||
if (event.which === 27) {
|
||||
if (event.key === 'Escape') {
|
||||
controls.toggleMenu.call(player, event);
|
||||
}
|
||||
});
|
||||
@ -704,10 +648,9 @@ class Listeners {
|
||||
// Pause while seeking
|
||||
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 attribute = 'play-on-seeked';
|
||||
|
||||
if (is.keyboardEvent(event) && code !== 39 && code !== 37) {
|
||||
if (is.keyboardEvent(event) && !['ArrowLeft', 'ArrowRight'].includes(event.key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -799,7 +742,7 @@ class Listeners {
|
||||
});
|
||||
|
||||
// Polyfill for lower fill in <input type="range"> for webkit
|
||||
if (browser.isWebkit) {
|
||||
if (browser.isWebKit) {
|
||||
Array.from(getElements.call(player, 'input[type="range"]')).forEach((element) => {
|
||||
this.bind(element, 'input', (event) => controls.updateRangeFill.call(player, event.target));
|
||||
});
|
||||
@ -883,7 +826,7 @@ class Listeners {
|
||||
elements.inputs.volume,
|
||||
'wheel',
|
||||
(event) => {
|
||||
// Detect "natural" scroll - suppored on OS X Safari only
|
||||
// Detect "natural" scroll - supported on OS X Safari only
|
||||
// Other browsers on OS X will be inverted until support improves
|
||||
const inverted = event.webkitDirectionInvertedFromDevice;
|
||||
// Get delta from event. Invert if `inverted` is true
|
||||
|
@ -240,7 +240,7 @@ class Ads {
|
||||
|
||||
/**
|
||||
* This method is called whenever the ads are ready inside the AdDisplayContainer
|
||||
* @param {Event} adsManagerLoadedEvent
|
||||
* @param {Event} event - adsManagerLoadedEvent
|
||||
*/
|
||||
onAdsManagerLoaded = (event) => {
|
||||
// Load could occur after a source change (race condition)
|
||||
@ -581,6 +581,7 @@ class Ads {
|
||||
/**
|
||||
* Handles callbacks after an ad event was invoked
|
||||
* @param {String} event - Event type
|
||||
* @param args
|
||||
*/
|
||||
trigger = (event, ...args) => {
|
||||
const handlers = this.events[event];
|
||||
|
@ -2,6 +2,7 @@ import { createElement } from '../utils/elements';
|
||||
import { once } from '../utils/events';
|
||||
import fetch from '../utils/fetch';
|
||||
import is from '../utils/is';
|
||||
import { clamp } from '../utils/numbers';
|
||||
import { formatTime } from '../utils/time';
|
||||
|
||||
// Arg: vttDataString example: "WEBVTT\n\n1\n00:00:05.000 --> 00:00:10.000\n1080p-00001.jpg"
|
||||
@ -109,9 +110,7 @@ class PreviewThumbnails {
|
||||
this.player.elements.display.seekTooltip.hidden = this.enabled;
|
||||
}
|
||||
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
if (!this.enabled) return;
|
||||
|
||||
this.getThumbnails().then(() => {
|
||||
if (!this.enabled) {
|
||||
@ -124,6 +123,9 @@ class PreviewThumbnails {
|
||||
// Check to see if thumb container size was specified manually in CSS
|
||||
this.determineContainerAutoSizing();
|
||||
|
||||
// Set up listeners
|
||||
this.listeners();
|
||||
|
||||
this.loaded = true;
|
||||
});
|
||||
};
|
||||
@ -205,18 +207,12 @@ class PreviewThumbnails {
|
||||
};
|
||||
|
||||
startMove = (event) => {
|
||||
if (!this.loaded) {
|
||||
return;
|
||||
}
|
||||
if (!this.loaded) return;
|
||||
|
||||
if (!is.event(event) || !['touchmove', 'mousemove'].includes(event.type)) {
|
||||
return;
|
||||
}
|
||||
if (!is.event(event) || !['touchmove', 'mousemove'].includes(event.type)) return;
|
||||
|
||||
// Wait until media has a duration
|
||||
if (!this.player.media.duration) {
|
||||
return;
|
||||
}
|
||||
if (!this.player.media.duration) return;
|
||||
|
||||
if (event.type === 'touchmove') {
|
||||
// Calculate seek hover position as approx video seconds
|
||||
@ -391,7 +387,7 @@ class PreviewThumbnails {
|
||||
}
|
||||
});
|
||||
|
||||
// Only proceed if either thumbnum or thumbfilename has changed
|
||||
// Only proceed if either thumb num or thumbfilename has changed
|
||||
if (thumbNum !== this.showingThumb) {
|
||||
this.showingThumb = thumbNum;
|
||||
this.loadImage(qualityIndex);
|
||||
@ -562,11 +558,7 @@ class PreviewThumbnails {
|
||||
};
|
||||
|
||||
get currentImageContainer() {
|
||||
if (this.mouseDown) {
|
||||
return this.elements.scrubbing.container;
|
||||
}
|
||||
|
||||
return this.elements.thumb.imageContainer;
|
||||
return this.mouseDown ? this.elements.scrubbing.container : this.elements.thumb.imageContainer;
|
||||
}
|
||||
|
||||
get usingSprites() {
|
||||
@ -599,11 +591,7 @@ class PreviewThumbnails {
|
||||
}
|
||||
|
||||
get currentImageElement() {
|
||||
if (this.mouseDown) {
|
||||
return this.currentScrubbingImageElement;
|
||||
}
|
||||
|
||||
return this.currentThumbnailImageElement;
|
||||
return this.mouseDown ? this.currentScrubbingImageElement : this.currentThumbnailImageElement;
|
||||
}
|
||||
|
||||
set currentImageElement(element) {
|
||||
@ -643,46 +631,39 @@ class PreviewThumbnails {
|
||||
|
||||
// Set the size to be about a quarter of the size of video. Unless option dynamicSize === false, in which case it needs to be set in CSS
|
||||
setThumbContainerSizeAndPos = () => {
|
||||
const { imageContainer } = this.elements.thumb;
|
||||
|
||||
if (!this.sizeSpecifiedInCSS) {
|
||||
const thumbWidth = Math.floor(this.thumbContainerHeight * this.thumbAspectRatio);
|
||||
this.elements.thumb.imageContainer.style.height = `${this.thumbContainerHeight}px`;
|
||||
this.elements.thumb.imageContainer.style.width = `${thumbWidth}px`;
|
||||
} else if (
|
||||
this.elements.thumb.imageContainer.clientHeight > 20 &&
|
||||
this.elements.thumb.imageContainer.clientWidth < 20
|
||||
) {
|
||||
const thumbWidth = Math.floor(this.elements.thumb.imageContainer.clientHeight * this.thumbAspectRatio);
|
||||
this.elements.thumb.imageContainer.style.width = `${thumbWidth}px`;
|
||||
} else if (
|
||||
this.elements.thumb.imageContainer.clientHeight < 20 &&
|
||||
this.elements.thumb.imageContainer.clientWidth > 20
|
||||
) {
|
||||
const thumbHeight = Math.floor(this.elements.thumb.imageContainer.clientWidth / this.thumbAspectRatio);
|
||||
this.elements.thumb.imageContainer.style.height = `${thumbHeight}px`;
|
||||
imageContainer.style.height = `${this.thumbContainerHeight}px`;
|
||||
imageContainer.style.width = `${thumbWidth}px`;
|
||||
} else if (imageContainer.clientHeight > 20 && imageContainer.clientWidth < 20) {
|
||||
const thumbWidth = Math.floor(imageContainer.clientHeight * this.thumbAspectRatio);
|
||||
imageContainer.style.width = `${thumbWidth}px`;
|
||||
} else if (imageContainer.clientHeight < 20 && imageContainer.clientWidth > 20) {
|
||||
const thumbHeight = Math.floor(imageContainer.clientWidth / this.thumbAspectRatio);
|
||||
imageContainer.style.height = `${thumbHeight}px`;
|
||||
}
|
||||
|
||||
this.setThumbContainerPos();
|
||||
};
|
||||
|
||||
setThumbContainerPos = () => {
|
||||
const seekbarRect = this.player.elements.progress.getBoundingClientRect();
|
||||
const plyrRect = this.player.elements.container.getBoundingClientRect();
|
||||
const scrubberRect = this.player.elements.progress.getBoundingClientRect();
|
||||
const containerRect = this.player.elements.container.getBoundingClientRect();
|
||||
const { container } = this.elements.thumb;
|
||||
// Find the lowest and highest desired left-position, so we don't slide out the side of the video container
|
||||
const minVal = plyrRect.left - seekbarRect.left + 10;
|
||||
const maxVal = plyrRect.right - seekbarRect.left - container.clientWidth - 10;
|
||||
const min = containerRect.left - scrubberRect.left + 10;
|
||||
const max = containerRect.right - scrubberRect.left - container.clientWidth - 10;
|
||||
// Set preview container position to: mousepos, minus seekbar.left, minus half of previewContainer.clientWidth
|
||||
let previewPos = this.mousePosX - seekbarRect.left - container.clientWidth / 2;
|
||||
const position = this.mousePosX - scrubberRect.left - container.clientWidth / 2;
|
||||
const clamped = clamp(position, min, max);
|
||||
|
||||
if (previewPos < minVal) {
|
||||
previewPos = minVal;
|
||||
}
|
||||
// Move the popover position
|
||||
container.style.left = `${clamped}px`;
|
||||
|
||||
if (previewPos > maxVal) {
|
||||
previewPos = maxVal;
|
||||
}
|
||||
|
||||
container.style.left = `${previewPos}px`;
|
||||
// The arrow can follow the cursor
|
||||
container.style.setProperty('--preview-arrow-offset', `${position - clamped}px`);
|
||||
};
|
||||
|
||||
// Can't use 100% width, in case the video is a different aspect ratio to the video container
|
||||
@ -697,9 +678,7 @@ class PreviewThumbnails {
|
||||
|
||||
// Sprites need to be offset to the correct location
|
||||
setImageSizeAndOffset = (previewImage, frame) => {
|
||||
if (!this.usingSprites) {
|
||||
return;
|
||||
}
|
||||
if (!this.usingSprites) return;
|
||||
|
||||
// Find difference between height and preview container height
|
||||
const multiplier = this.thumbContainerHeight / frame.h;
|
||||
|
@ -113,7 +113,7 @@ const vimeo = {
|
||||
autoplay: player.autoplay,
|
||||
muted: player.muted,
|
||||
gesture: 'media',
|
||||
playsinline: !this.config.fullscreen.iosNative,
|
||||
playsinline: player.config.playsinline,
|
||||
// hash has to be added to iframe-URL
|
||||
...hashParam,
|
||||
...frameParams,
|
||||
@ -265,7 +265,7 @@ const vimeo = {
|
||||
set(input) {
|
||||
const toggle = is.boolean(input) ? input : false;
|
||||
|
||||
player.embed.setVolume(toggle ? 0 : player.config.volume).then(() => {
|
||||
player.embed.setMuted(toggle ? true : player.config.muted).then(() => {
|
||||
muted = toggle;
|
||||
triggerEvent.call(player, player.media, 'volumechange');
|
||||
});
|
||||
|
@ -131,7 +131,7 @@ const youtube = {
|
||||
const posterSrc = (s) => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`;
|
||||
|
||||
// Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)
|
||||
loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded
|
||||
loadImage(posterSrc('maxres'), 121) // Highest quality and un-padded
|
||||
.catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3
|
||||
.catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists
|
||||
.then((image) => ui.setPoster.call(player, image.src))
|
||||
@ -161,7 +161,7 @@ const youtube = {
|
||||
// Disable keyboard as we handle it
|
||||
disablekb: 1,
|
||||
// Allow iOS inline playback
|
||||
playsinline: !player.config.fullscreen.iosNative ? 1 : 0,
|
||||
playsinline: player.config.playsinline && !player.config.fullscreen.iosNative ? 1 : 0,
|
||||
// Captions are flaky on YouTube
|
||||
cc_load_policy: player.captions.active ? 1 : 0,
|
||||
cc_lang_pref: player.config.captions.language,
|
||||
@ -183,7 +183,7 @@ const youtube = {
|
||||
100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',
|
||||
101: 'The owner of the requested video does not allow it to be played in embedded players.',
|
||||
150: 'The owner of the requested video does not allow it to be played in embedded players.',
|
||||
}[code] || 'An unknown error occured';
|
||||
}[code] || 'An unknown error occurred';
|
||||
|
||||
player.media.error = { code, message };
|
||||
|
||||
|
27
src/js/plyr.d.ts
vendored
27
src/js/plyr.d.ts
vendored
@ -212,7 +212,7 @@ declare class Plyr {
|
||||
airplay(): void;
|
||||
|
||||
/**
|
||||
* Sets the preview thubmnails for the current source.
|
||||
* Sets the preview thumbnails for the current source.
|
||||
*/
|
||||
setPreviewThumbnails(source: Plyr.PreviewThumbnailsOptions): void;
|
||||
|
||||
@ -272,8 +272,8 @@ declare namespace Plyr {
|
||||
controlsshown: PlyrEvent;
|
||||
ready: PlyrEvent;
|
||||
};
|
||||
// For retrocompatibility, we keep StandadEvent
|
||||
type StandadEvent = keyof Plyr.StandardEventMap;
|
||||
// For retrocompatibility, we keep StandardEvent
|
||||
type StandardEvent = keyof Plyr.StandardEventMap;
|
||||
type Html5EventMap = {
|
||||
loadstart: PlyrEvent;
|
||||
loadeddata: PlyrEvent;
|
||||
@ -341,7 +341,7 @@ declare namespace Plyr {
|
||||
* id (the unique id for the player), seektime (the seektime step in seconds), and title (the media title). See CONTROLS.md for more info on how the html needs to be structured.
|
||||
* Defaults to ['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen']
|
||||
*/
|
||||
controls?: string[] | ((id: string, seektime: number, title: string) => unknown) | Element;
|
||||
controls?: string | string[] | ((id: string, seektime: number, title: string) => unknown) | Element;
|
||||
|
||||
/**
|
||||
* If you're using the default controls are used then you can specify which settings to show in the menu
|
||||
@ -459,7 +459,7 @@ declare namespace Plyr {
|
||||
* Allows binding of event listeners to the controls before the default handlers. See the defaults.js for available listeners.
|
||||
* If your handler prevents default on the event (event.preventDefault()), the default handler will not fire.
|
||||
*/
|
||||
listeners?: {[key: string]: (error: PlyrEvent) => void};
|
||||
listeners?: { [key: string]: (error: PlyrEvent) => void };
|
||||
|
||||
/**
|
||||
* active: Toggles if captions should be active by default. language: Sets the default language to load (if available). 'auto' uses the browser language.
|
||||
@ -523,6 +523,11 @@ declare namespace Plyr {
|
||||
* Media Metadata Options.
|
||||
*/
|
||||
mediaMetadata?: MediaMetadataOptions;
|
||||
|
||||
/**
|
||||
* Markers Options
|
||||
*/
|
||||
markers?: MarkersOptions;
|
||||
}
|
||||
|
||||
interface QualityOptions {
|
||||
@ -594,6 +599,16 @@ declare namespace Plyr {
|
||||
artwork?: MediaMetadataArtwork[];
|
||||
}
|
||||
|
||||
interface MarkersPoints {
|
||||
time: number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface MarkersOptions {
|
||||
enabled: boolean;
|
||||
points: MarkersPoints[];
|
||||
}
|
||||
|
||||
export interface Elements {
|
||||
buttons: {
|
||||
airplay?: HTMLButtonElement;
|
||||
@ -685,7 +700,7 @@ declare namespace Plyr {
|
||||
}
|
||||
|
||||
interface PlyrEvent extends CustomEvent {
|
||||
readonly detail: {readonly plyr: Plyr};
|
||||
readonly detail: { readonly plyr: Plyr };
|
||||
}
|
||||
|
||||
enum YoutubeState {
|
||||
|
@ -1,6 +1,6 @@
|
||||
// ==========================================================================
|
||||
// Plyr
|
||||
// plyr.js v3.7.0
|
||||
// plyr.js v3.7.8
|
||||
// https://github.com/sampotts/plyr
|
||||
// License: The MIT License (MIT)
|
||||
// ==========================================================================
|
||||
@ -246,7 +246,7 @@ class Plyr {
|
||||
}
|
||||
|
||||
// Check for support again but with type
|
||||
this.supported = support.check(this.type, this.provider, this.config.playsinline);
|
||||
this.supported = support.check(this.type, this.provider);
|
||||
|
||||
// If no support for even API, bail
|
||||
if (!this.supported.api) {
|
||||
@ -267,7 +267,7 @@ class Plyr {
|
||||
|
||||
// Wrap media
|
||||
if (!is.element(this.elements.container)) {
|
||||
this.elements.container = createElement('div', { tabindex: 0 });
|
||||
this.elements.container = createElement('div');
|
||||
wrap(this.media, this.elements.container);
|
||||
}
|
||||
|
||||
@ -649,7 +649,7 @@ class Plyr {
|
||||
|
||||
/**
|
||||
* Set playback speed
|
||||
* @param {Number} speed - the speed of playback (0.5-2.0)
|
||||
* @param {Number} input - the speed of playback (0.5-2.0)
|
||||
*/
|
||||
set speed(input) {
|
||||
let speed = null;
|
||||
@ -933,8 +933,7 @@ class Plyr {
|
||||
* @param {Boolean} input - Whether to autoplay or not
|
||||
*/
|
||||
set autoplay(input) {
|
||||
const toggle = is.boolean(input) ? input : this.config.autoplay;
|
||||
this.config.autoplay = toggle;
|
||||
this.config.autoplay = is.boolean(input) ? input : this.config.autoplay;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -954,7 +953,7 @@ class Plyr {
|
||||
|
||||
/**
|
||||
* Set the caption track by index
|
||||
* @param {Number} - Caption index
|
||||
* @param {Number} input - Caption index
|
||||
*/
|
||||
set currentTrack(input) {
|
||||
captions.set.call(this, input, false);
|
||||
@ -972,7 +971,7 @@ class Plyr {
|
||||
/**
|
||||
* Set the wanted language for captions
|
||||
* Since tracks can be added later it won't update the actual caption track until there is a matching track
|
||||
* @param {String} - Two character ISO language code (e.g. EN, FR, PT, etc)
|
||||
* @param {String} input - Two character ISO language code (e.g. EN, FR, PT, etc)
|
||||
*/
|
||||
set language(input) {
|
||||
captions.setLanguage.call(this, input, false);
|
||||
@ -1033,7 +1032,7 @@ class Plyr {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the preview thubmnails for the current source
|
||||
* Sets the preview thumbnails for the current source
|
||||
*/
|
||||
setPreviewThumbnails(thumbnailSource) {
|
||||
if (this.previewThumbnails && this.previewThumbnails.loaded) {
|
||||
@ -1240,10 +1239,9 @@ class Plyr {
|
||||
* Check for support
|
||||
* @param {String} type - Player type (audio/video)
|
||||
* @param {String} provider - Provider (html5/youtube/vimeo)
|
||||
* @param {Boolean} inline - Where player has `playsinline` sttribute
|
||||
*/
|
||||
static supported(type, provider, inline) {
|
||||
return support.check(type, provider, inline);
|
||||
static supported(type, provider) {
|
||||
return support.check(type, provider);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,6 +1,6 @@
|
||||
// ==========================================================================
|
||||
// Plyr Polyfilled Build
|
||||
// plyr.js v3.7.0
|
||||
// plyr.js v3.7.8
|
||||
// https://github.com/sampotts/plyr
|
||||
// License: The MIT License (MIT)
|
||||
// ==========================================================================
|
||||
|
@ -24,10 +24,9 @@ const support = {
|
||||
|
||||
// Check for support
|
||||
// Basic functionality vs full UI
|
||||
check(type, provider, playsinline) {
|
||||
const canPlayInline = browser.isIPhone && playsinline && support.playsinline;
|
||||
check(type, provider) {
|
||||
const api = support[type] || provider !== 'html5';
|
||||
const ui = api && support.rangeInput && (type !== 'video' || !browser.isIPhone || canPlayInline);
|
||||
const ui = api && support.rangeInput;
|
||||
|
||||
return {
|
||||
api,
|
||||
@ -38,6 +37,9 @@ const support = {
|
||||
// Picture-in-picture support
|
||||
// Safari & Chrome only currently
|
||||
pip: (() => {
|
||||
// While iPhone's support picture-in-picture for some apps, seemingly Safari isn't one of them
|
||||
// It will throw the following error when trying to enter picture-in-picture
|
||||
// `NotSupportedError: The Picture-in-Picture mode is not supported.`
|
||||
if (browser.isIPhone) {
|
||||
return false;
|
||||
}
|
||||
|
@ -5,7 +5,6 @@
|
||||
import captions from './captions';
|
||||
import controls from './controls';
|
||||
import support from './support';
|
||||
import browser from './utils/browser';
|
||||
import { getElement, toggleClass } from './utils/elements';
|
||||
import { ready, triggerEvent } from './utils/events';
|
||||
import i18n from './utils/i18n';
|
||||
@ -98,9 +97,6 @@ const ui = {
|
||||
// Check for airplay support
|
||||
toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
|
||||
|
||||
// Add iOS class
|
||||
toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
|
||||
|
||||
// Add touch class
|
||||
toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
|
||||
|
||||
|
@ -3,14 +3,19 @@
|
||||
// Unfortunately, due to mixed support, UA sniffing is required
|
||||
// ==========================================================================
|
||||
|
||||
const browser = {
|
||||
isIE: Boolean(window.document.documentMode),
|
||||
isEdge: window.navigator.userAgent.includes('Edge'),
|
||||
isWebkit: 'WebkitAppearance' in document.documentElement.style && !/Edge/.test(navigator.userAgent),
|
||||
isIPhone: /(iPhone|iPod)/gi.test(navigator.platform),
|
||||
isIos:
|
||||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1) ||
|
||||
/(iPad|iPhone|iPod)/gi.test(navigator.platform),
|
||||
};
|
||||
const isIE = Boolean(window.document.documentMode);
|
||||
const isEdge = /Edge/g.test(navigator.userAgent);
|
||||
const isWebKit = 'WebkitAppearance' in document.documentElement.style && !/Edge/g.test(navigator.userAgent);
|
||||
const isIPhone = /iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;
|
||||
// navigator.platform may be deprecated but this check is still required
|
||||
const isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1;
|
||||
const isIos = /iPad|iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;
|
||||
|
||||
export default browser;
|
||||
export default {
|
||||
isIE,
|
||||
isEdge,
|
||||
isWebKit,
|
||||
isIPhone,
|
||||
isIPadOS,
|
||||
isIos,
|
||||
};
|
||||
|
@ -37,9 +37,7 @@ export function wrap(elements, wrapper) {
|
||||
|
||||
// Set attributes
|
||||
export function setAttributes(element, attributes) {
|
||||
if (!is.element(element) || is.empty(attributes)) {
|
||||
return;
|
||||
}
|
||||
if (!is.element(element) || is.empty(attributes)) return;
|
||||
|
||||
// Assume null and undefined attributes should be left out,
|
||||
// Setting them would otherwise convert them to "null" and "undefined"
|
||||
@ -67,20 +65,16 @@ export function createElement(type, attributes, text) {
|
||||
return element;
|
||||
}
|
||||
|
||||
// Inaert an element after another
|
||||
// Insert an element after another
|
||||
export function insertAfter(element, target) {
|
||||
if (!is.element(element) || !is.element(target)) {
|
||||
return;
|
||||
}
|
||||
if (!is.element(element) || !is.element(target)) return;
|
||||
|
||||
target.parentNode.insertBefore(element, target.nextSibling);
|
||||
}
|
||||
|
||||
// Insert a DocumentFragment
|
||||
export function insertElement(type, parent, attributes, text) {
|
||||
if (!is.element(parent)) {
|
||||
return;
|
||||
}
|
||||
if (!is.element(parent)) return;
|
||||
|
||||
parent.appendChild(createElement(type, attributes, text));
|
||||
}
|
||||
@ -101,9 +95,7 @@ export function removeElement(element) {
|
||||
|
||||
// Remove all child elements
|
||||
export function emptyElement(element) {
|
||||
if (!is.element(element)) {
|
||||
return;
|
||||
}
|
||||
if (!is.element(element)) return;
|
||||
|
||||
let { length } = element.childNodes;
|
||||
|
||||
@ -115,9 +107,7 @@ export function emptyElement(element) {
|
||||
|
||||
// Replace element
|
||||
export function replaceElement(newChild, oldChild) {
|
||||
if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) {
|
||||
return null;
|
||||
}
|
||||
if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) return null;
|
||||
|
||||
oldChild.parentNode.replaceChild(newChild, oldChild);
|
||||
|
||||
@ -131,9 +121,7 @@ export function getAttributesFromSelector(sel, existingAttributes) {
|
||||
// '#test' to { id: 'test' }
|
||||
// '[data-test="test"]' to { 'data-test': 'test' }
|
||||
|
||||
if (!is.string(sel) || is.empty(sel)) {
|
||||
return {};
|
||||
}
|
||||
if (!is.string(sel) || is.empty(sel)) return {};
|
||||
|
||||
const attributes = {};
|
||||
const existing = extend({}, existingAttributes);
|
||||
@ -181,9 +169,7 @@ export function getAttributesFromSelector(sel, existingAttributes) {
|
||||
|
||||
// Toggle hidden
|
||||
export function toggleHidden(element, hidden) {
|
||||
if (!is.element(element)) {
|
||||
return;
|
||||
}
|
||||
if (!is.element(element)) return;
|
||||
|
||||
let hide = hidden;
|
||||
|
||||
@ -268,16 +254,9 @@ export function getElement(selector) {
|
||||
}
|
||||
|
||||
// Set focus and tab focus class
|
||||
export function setFocus(element = null, tabFocus = false) {
|
||||
if (!is.element(element)) {
|
||||
return;
|
||||
}
|
||||
export function setFocus(element = null, focusVisible = false) {
|
||||
if (!is.element(element)) return;
|
||||
|
||||
// Set regular focus
|
||||
element.focus({ preventScroll: true });
|
||||
|
||||
// If we want to mimic keyboard focus via tab
|
||||
if (tabFocus) {
|
||||
toggleClass(element, this.config.classNames.tabFocus);
|
||||
}
|
||||
element.focus({ preventScroll: true, focusVisible });
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ const isObject = (input) => getConstructor(input) === Object;
|
||||
const isNumber = (input) => getConstructor(input) === Number && !Number.isNaN(input);
|
||||
const isString = (input) => getConstructor(input) === String;
|
||||
const isBoolean = (input) => getConstructor(input) === Boolean;
|
||||
const isFunction = (input) => getConstructor(input) === Function;
|
||||
const isFunction = (input) => typeof input === 'function';
|
||||
const isArray = (input) => Array.isArray(input);
|
||||
const isWeakMap = (input) => instanceOf(input, WeakMap);
|
||||
const isNodeList = (input) => instanceOf(input, NodeList);
|
||||
|
@ -7,7 +7,7 @@
|
||||
* @param {Number} input
|
||||
* @param {Number} min The lower boundary of the output range
|
||||
* @param {Number} max The upper boundary of the output range
|
||||
* @returns A number in the range [min, max]
|
||||
* @returns A number within the bounds of min and max
|
||||
* @type Number
|
||||
*/
|
||||
export function clamp(input = 0, min = 0, max = 255) {
|
||||
|
@ -11,11 +11,9 @@ export function generateId(prefix) {
|
||||
|
||||
// Format string
|
||||
export function format(input, ...args) {
|
||||
if (is.empty(input)) {
|
||||
return input;
|
||||
}
|
||||
if (is.empty(input)) return input;
|
||||
|
||||
return input.toString().replace(/{(\d+)}/g, (match, i) => args[i].toString());
|
||||
return input.toString().replace(/{(\d+)}/g, (_, i) => args[i].toString());
|
||||
}
|
||||
|
||||
// Get percentage
|
||||
@ -27,7 +25,7 @@ export function getPercentage(current, max) {
|
||||
return ((current / max) * 100).toFixed(2);
|
||||
}
|
||||
|
||||
// Replace all occurances of a string in a string
|
||||
// Replace all occurrences of a string in a string
|
||||
export const replaceAll = (input = '', find = '', replace = '') =>
|
||||
input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString());
|
||||
|
||||
|
@ -28,8 +28,8 @@
|
||||
}
|
||||
|
||||
// Tab focus
|
||||
&.plyr__tab-focus {
|
||||
@include plyr-tab-focus;
|
||||
&:focus-visible {
|
||||
@include plyr-focus-visible;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,7 +26,7 @@
|
||||
&__container {
|
||||
animation: plyr-popup 0.2s ease;
|
||||
background: $plyr-menu-background;
|
||||
border-radius: 4px;
|
||||
border-radius: $plyr-menu-radius;
|
||||
bottom: 100%;
|
||||
box-shadow: $plyr-menu-shadow;
|
||||
color: $plyr-menu-color;
|
||||
@ -100,7 +100,7 @@
|
||||
right: calc((#{$plyr-control-padding} * 1.5) - #{$plyr-menu-item-arrow-size});
|
||||
}
|
||||
|
||||
&.plyr__tab-focus::after,
|
||||
&:focus-visible::after,
|
||||
&:hover::after {
|
||||
border-left-color: currentColor;
|
||||
}
|
||||
@ -132,7 +132,7 @@
|
||||
top: 100%;
|
||||
}
|
||||
|
||||
&.plyr__tab-focus::after,
|
||||
&:focus-visible::after,
|
||||
&:hover::after {
|
||||
border-right-color: currentColor;
|
||||
}
|
||||
@ -181,7 +181,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.plyr__tab-focus::before,
|
||||
&:focus-visible::before,
|
||||
&:hover::before {
|
||||
background: rgba($plyr-color-gray-900, 0.1);
|
||||
}
|
||||
@ -192,7 +192,7 @@
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin-left: auto;
|
||||
margin-right: calc((#{$plyr-control-padding} - 2) * -1);
|
||||
margin-right: calc((#{$plyr-control-padding} - 2px) * -1);
|
||||
overflow: hidden;
|
||||
padding-left: calc(#{$plyr-control-padding} * 3.5);
|
||||
pointer-events: none;
|
||||
|
@ -27,7 +27,6 @@ $plyr-progress-offset: $plyr-range-thumb-height;
|
||||
left: 0;
|
||||
max-width: 120px;
|
||||
overflow-wrap: break-word;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -83,17 +83,17 @@
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&.plyr__tab-focus {
|
||||
&:focus-visible {
|
||||
&::-webkit-slider-runnable-track {
|
||||
@include plyr-tab-focus;
|
||||
@include plyr-focus-visible;
|
||||
}
|
||||
|
||||
&::-moz-range-track {
|
||||
@include plyr-tab-focus;
|
||||
@include plyr-focus-visible;
|
||||
}
|
||||
|
||||
&::-ms-track {
|
||||
@include plyr-tab-focus;
|
||||
@include plyr-focus-visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -42,7 +42,7 @@
|
||||
|
||||
// Displaying
|
||||
.plyr .plyr__control:hover .plyr__tooltip,
|
||||
.plyr .plyr__control.plyr__tab-focus .plyr__tooltip,
|
||||
.plyr .plyr__control:focus-visible .plyr__tooltip,
|
||||
.plyr__tooltip--visible {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, 0) scale(1);
|
||||
@ -82,7 +82,7 @@
|
||||
.plyr__controls > .plyr__control:first-child + .plyr__control,
|
||||
.plyr__controls > .plyr__control:last-child {
|
||||
&:hover .plyr__tooltip,
|
||||
&.plyr__tab-focus .plyr__tooltip,
|
||||
&:focus-visible .plyr__tooltip,
|
||||
.plyr__tooltip--visible {
|
||||
transform: translate(0, 0) scale(1);
|
||||
}
|
||||
|
@ -5,21 +5,14 @@
|
||||
.plyr__volume {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
max-width: 110px;
|
||||
min-width: 80px;
|
||||
position: relative;
|
||||
width: 20%;
|
||||
|
||||
input[type='range'] {
|
||||
margin-left: calc(#{$plyr-control-spacing} / 2);
|
||||
margin-right: calc(#{$plyr-control-spacing} / 2);
|
||||
max-width: 90px;
|
||||
min-width: 60px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto size on iOS as there's no slider
|
||||
.plyr--is-ios .plyr__volume {
|
||||
min-width: 0;
|
||||
width: auto;
|
||||
}
|
||||
|
@ -4,8 +4,8 @@
|
||||
|
||||
// Nicer focus styles
|
||||
// ---------------------------------------
|
||||
@mixin plyr-tab-focus($color: $plyr-tab-focus-color) {
|
||||
outline: $color dotted 3px;
|
||||
@mixin plyr-focus-visible($color: $plyr-focus-visible-color) {
|
||||
outline: 2px dashed $color;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
|
@ -32,7 +32,7 @@
|
||||
bottom: calc(#{$plyr-preview-arrow-size} * -1);
|
||||
content: '';
|
||||
height: 0;
|
||||
left: 50%;
|
||||
left: calc(50% + var(--preview-arrow-offset));
|
||||
position: absolute;
|
||||
transform: translateX(-50%);
|
||||
width: 0;
|
||||
@ -46,15 +46,27 @@
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
|
||||
img {
|
||||
height: 100%; // Non sprite images are 100%. Sprites will have their size applied by JavaScript
|
||||
img,
|
||||
&::after {
|
||||
height: 100%;
|
||||
left: 0;
|
||||
max-height: none;
|
||||
max-width: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&::after {
|
||||
border-radius: inherit;
|
||||
box-shadow: inset 0 0 0 1px rgba(#000, 15%);
|
||||
content: '';
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
img {
|
||||
// Non sprite images are 100%. Sprites will have their size applied by JavaScript
|
||||
max-height: none;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Seek time text
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
$plyr-preview-padding: $plyr-tooltip-padding !default;
|
||||
$plyr-preview-background: $plyr-tooltip-background !default;
|
||||
$plyr-preview-radius: 6px !default;
|
||||
$plyr-preview-radius: $plyr-menu-radius !default;
|
||||
$plyr-preview-shadow: $plyr-tooltip-shadow !default;
|
||||
$plyr-preview-arrow-size: $plyr-tooltip-arrow-size !default;
|
||||
$plyr-preview-image-background: $plyr-color-gray-200 !default;
|
||||
|
@ -1,9 +1,11 @@
|
||||
@charset "UTF-8";
|
||||
|
||||
// ==========================================================================
|
||||
// Plyr styles
|
||||
// https://github.com/sampotts/plyr
|
||||
// TODO: Review use of BEM classnames
|
||||
// ==========================================================================
|
||||
@charset 'UTF-8';
|
||||
|
||||
@import 'lib/css-vars';
|
||||
|
||||
$css-vars-use-native: true;
|
||||
|
@ -6,7 +6,7 @@ $plyr-control-icon-size: var(--plyr-control-icon-size, 18px) !default;
|
||||
$plyr-control-spacing: var(--plyr-control-spacing, 10px) !default;
|
||||
$plyr-control-padding: calc(#{$plyr-control-spacing} * 0.7);
|
||||
$plyr-control-padding: var(--plyr-control-padding, $plyr-control-padding) !default;
|
||||
$plyr-control-radius: var(--plyr-control-radius, 3px) !default;
|
||||
$plyr-control-radius: var(--plyr-control-radius, 4px) !default;
|
||||
$plyr-control-toggle-checked-background: var(
|
||||
--plyr-control-toggle-checked-background,
|
||||
var(--plyr-color-main, $plyr-color-main)
|
||||
|
@ -2,4 +2,4 @@
|
||||
// Cosmetic
|
||||
// ==========================================================================
|
||||
|
||||
$plyr-tab-focus-color: var(--plyr-tab-focus-color, var(--plyr-color-main, $plyr-color-main)) !default;
|
||||
$plyr-focus-visible-color: var(--plyr-focus-visible-color, var(--plyr-color-main, $plyr-color-main)) !default;
|
||||
|
@ -3,7 +3,7 @@
|
||||
// ==========================================================================
|
||||
|
||||
$plyr-menu-background: var(--plyr-menu-background, rgba(#fff, 0.9)) !default;
|
||||
$plyr-menu-radius: var(--plyr-menu-radius, 4px) !default;
|
||||
$plyr-menu-radius: var(--plyr-menu-radius, 8px) !default;
|
||||
$plyr-menu-color: var(--plyr-menu-color, $plyr-color-gray-700) !default;
|
||||
$plyr-menu-shadow: var(--plyr-menu-shadow, 0 1px 2px rgba(#000, 0.15)) !default;
|
||||
$plyr-menu-arrow-size: var(--plyr-menu-arrow-size, 4px) !default;
|
||||
|
@ -2,10 +2,10 @@
|
||||
// Tooltips
|
||||
// ==========================================================================
|
||||
|
||||
$plyr-tooltip-background: var(--plyr-tooltip-background, rgba(#fff, 0.9)) !default;
|
||||
$plyr-tooltip-background: var(--plyr-tooltip-background, #fff) !default;
|
||||
$plyr-tooltip-color: var(--plyr-tooltip-color, $plyr-color-gray-700) !default;
|
||||
$plyr-tooltip-padding: calc(#{$plyr-control-spacing} / 2);
|
||||
$plyr-tooltip-padding: var(--plyr-tooltip-padding, $plyr-tooltip-padding) !default;
|
||||
$plyr-tooltip-arrow-size: var(--plyr-tooltip-arrow-size, 4px) !default;
|
||||
$plyr-tooltip-radius: var(--plyr-tooltip-radius, 3px) !default;
|
||||
$plyr-tooltip-radius: var(--plyr-tooltip-radius, 5px) !default;
|
||||
$plyr-tooltip-shadow: var(--plyr-tooltip-shadow, 0 1px 2px rgba(0, 0, 0, 0.15)) !default;
|
||||
|
@ -11,7 +11,6 @@
|
||||
@include plyr-fullscreen-active;
|
||||
|
||||
bottom: 0;
|
||||
display: block;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
|
@ -17,7 +17,7 @@
|
||||
|
||||
// Control elements
|
||||
.plyr--audio .plyr__control {
|
||||
&.plyr__tab-focus,
|
||||
&:focus-visible,
|
||||
&:hover,
|
||||
&[aria-expanded='true'] {
|
||||
background: $plyr-audio-control-background-hover;
|
||||
|
@ -6,7 +6,6 @@
|
||||
|
||||
// Container
|
||||
.plyr--video {
|
||||
background: var(--plyr-video-background, $plyr-video-background);
|
||||
overflow: hidden;
|
||||
|
||||
&.plyr--menu-open {
|
||||
@ -16,6 +15,7 @@
|
||||
|
||||
.plyr__video-wrapper {
|
||||
background: var(--plyr-video-background, $plyr-video-background);
|
||||
border-radius: inherit;
|
||||
height: 100%;
|
||||
margin: auto;
|
||||
overflow: hidden;
|
||||
@ -87,8 +87,7 @@ $embed-padding: (math.div(100, 16) * 9);
|
||||
|
||||
// Control elements
|
||||
.plyr--video .plyr__control {
|
||||
// Hover and tab focus
|
||||
&.plyr__tab-focus,
|
||||
&:focus-visible,
|
||||
&:hover,
|
||||
&[aria-expanded='true'] {
|
||||
background: $plyr-video-control-background-hover;
|
||||
|
@ -118,7 +118,7 @@ Object.entries(build.js).forEach(([filename, entry]) => {
|
||||
},
|
||||
],
|
||||
],
|
||||
plugins: ['@babel/plugin-proposal-class-properties'],
|
||||
plugins: ['@babel/plugin-proposal-class-properties', '@babel/plugin-proposal-optional-chaining'],
|
||||
babelrc: false,
|
||||
exclude: [/\/core-js\//],
|
||||
}),
|
||||
|
Loading…
x
Reference in New Issue
Block a user